403Webshell
Server IP : 68.178.247.200  /  Your IP : 216.73.216.14
Web Server : Apache
System : Linux p3plzcpnl489463.prod.phx3.secureserver.net 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User : x9dppmxs4rgd ( 8559391)
PHP Version : 7.4.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /proc/thread-self/cwd/wp-content/plugins/publishpress/modules/content-overview/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /proc/thread-self/cwd/wp-content/plugins/publishpress/modules/content-overview/content-overview.php
<?php
/**
 * @package PublishPress
 * @author  PublishPress
 *
 * Copyright (c) 2018 PublishPress
 *
 * ------------------------------------------------------------------------------
 * Based on Edit Flow
 * Author: Daniel Bachhuber, Scott Bressler, Mohammad Jangda, Automattic, and
 * others
 * Copyright (c) 2009-2016 Mohammad Jangda, Daniel Bachhuber, et al.
 * ------------------------------------------------------------------------------
 *
 * This file is part of PublishPress
 *
 * PublishPress is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PublishPress is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PublishPress.  If not, see <http://www.gnu.org/licenses/>.
 */

use PublishPress\Core\Ajax;
use PublishPress\Core\Error;
use PublishPress\Notifications\Traits\Dependency_Injector;

/**
 * class PP_Content_Overview
 * This class displays a budgeting system for an editorial desk's publishing workflow.
 *
 * @author sbressler
 */
class PP_Content_Overview extends PP_Module
{
    use Dependency_Injector;

    /**
     * Screen id
     */
    const SCREEN_ID = 'dashboard_page_content-overview';

    /**
     * Usermeta key prefix
     */
    const USERMETA_KEY_PREFIX = 'PP_Content_Overview_';

    /**
     * Default number of columns
     */
    const DEFAULT_NUM_COLUMNS = 1;

    /**
     * @var string
     */
    const MENU_SLUG = 'pp-content-overview';

    /**
     * [$taxonomy_used description]
     *
     * @var string
     */
    public $taxonomy_used = 'category';

    /**
     * [$module description]
     *
     * @var [type]
     */
    public $module;

    /**
     * [$num_columns description]
     *
     * @var integer
     */
    public $num_columns = 0;

    /**
     * [$max_num_columns description]
     *
     * @var [type]
     */
    public $max_num_columns;

    /**
     * [$no_matching_posts description]
     *
     * @var boolean
     */
    public $no_matching_posts = true;

    /**
     * [$terms description]
     *
     * @var array
     */
    public $terms = [];

    /**
     * @var array
     */
    public $columns;

    /**
     * [$user_filters description]
     *
     * @var [type]
     */
    public $user_filters;

    /**
     * Custom methods
     *
     * @var Array
     */
    private $terms_options = [];

    /**
     * Register the module with PublishPress but don't do anything else
     */
    public function __construct()
    {
        $this->module_url = $this->get_module_url(__FILE__);

        // Register the module with PublishPress
        $args = [
            'title' => esc_html__('Content Overview', 'publishpress'),
            'short_description' => false,
            'extended_description' => false,
            'module_url' => $this->module_url,
            'icon_class' => 'dashicons dashicons-list-view',
            'slug' => 'content-overview',
            'default_options' => [
                'enabled' => 'on',
                'post_types' => [
                    'post' => 'on',
                    'page' => 'off',
                ],
            ],
            'general_options' => true,
            'options_page' => false,
            'autoload' => false,
            'add_menu' => true,
            'page_link' => admin_url('admin.php?page=content-overview'),
        ];

        $this->module = PublishPress()->register_module('content_overview', $args);
    }

    /**
     * Initialize the rest of the stuff in the class if the module is active
     */
    public function init()
    {
        if (false === is_admin()) {
            return;
        }

        $this->setDefaultCapabilities();

        if (! $this->currentUserCanViewContentOverview()) {
            return;
        }

        $this->num_columns = $this->get_num_columns();
        $this->max_num_columns = apply_filters('PP_Content_Overview_max_num_columns', 3);

        // Filter to allow users to pick a taxonomy other than 'category' for sorting their posts
        $this->taxonomy_used = apply_filters('PP_Content_Overview_taxonomy_used', $this->taxonomy_used);

        add_action('admin_init', [$this, 'handle_form_date_range_change']);

        add_action('admin_init', [$this, 'handle_screen_options']);

        // Register our settings
        add_action('admin_init', [$this, 'register_settings']);

        add_action('admin_init', [$this, 'register_columns']);

        add_action('wp_ajax_publishpress_content_overview_search_authors', [$this, 'sendJsonSearchAuthors']);
        add_action('wp_ajax_publishpress_content_overview_search_categories', [$this, 'sendJsonSearchCategories']);

        // Menu
        add_filter('publishpress_admin_menu_slug', [$this, 'filter_admin_menu_slug'], 20);
        add_action('publishpress_admin_menu_page', [$this, 'action_admin_menu_page'], 20);
        add_action('publishpress_admin_submenu', [$this, 'action_admin_submenu'], 20);

        // Load necessary scripts and stylesheets
        add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
        add_action('admin_enqueue_scripts', [$this, 'action_enqueue_admin_styles']);
    }

    private function getViewCapability()
    {
        return apply_filters('pp_view_content_overview_cap', 'pp_view_content_overview');
    }

    private function currentUserCanViewContentOverview()
    {
        return current_user_can($this->getViewCapability());
    }

    public function setDefaultCapabilities()
    {
        $role = get_role('administrator');

        $view_content_overview_cap = $this->getViewCapability();

        if (! $role->has_cap($view_content_overview_cap)) {
            $role->add_cap($view_content_overview_cap);
        }
    }

    /**
     * Get the number of columns to show on the content overview
     */
    public function get_num_columns()
    {
        if (empty($this->num_columns)) {
            $current_user = wp_get_current_user();
            $this->num_columns = $this->get_user_meta(
                $current_user->ID,
                self::USERMETA_KEY_PREFIX . 'screen_columns',
                true
            );
            // If usermeta didn't have a value already, use a default value and insert into DB
            if (empty($this->num_columns)) {
                $this->num_columns = self::DEFAULT_NUM_COLUMNS;
                $this->save_column_prefs([self::USERMETA_KEY_PREFIX . 'screen_columns' => $this->num_columns]);
            }
        }

        return $this->num_columns;
    }

    /**
     * Save the current user's preference for number of columns.
     */
    public function save_column_prefs($posted_fields)
    {
        $key = self::USERMETA_KEY_PREFIX . 'screen_columns';
        $this->num_columns = (int)$posted_fields[$key];

        $current_user = wp_get_current_user();
        $this->update_user_meta($current_user->ID, $key, $this->num_columns);
    }

    public function handle_screen_options()
    {
        include_once PUBLISHPRESS_BASE_PATH . '/common/php/' . 'screen-options.php';

        if (function_exists('add_screen_options_panel')) {
            add_screen_options_panel(
                self::USERMETA_KEY_PREFIX . 'screen_columns',
                esc_html__('Screen Layout', 'publishpress'),
                [$this, 'print_column_prefs'],
                self::SCREEN_ID,
                [$this, 'save_column_prefs'],
                true
            );
        }
    }

    /**
     * Register settings for notifications so we can partially use the Settings API
     * (We use the Settings API for form generation, but not saving)
     *
     * @since 0.7
     * @uses  add_settings_section(), add_settings_field()
     */
    public function register_settings()
    {
        add_settings_section(
            $this->module->options_group_name . '_general',
            false,
            '__return_false',
            $this->module->options_group_name
        );

        add_settings_field(
            'post_types',
            esc_html__('Add to these post types:', 'publishpress'),
            [$this, 'settings_post_types_option'],
            $this->module->options_group_name,
            $this->module->options_group_name . '_general'
        );
    }

    /**
     * Choose the post types for editorial metadata
     *
     * @since 0.7
     */
    public function settings_post_types_option()
    {
        global $publishpress;
        $publishpress->settings->helper_option_custom_post_type($this->module);
    }

    /**
     * Get the post types for editorial metadata
     *
     * @return array $post_types All existing post types
     *
     * @since 0.7
     */
    public function get_settings_post_types()
    {
        global $publishpress;
        return $publishpress->settings->get_supported_post_types_for_module($this->module);
    }


    /**
     * Validate data entered by the user
     *
     * @param array $new_options New values that have been entered by the user
     *
     * @return array $new_options Form values after they've been sanitized
     * @since 0.7
     *
     */
    public function settings_validate($new_options)
    {
        // Whitelist validation for the post type options
        if (! isset($new_options['post_types'])) {
            $new_options['post_types'] = [];
        }
        $new_options['post_types'] = $this->clean_post_type_options(
            $new_options['post_types'],
            $this->module->post_type_support
        );

        return $new_options;
    }

    /**
     * Settings page for notifications
     *
     * @since 0.7
     */
    public function print_configure_view()
    {
        settings_fields($this->module->options_group_name);
        do_settings_sections($this->module->options_group_name);
    }

    /**
     * Give users the appropriate permissions to view the content overview the first time the module is loaded
     *
     * @since 0.7
     */
    public function install()
    {
    }

    /**
     * Upgrade our data in case we need to
     *
     * @since 0.7
     */
    public function upgrade($previous_version)
    {
        global $publishpress;

        // Upgrade path to v0.7
        if (version_compare($previous_version, '0.7', '<')) {
            // Migrate whether the content overview was enabled or not and clean up old option
            if ($enabled = get_option('publishpress_content_overview_enabled')) {
                $enabled = 'on';
            } else {
                $enabled = 'off';
            }
            $publishpress->update_module_option($this->module->name, 'enabled', $enabled);
            delete_option('publishpress_content_overview_enabled');

            // Technically we've run this code before so we don't want to auto-install new data
            $publishpress->update_module_option($this->module->name, 'loaded_once', true);
        }
    }

    /**
     * Filters the menu slug.
     *
     * @param $menu_slug
     *
     * @return string
     */
    public function filter_admin_menu_slug($menu_slug)
    {
        if (empty($menu_slug) && $this->module_enabled('content_overview')) {
            $menu_slug = self::MENU_SLUG;
        }

        return $menu_slug;
    }

    /**
     * Creates the admin menu if there is no menu set.
     */
    public function action_admin_menu_page()
    {
        $publishpress = $this->get_service('publishpress');

        if ($publishpress->get_menu_slug() !== self::MENU_SLUG) {
            return;
        }

        $publishpress->add_menu_page(
            esc_html__('Content Overview', 'publishpress'),
            apply_filters('pp_view_content_overview_cap', 'pp_view_calendar'),
            self::MENU_SLUG,
            [$this, 'render_admin_page']
        );
    }

    /**
     * Add necessary things to the admin menu
     */
    public function action_admin_submenu()
    {
        $publishpress = $this->get_service('publishpress');

        // Main Menu
        add_submenu_page(
            $publishpress->get_menu_slug(),
            esc_html__('Content Overview', 'publishpress'),
            esc_html__('Content Overview', 'publishpress'),
            apply_filters('pp_view_content_overview_cap', 'pp_view_calendar'),
            self::MENU_SLUG,
            [$this, 'render_admin_page'],
            20
        );
    }

    /**
     * Enqueue necessary admin scripts only on the content overview page.
     *
     * @uses enqueue_admin_script()
     */
    public function enqueue_admin_scripts()
    {
        global $pagenow;

        // Only load calendar styles on the calendar page
        if ('admin.php' === $pagenow && isset($_GET['page']) && $_GET['page'] === 'pp-content-overview') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $num_columns = $this->get_num_columns();
            echo '<script type="text/javascript"> var PP_Content_Overview_number_of_columns="' . esc_js(
                    $this->num_columns
                ) . '";</script>';

            $this->enqueue_datepicker_resources();
            wp_enqueue_script(
                'publishpress-content_overview',
                $this->module_url . 'lib/content-overview.js',
                ['publishpress-date_picker', 'publishpress-select2'],
                PUBLISHPRESS_VERSION,
                true
            );

            wp_enqueue_script(
                'publishpress-select2',
                PUBLISHPRESS_URL . 'common/libs/select2-v4.0.13.1/js/select2.min.js',
                ['jquery'],
                PUBLISHPRESS_VERSION
            );

            wp_localize_script(
                'publishpress-content_overview',
                'PPContentOverview',
                [
                    'nonce' => wp_create_nonce('content_overview_filter_nonce'),
                ]
            );
        }
    }

    /**
     * Enqueue a screen and print stylesheet for the content overview.
     */
    public function action_enqueue_admin_styles()
    {
        global $pagenow;

        // Only load calendar styles on the calendar page
        if ('admin.php' === $pagenow && isset($_GET['page']) && $_GET['page'] === 'pp-content-overview') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            wp_enqueue_style(
                'pp-admin-css',
                PUBLISHPRESS_URL . 'common/css/publishpress-admin.css',
                ['publishpress-select2'],
                PUBLISHPRESS_VERSION,
                'screen'
            );
            wp_enqueue_style(
                'publishpress-content_overview-styles',
                $this->module_url . 'lib/content-overview.css',
                false,
                PUBLISHPRESS_VERSION,
                'screen'
            );
            wp_enqueue_style(
                'publishpress-content_overview-print-styles',
                $this->module_url . 'lib/content-overview-print.css',
                false,
                PUBLISHPRESS_VERSION,
                'print'
            );

            wp_enqueue_style(
                'publishpress-select2',
                PUBLISHPRESS_URL . 'common/libs/select2-v4.0.13.1/css/select2.min.css',
                false,
                PUBLISHPRESS_VERSION,
                'screen'
            );
        }
    }

    /**
     * Register the columns of information that appear for each term module.
     * Modeled after how WP_List_Table works, but focused on hooks instead of OOP extending
     *
     * @since 0.7
     */
    public function register_columns()
    {
        $columns = [
            'post_title' => esc_html__('Title', 'publishpress'),
            'post_status' => esc_html__('Status', 'publishpress'),
            'post_author' => esc_html__('Author', 'publishpress'),
            'post_date' => esc_html__('Post Date', 'publishpress'),
            'post_modified' => esc_html__('Last Modified', 'publishpress'),
        ];

        /**
         * @param array $columns
         *
         * @return array
         * @deprecated Use publishpress_content_overview_columns
         */
        $columns = apply_filters('PP_Content_Overview_term_columns', $columns);

        /**
         * @param array $columns
         *
         * @return array
         */
        $columns = apply_filters('publishpress_content_overview_columns', $columns);

        if (class_exists('PP_Editorial_Metadata')) {
            $additional_terms = get_terms(
                [
                    'taxonomy' => PP_Editorial_Metadata::metadata_taxonomy,
                    'orderby' => 'name',
                    'order' => 'asc',
                    'hide_empty' => 0,
                    'parent' => 0,
                    'fields' => 'all',
                ]
            );

            $additional_terms = apply_filters('PP_Content_Overview_filter_terms', $additional_terms);
            foreach ($additional_terms as $term) {
                if (! is_object($term) || $term->taxonomy !== PP_Editorial_Metadata::metadata_taxonomy) {
                    continue;
                }

                $term_options = $this->get_unencoded_description($term->description);

                if (! isset($term_options['viewable']) ||
                    (bool)$term_options['viewable'] === false ||
                    isset($columns[$term->slug])) {
                    continue;
                }

                $this->terms_options[$term->slug] = $term_options;

                $columns[$term->slug] = $term->name;
            }

            $this->columns = $columns;
        }
    }

    /**
     * Handle a form submission to change the user's date range on the budget
     *
     * @since 0.7
     */
    public function handle_form_date_range_change()
    {
        if (
            ! isset(
                $_POST['pp-content-overview-number-days'],
                $_POST['pp-content-overview-start-date_hidden'],
                $_POST['pp-content-overview-range-use-today'],
                $_POST['nonce']
            )
            || (
                ! isset($_POST['pp-content-overview-range-submit'])
                && $_POST['pp-content-overview-range-use-today'] == '0'
            )
        ) {
            return;
        }

        if (! wp_verify_nonce(sanitize_key($_POST['nonce']), 'change-date')) {
            wp_die(esc_html($this->module->messages['nonce-failed']));
        }

        $current_user = wp_get_current_user();
        $user_filters = $this->get_user_meta(
            $current_user->ID,
            self::USERMETA_KEY_PREFIX . 'filters',
            true
        );

        $use_today_as_start_date = (bool)$_POST['pp-content-overview-range-use-today'];

        $start_date_format = 'Y-m-d';
        $user_filters['start_date'] = $use_today_as_start_date
            ? current_time($start_date_format)
            : date($start_date_format, strtotime(sanitize_text_field($_POST['pp-content-overview-start-date_hidden']))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

        $user_filters['number_days'] = (int)$_POST['pp-content-overview-number-days'];

        if ($user_filters['number_days'] <= 1) {
            $user_filters['number_days'] = 1;
        }

        $this->update_user_meta($current_user->ID, self::USERMETA_KEY_PREFIX . 'filters', $user_filters);
        wp_redirect(menu_page_url('pp-content-overview', false));

        exit;
    }

    /**
     * Print column number preferences for screen options
     */
    public function print_column_prefs()
    {
        $return_val = esc_html__('Number of Columns: ', 'publishpress');

        for ($i = 1; $i <= $this->max_num_columns; ++$i) {
            $return_val .= "<label><input type='radio' name='" . esc_attr(
                    self::USERMETA_KEY_PREFIX
                ) . "screen_columns' value='" . esc_attr($i) . "' " . checked(
                    $this->get_num_columns(),
                    $i,
                    false
                ) . " />&nbsp;" . esc_attr($i) . "</label>\n";
        }

        return $return_val;
    }

    /**
     * Create the content overview view. This calls lots of other methods to do its work. This will
     * output any messages, create the table navigation, then print the columns based on
     * get_num_columns(), which will in turn print the stories themselves.
     */
    public function render_admin_page()
    {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        global $publishpress;

        // Update the current user's filters with the variables set in $_GET
        $this->user_filters = $this->update_user_filters();

        if (! empty($this->user_filters['cat'])) {
            $terms = [];
            $terms[] = get_term($this->user_filters['cat'], $this->taxonomy_used);
        } else {
            // Get all of the terms from the taxonomy, regardless whether there are published posts
            $args = [
                'orderby' => 'name',
                'order' => 'asc',
                'hide_empty' => 0,
                'parent' => 0,
            ];
            $terms = get_terms($this->taxonomy_used, $args);
        }

        if (class_exists('PP_Editorial_Metadata')) {
            $this->terms = array_filter(
            // allow for reordering or any other filtering of terms
                apply_filters('PP_Content_Overview_filter_terms', $terms),
                function ($term) {
                    if ($term->taxonomy !== PP_Editorial_Metadata::metadata_taxonomy) {
                        return true;
                    }

                    $term_options = $this->get_unencoded_description($term->description);

                    return isset($term_options['viewable']) && (bool)$term_options['viewable'];
                }
            );
        } else {
            // allow for reordering or any other filtering of terms
            $this->terms = apply_filters('PP_Content_Overview_filter_terms', $terms);
        }

        $description = sprintf(
            '%s <span class="time-range">%s</span>',
            esc_html__('Content Overview', 'publishpress'),
            $this->content_overview_time_range()
        );
        $publishpress->settings->print_default_header($publishpress->modules->content_overview, $description); ?>
        <div class="wrap" id="pp-content-overview-wrap">
            <?php
            $this->print_messages(); ?>
            <?php
            $this->table_navigation(); ?>

            <div class="metabox-holder">
                <?php
                if (isset($_GET['ptype']) && ! empty($_GET['ptype'])) {
                    $selectedPostTypes = [sanitize_text_field($_GET['ptype'])];
                } else {
                    $selectedPostTypes = $this->get_selected_post_types();
                }

                foreach ($selectedPostTypes as $postType) {
                    echo '<div class="postbox-container">';
                    $this->printPostForPostType(null, $postType);
                    echo '</div>';
                }
                ?>
            </div>
        </div>
        <br clear="all">
        <?php

        $publishpress->settings->print_default_footer($publishpress->modules->content_overview);
        // phpcs:enable
    }

    public function get_selected_post_types()
    {
        $postTypesOption = $this->module->options->post_types;

        $enabledPostTypes = [];
        foreach ($postTypesOption as $postType => $status) {
            if ('on' === $status
                && ! in_array($postType, $enabledPostTypes)) {
                $enabledPostTypes[] = $postType;
            }
        }

        return $enabledPostTypes;
    }

    /**
     * Update the current user's filters for content overview display with the filters in $_GET. The filters
     * in $_GET take precedence over the current users filters if they exist.
     */
    public function update_user_filters()
    {
        $current_user = wp_get_current_user();

        $user_filters = [
            'post_status' => $this->filter_get_param('post_status'),
            'cat' => $this->filter_get_param('cat'),
            'author' => $this->filter_get_param('author'),
            'start_date' => $this->filter_get_param('start_date'),
            'number_days' => $this->filter_get_param('number_days'),
            'ptype' => $this->filter_get_param('ptype'),
        ];

        $current_user_filters = [];
        $current_user_filters = $this->get_user_meta($current_user->ID, self::USERMETA_KEY_PREFIX . 'filters', true);

        // If any of the $_GET vars are missing, then use the current user filter
        foreach ($user_filters as $key => $value) {
            if (is_null($value) && ! empty($current_user_filters[$key])) {
                $user_filters[$key] = $current_user_filters[$key];
            }
        }

        if (! $user_filters['start_date']) {
            $user_filters['start_date'] = date('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        }

        if (! $user_filters['number_days']) {
            $user_filters['number_days'] = 10;
        }

        $user_filters = apply_filters('PP_Content_Overview_filter_values', $user_filters, $current_user_filters);

        $this->update_user_meta($current_user->ID, self::USERMETA_KEY_PREFIX . 'filters', $user_filters);

        return $user_filters;
    }

    /**
     *
     * @param string $param The parameter to look for in $_GET
     *
     * @return null if the parameter is not set in $_GET, empty string if the parameter is empty in $_GET,
     *                      or a sanitized version of the parameter from $_GET if set and not empty
     */
    public function filter_get_param($param)
    {
        // Sure, this could be done in one line. But we're cooler than that: let's make it more readable!
        if (! isset($_GET[$param])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            return null;
        } elseif (empty($_GET[$param])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            return '';
        }

        return sanitize_key($_GET[$param]); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    }

    /**
     * Allow the user to define the date range in a new and exciting way
     *
     * @since 0.7
     */
    public function content_overview_time_range()
    {
        $filtered_start_date = $this->user_filters['start_date'];
        $filtered_start_date_timestamp = strtotime($filtered_start_date);

        $output = '<form method="POST" action="' . menu_page_url('pp-content-overview', false) . '">';

        $date_format = get_option('date_format');

        $start_date_value = '<input type="text" id="pp-content-overview-start-date" name="pp-content-overview-start-date"'
            . ' size="10" class="date-pick" data-alt-field="pp-content-overview-start-date_hidden" data-alt-format="' . pp_convert_date_format_to_jqueryui_datepicker(
                'Y-m-d'
            ) . '" value="'
            . esc_attr(date_i18n($date_format, $filtered_start_date_timestamp)) . '" />';
        $start_date_value .= '<input type="hidden" name="pp-content-overview-start-date_hidden" value="' . $filtered_start_date . '" />';
        $start_date_value .= '<span class="form-value">';

        $start_date_value .= esc_html(date_i18n($date_format, $filtered_start_date_timestamp));
        $start_date_value .= '</span>';

        $number_days_value = '<input type="text" id="pp-content-overview-number-days" name="pp-content-overview-number-days"'
            . ' size="3" maxlength="3" value="'
            . esc_attr($this->user_filters['number_days']) . '" /><span class="form-value">' . esc_html(
                $this->user_filters['number_days']
            )
            . '</span>';

        $output .= sprintf(
            _x(
                'starting %1$s showing %2$s %3$s',
                '%1$s = start date, %2$s = number of days, %3$s = translation of \'Days\'',
                'publishpress'
            ),
            $start_date_value,
            $number_days_value,
            _n('day', 'days', $this->user_filters['number_days'], 'publishpress')
        );
        $output .= '&nbsp;&nbsp;<span class="change-date-buttons">';
        $output .= '<input id="pp-content-overview-range-submit" name="pp-content-overview-range-submit" type="submit"';
        $output .= ' class="button button-primary hidden" value="' . esc_html__('Change', 'publishpress') . '" />';
        $output .= '&nbsp;';
        $output .= '<input id="pp-content-overview-range-today-btn" name="pp-content-overview-range-today-btn" type="submit"';
        $output .= ' class="button button-secondary hidden" value="' . esc_html__('Reset', 'publishpress') . '" />';
        $output .= '<input id="pp-content-overview-range-use-today" name="pp-content-overview-range-use-today" value="0" type="hidden" />';
        $output .= '&nbsp;';
        $output .= '<a class="change-date-cancel hidden" href="#">' . esc_html__('Cancel', 'publishpress') . '</a>';
        $output .= '<a class="change-date" href="#">' . esc_html__('Change', 'publishpress') . '</a>';
        $output .= wp_nonce_field('change-date', 'nonce', 'change-date-nonce', false);
        $output .= '</span></form>';

        return $output;
    }

    /**
     * Print any messages that should appear based on the action performed
     */
    public function print_messages()
    {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        if (isset($_GET['trashed']) || isset($_GET['untrashed'])) {
            echo '<div id="trashed-message" class="updated"><p>';

            // Following mostly stolen from edit.php
            if (isset($_GET['trashed']) && (int)$_GET['trashed']) {
                $count = (int)$_GET['trashed'];

                echo esc_html(_n('Item moved to the trash.', '%d items moved to the trash.', $count));
                $ids = isset($_GET['ids']) ? sanitize_text_field($_GET['ids']) : 0;
                echo ' <a href="' . esc_url(
                        wp_nonce_url(
                            "edit.php?post_type=post&doaction=undo&action=untrash&ids=$ids",
                            "bulk-posts"
                        )
                    ) . '">' . esc_html__('Undo', 'publishpress') . '</a><br />';
                unset($_GET['trashed']);
            }

            if (isset($_GET['untrashed']) && (int)$_GET['untrashed']) {
                $count = (int)$_GET['untrashed'];

                echo esc_html(_n(
                    'Item restored from the Trash.',
                    '%d items restored from the Trash.',
                    $count
                ));
                unset($_GET['undeleted']);
            }

            echo '</p></div>';
        }
        // phpcs:enable
    }

    /**
     * Print the table navigation and filter controls, using the current user's filters if any are set.
     */
    public function table_navigation()
    {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        ?>
        <div class="tablenav" id="pp-content-overview-tablenav">
            <div class="alignleft actions">
                <form method="GET" id="pp-content-filters">
                    <input type="hidden" name="page" value="pp-content-overview"/>
                    <?php
                    foreach ($this->content_overview_filters() as $select_id => $select_name) {
                        $this->content_overview_filter_options($select_id, $select_name, $this->user_filters); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                    } ?>
                </form>

                <form method="GET" id="pp-content-filters-hidden">
                    <input type="hidden" name="page" value="pp-content-overview"/>
                    <input type="hidden" name="post_status" value=""/>
                    <input type="hidden" name="cat" value=""/>
                    <input type="hidden" name="author" value=""/>
                    <input type="hidden" name="orderby" value="<?php
                    echo (isset($_GET['orderby']) && ! empty($_GET['orderby'])) ?
                        esc_attr(sanitize_key($_GET['orderby'])) : 'post_date'; ?>"/>
                    <input type="hidden" name="order" value="<?php
                    echo (isset($_GET['order']) && ! empty($_GET['order'])) ? esc_attr(sanitize_key($_GET['order'])) : 'ASC'; ?>"/>
                    <?php
                    foreach ($this->content_overview_filters() as $select_id => $select_name) {
                        echo '<input type="hidden" name="' . esc_attr($select_name) . '" value="" />';
                    } ?>
                    <input type="submit" id="post-query-clear" value="<?php
                    echo esc_attr(__('Reset', 'publishpress')); ?>"
                           class="button-secondary button"/>
                </form>
            </div><!-- /alignleft actions -->

            <div class="print-box" style="float:right; margin-right: 30px;"><!-- Print link -->
                <a href="#" id="print_link"><span
                            class="pp-icon pp-icon-print"></span>&nbsp;<?php
                    echo esc_attr(__('Print', 'publishpress')); ?>
                </a>
            </div>
            <div class="clear"></div>
        </div><!-- /tablenav -->
        <?php
        // phpcs:enable
    }

    public function content_overview_filters()
    {
        $select_filter_names = [];


        $select_filter_names['post_status'] = 'post_status';

        if (isset($this->module->options->post_types['post']) && $this->module->options->post_types['post'] == 'on') {
            $select_filter_names['cat'] = 'cat';
        }

        $select_filter_names['author'] = 'author';
        $select_filter_names['ptype'] = 'ptype';

        return apply_filters('PP_Content_Overview_filter_names', $select_filter_names);
    }

    public function content_overview_filter_options($select_id, $select_name, $filters)
    {
        switch ($select_id) {
            case 'post_status':
                $post_statuses = $this->get_post_statuses();
                ?>
                <select id="post_status" name="post_status"><!-- Status selectors -->
                    <option value=""><?php
                        _e('View all statuses', 'publishpress'); ?></option>
                    <?php
                    foreach ($post_statuses as $post_status) {
                        echo "<option value='" . esc_attr($post_status->slug) . "' " . selected(
                                $post_status->slug,
                                $filters['post_status']
                            ) . ">" . esc_html($post_status->name) . "</option>";
                    }
                    ?>
                </select>
                <?php
                break;

            case 'cat':
                $categoryId = isset($filters['cat']) ? (int)$filters['cat'] : 0;
                ?>
                <select id="filter_category" name="cat">
                    <option value=""><?php
                        _e('View all categories', 'publishpress'); ?></option>
                    <?php
                    if (! empty($categoryId)) {
                        $category = get_term($categoryId, 'category');

                        echo "<option value='" . esc_attr($categoryId) . "' selected='selected'>" . esc_html(
                                $category->name
                            ) . "</option>";
                    }
                    ?>
                </select>
                <?php
                break;

            case 'author':
                $authorId = isset($filters['author']) ? (int)$filters['author'] : 0;
                $selectedOptionAll = empty($authorId) ? 'selected="selected"' : '';
                ?>
                <select id="filter_author" name="author">
                    <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
                    <option value="" <?php echo $selectedOptionAll; ?>>
                        <?php esc_html_e('All authors', 'publishpress'); ?>
                    </option>
                    <?php
                    if (! empty($authorId)) {
                        $author = get_user_by('id', $authorId);
                        $option = '';

                        if (! empty($author)) {
                            $option = '<option value="' . esc_attr($authorId) . '" selected="selected">' . esc_html(
                                    $author->display_name
                                ) . '</option>';
                        }

                        $option = apply_filters('publishpress_author_filter_selected_option', $option, $authorId);

                        echo $option; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                    }
                    ?>
                </select>
                <?php
                break;

            case 'ptype':
                $selectedPostType = isset($filters['ptype']) ? sanitize_text_field($filters['ptype']) : '';
                ?>
                <select id="filter_post_type" name="ptype">
                    <option value=""><?php
                        _e('View all post types', 'publishpress'); ?></option>
                    <?php
                    $postTypes = $this->get_selected_post_types();
                    foreach ($postTypes as $postType) {
                        $postTypeObject = get_post_type_object($postType);
                        echo '<option value="' . esc_attr($postType) . '" ' . selected(
                                $selectedPostType,
                                $postType
                            ) . '>' . esc_html($postTypeObject->label) . '</option>';
                    }
                    ?>
                </select>
                <?php
                break;

            default:
                do_action('PP_Content_Overview_filter_display', $select_id, $select_name, $filters);
                break;
        }
    }

    /**
     * Prints the stories in a single term in the content overview.
     *
     * @param object $term The term to print.
     * @param string $postType
     */
    public function printPostForPostType($term, $postType)
    {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        $order = (isset($_GET['order']) && ! empty($_GET['order'])) ? sanitize_key($_GET['order']) : 'ASC';
        $orderBy = (isset($_GET['orderby']) && ! empty($_GET['orderby'])) ? sanitize_key($_GET['orderby']) : 'post_date';

        $this->user_filters['orderby'] = $orderBy;
        $this->user_filters['order'] = $order;

        $posts = $this->getPostsForPostType($term, $postType, $this->user_filters);
        $postTypeObject = get_post_type_object($postType);
        $sortableColumns = $this->getSortableColumns();

        if (! empty($posts)) {
            // Don't display the message for $no_matching_posts
            $this->no_matching_posts = false;
        } ?>

        <div class="postbox<?php
        echo (! empty($posts)) ? ' postbox-has-posts' : ''; ?>">
            <div class="handlediv" title="<?php echo esc_attr(__('Click to toggle', 'publishpress')); ?>">
                <br/></div>
            <h3 class=\'hndle\'><span><?php echo esc_html($postTypeObject->label); ?></span></h3>
            <div class="inside">
                <?php
                if (! empty($posts)) : ?>
                    <table class="widefat post fixed content-overview striped" cellspacing="0">
                        <thead>
                        <tr>
                            <?php
                            foreach ((array)$this->columns as $key => $name): ?>
                                <?php
                                $key = sanitize_key($key);

                                $newOrder = 'ASC';
                                if ($key === $orderBy) :
                                    $newOrder = ($order === 'ASC') ? 'DESC' : 'ASC';
                                endif;
                                ?>
                                <th scope="col" id="<?php echo esc_attr($key); ?>"
                                    class="manage-column column-<?php echo esc_attr($key); ?>">
                                    <?php
                                    if (in_array($key, $sortableColumns)) : ?>
                                        <a href="<?php
                                        echo esc_url(add_query_arg(
                                            ['orderby' => $key, 'order' => $newOrder]
                                        )); ?>">
                                            <?php
                                            echo esc_html($name); ?>
                                            <?php
                                            if ($orderBy === $key) : ?>
                                                <?php
                                                $orderIconClass = $order === 'DESC' ? 'dashicons-arrow-down-alt2' : 'dashicons-arrow-up-alt2'; ?>
                                                <i class="dashicons <?php echo esc_attr($orderIconClass); ?>"></i>
                                            <?php
                                            endif; ?>
                                        </a>
                                    <?php
                                    else: ?>
                                        <?php
                                        echo esc_html($name); ?>
                                    <?php
                                    endif; ?>
                                </th>
                            <?php
                            endforeach; ?>
                        </tr>
                        </thead>
                        <tfoot></tfoot>
                        <tbody>
                        <?php
                        foreach ($posts as $post) {
                            $this->print_post($post, $term);
                        } ?>
                        </tbody>
                    </table>
                <?php
                else: ?>
                    <div class="message info">
                        <p><?php
                            esc_html_e(
                                'There are no posts in the range or filter specified.',
                                'publishpress'
                            ); ?></p>
                    </div>
                <?php
                endif; ?>
            </div>
        </div>
        <?php
        // phpcs:enable
    }

    private function getSortableColumns()
    {
        $sortableColumns = [
            'post_title',
            'post_date',
            'post_modified',
        ];

        return apply_filters('publishpress_content_overview_sortable_columns', $sortableColumns);
    }

    /**
     * Get all of the posts for a given term based on filters
     *
     * @param object $term The term we're getting posts for
     * @param string $postType
     * @param array $args
     *
     * @return array $term_posts An array of post objects for the term
     */
    public function getPostsForPostType($term, $postType, $args = null)
    {
        $defaults = [
            'post_status' => null,
            'author' => null,
            'posts_per_page' => (int)apply_filters('PP_Content_Overview_max_query', 200),
        ];

        $args = array_merge($defaults, $args);

        if ($postType === 'post' && ! empty($term)) {
            // Filter to the term and any children if it's hierarchical
            $arg_terms = [
                $term->term_id,
            ];

            if (is_object($term) && property_exists($term, 'term_id')) {
                $arg_terms = array_merge($arg_terms, get_term_children($term->term_id, $this->taxonomy_used));
            }

            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
            $args['tax_query'] = [
                [
                    'taxonomy' => $this->taxonomy_used,
                    'field' => 'id',
                    'terms' => $arg_terms,
                    'operator' => 'IN',
                ],
            ];
        }

        $args['post_type'] = $postType;

        // Unpublished as a status is just an array of everything but 'publish'
        if ($args['post_status'] == 'unpublish') {
            $args['post_status'] = '';
            $post_statuses = $this->get_post_statuses();

            foreach ($post_statuses as $post_status) {
                $args['post_status'] .= sanitize_key($post_status->slug) . ', ';
            }

            $args['post_status'] = rtrim($args['post_status'], ', ');

            // Optional filter to include scheduled content as unpublished
            if (apply_filters('pp_show_scheduled_as_unpublished', false)) {
                $args['post_status'] .= ', future';
            }
        }

        // Filter by post_author if it's set
        if ($args['author'] === '0') {
            unset($args['author']);
        }

        // Order the post list by publishing date.
        if (! isset($args['orderby'])) {
            $args['orderby'] = 'post_date';
            $args['order'] = 'ASC';
        }

        // Filter for an end user to implement any of their own query args
        $args = apply_filters('PP_Content_Overview_posts_query_args', $args);

        add_filter('posts_where', [$this, 'posts_where_range']);
        $term_posts_query_results = new WP_Query($args);
        remove_filter('posts_where', [$this, 'posts_where_range']);

        $term_posts = [];
        while ($term_posts_query_results->have_posts()) {
            $term_posts_query_results->the_post();

            global $post;

            $term_posts[] = $post;
        }


        return $term_posts;
    }

    /**
     * Prints a single post within a term in the content overview.
     *
     * @param object $post The post to print.
     * @param object $parent_term The top-level term to which this post belongs.
     */
    public function print_post($post, $parent_term)
    {
        ?>
        <tr id='post-<?php
        echo esc_attr($post->ID); ?>' valign="top">
            <?php
            foreach ((array)$this->columns as $key => $name) {
                echo '<td>';
                if (method_exists($this, 'column_' . $key)) {
                    $method = 'column_' . $key;
                    echo $this->$method($post, $parent_term); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                } else {
                    echo $this->column_default($post, $key, $parent_term); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                }

                echo '</td>';
            } ?>
        </tr>
        <?php
    }

    /**
     * Default callback for producing the HTML for a term column's single post value
     * Includes a filter other modules can hook into
     *
     * @param object $post The post we're displaying
     * @param string $column_name Name of the column, as registered with register_columns
     * @param object $parent_term The parent term for the term column
     *
     * @return string $output Output value for the term column
     * @since 0.7
     *
     */
    public function column_default($post, $column_name)
    {
        // Hook for other modules to get data into columns
        $column_value = null;

        /**
         * @deprecated
         */
        $column_value = apply_filters('PP_Content_Overview_term_column_value', $column_name, $post, null);

        /**
         * @param string $column_name
         * @param WP_Post $post
         *
         * @return string
         */
        $column_value = apply_filters('publishpress_content_overview_column_value', $column_name, $post);

        if (! is_null($column_value) && $column_value != $column_name) {
            return $column_value;
        }

        if (strpos($column_name, '_pp_editorial_meta_') === 0) {
            $column_value = get_post_meta($post->ID, $column_name, true);

            if (empty($column_value)) {
                return '<span>' . esc_html__('None', 'publishpress') . '</span>';
            }

            return $column_value;
        }

        switch ($column_name) {
            case 'post_status':
                $status_name = $this->get_post_status_friendly_name($post->post_status);

                return $status_name;
                break;
            case 'post_author':
                $post_author = get_userdata($post->post_author);
                $author_name = is_object($post_author) ? $post_author->display_name : '';
                $author_name = apply_filters('the_author', $author_name);

                $author_name = apply_filters('publishpress_content_overview_author_column', $author_name, $post);

                return $author_name;
                break;
            case 'post_date':
                $output = get_the_time(get_option('date_format'), $post->ID) . '<br />';
                $output .= get_the_time(get_option('time_format'), $post->ID);

                return $output;
                break;
            case 'post_modified':
                $modified_time_gmt = strtotime($post->post_modified_gmt . " GMT");

                return $this->timesince($modified_time_gmt);
                break;
            default:
                break;
        }

        $meta_options = isset($this->terms_options[$column_name])
            ? $this->terms_options[$column_name]
            : null;

        if (is_null($meta_options)) {
            return '';
        }

        $column_type = $meta_options['type'];
        $column_value = get_post_meta($post->ID, "_pp_editorial_meta_{$column_type}_{$column_name}", true);

        return apply_filters("pp_editorial_metadata_{$column_type}_render_value_html", $column_value);
    }

    /**
     * Filter the WP_Query so we can get a range of posts
     *
     * @param string $where The original WHERE SQL query string
     *
     * @return string $where Our modified WHERE query string
     */
    public function posts_where_range($where = '')
    {
        global $wpdb;

        $beginning_date = date('Y-m-d', strtotime($this->user_filters['start_date'])); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        $end_day = $this->user_filters['number_days'];
        $ending_date = date("Y-m-d", strtotime("+" . $end_day . " days", strtotime($beginning_date))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        $where = $where . $wpdb->prepare(
                " AND ($wpdb->posts.post_date >= %s AND $wpdb->posts.post_date < %s)",
                $beginning_date,
                $ending_date
            );

        return $where;
    }

    /**
     * Prepare the data for the title term column
     *
     * @since 0.7
     */
    public function column_post_title($post, $parent_term)
    {
        $post_title = _draft_or_post_title($post->ID);

        $post_type_object = get_post_type_object($post->post_type);
        $can_edit_post = current_user_can($post_type_object->cap->edit_post, $post->ID);
        if ($can_edit_post) {
            $output = '<strong><a href="' . esc_url(get_edit_post_link($post->ID)) . '">' . esc_html(
                    $post_title
                ) . '</a></strong>';
        } else {
            $output = '<strong>' . esc_html($post_title) . '</strong>';
        }

        // Edit or Trash or View
        $output .= '<div class="row-actions">';
        $item_actions = [];

        if ($can_edit_post) {
            $item_actions['edit'] = '<a title="' . esc_attr(
                    esc_html__(
                        'Edit this post',
                        'publishpress'
                    )
                ) . '" href="' . esc_url(get_edit_post_link($post->ID)) . '">' . esc_html__(
                    'Edit',
                    'publishpress'
                ) . '</a>';
        }

        if (EMPTY_TRASH_DAYS > 0 && current_user_can($post_type_object->cap->delete_post, $post->ID)) {
            $item_actions['trash'] = '<a class="submitdelete" title="' . esc_attr(
                    esc_html__(
                        'Move this item to the Trash',
                        'publishpress'
                    )
                ) . '" href="' . esc_url(get_delete_post_link($post->ID)) . '">' . esc_html__(
                    'Trash',
                    'publishpress'
                ) . '</a>';
        }

        // Display a View or a Preview link depending on whether the post has been published or not
        if (in_array($post->post_status, ['publish'])) {
            $item_actions['view'] = '<a href="' . esc_url(get_permalink($post->ID)) . '" title="' . esc_attr(
                    sprintf(
                        __(
                            'View &#8220;%s&#8221;',
                            'publishpress'
                        ),
                        $post_title
                    )
                ) . '" rel="permalink">' . esc_html__('View', 'publishpress') . '</a>';
        } elseif ($can_edit_post) {
            $item_actions['previewpost'] = '<a href="' . esc_url(
                    apply_filters(
                        'preview_post_link',
                        add_query_arg('preview', 'true', get_permalink($post->ID)),
                        $post
                    )
                ) . '" title="' . esc_attr(
                    sprintf(
                        __('Preview &#8220;%s&#8221;', 'publishpress'),
                        $post_title
                    )
                ) . '" rel="permalink">' . esc_html__('Preview', 'publishpress') . '</a>';
        }

        $item_actions = apply_filters('PP_Content_Overview_item_actions', $item_actions, $post->ID);
        if (count($item_actions)) {
            $output .= '<div class="row-actions">';
            $html = '';
            foreach ($item_actions as $class => $item_action) {
                $html .= '<span class="' . esc_attr($class) . '">' . $item_action . '</span> | ';
            }
            $output .= rtrim($html, '| ');
            $output .= '</div>';
        }

        return $output;
    }

    /**
     * Get the filters for the current user for the content overview display, or insert the default
     * filters if not already set.
     *
     * @return array The filters for the current user, or the default filters if the current user has none.
     */
    public function get_user_filters()
    {
        $current_user = wp_get_current_user();
        $user_filters = [];
        $user_filters = $this->get_user_meta($current_user->ID, self::USERMETA_KEY_PREFIX . 'filters', true);

        // If usermeta didn't have filters already, insert defaults into DB
        if (empty($user_filters)) {
            $user_filters = $this->update_user_filters();
        }

        return $user_filters;
    }

    public function sendJsonSearchAuthors()
    {
        $ajax = Ajax::getInstance();

        if (
            (! isset($_GET['nonce']))
            || (! wp_verify_nonce(sanitize_key($_GET['nonce']), 'content_overview_filter_nonce'))
        ) {
            $ajax->sendJsonError(Error::ERROR_CODE_INVALID_NONCE);
        }

        if (! $this->currentUserCanViewContentOverview()) {
            $ajax->sendJsonError(Error::ERROR_CODE_ACCESS_DENIED);
        }

        $queryText = isset($_GET['q']) ? sanitize_text_field($_GET['q']) : '';

        /**
         * @param array $results
         * @param string $searchText
         */
        $results = apply_filters('publishpress_search_authors_results_pre_search', [], $queryText);

        if (! empty($results)) {
            $ajax->sendJson($results);
        }

        global $wpdb;

        $cacheKey = 'search_authors_result_' . md5($queryText);
        $cacheGroup = 'content_overview';

        $queryResult = wp_cache_get($cacheKey, $cacheGroup);

        if (false === $queryResult) {
            // phpcs:disable WordPressVIPMinimum.Variables.RestrictedVariables.user_meta__wpdb__users
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $queryResult = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT DISTINCT u.ID as 'id', u.display_name as 'text'
                FROM {$wpdb->posts} as p
                INNER JOIN {$wpdb->users} as u ON p.post_author = u.ID
                WHERE u.display_name LIKE %s
                ORDER BY 2
                LIMIT 20",
                    '%' . $wpdb->esc_like($queryText) . '%'
                )
            );
            // phpcs:enable

            wp_cache_set($cacheKey, $queryResult, $cacheGroup);
        }

        $ajax->sendJson($queryResult);
    }

    public function sendJsonSearchCategories()
    {
        $ajax = Ajax::getInstance();

        if (
            (! isset($_GET['nonce']))
            || (! wp_verify_nonce(sanitize_key($_GET['nonce']), 'content_overview_filter_nonce'))
        ) {
            $ajax->sendJsonError(Error::ERROR_CODE_INVALID_NONCE);
        }

        if (! $this->currentUserCanViewContentOverview()) {
            $ajax->sendJsonError(Error::ERROR_CODE_ACCESS_DENIED);
        }

        $queryText = isset($_GET['q']) ? sanitize_text_field($_GET['q']) : '';

        $cacheKey = 'search_categories_result_' . md5($queryText);
        $cacheGroup = 'content_overview';

        $queryResult = wp_cache_get($cacheKey, $cacheGroup);

        if (false === $queryResult) {
            global $wpdb;

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $queryResult = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT DISTINCT t.term_id AS id, t.name AS text
                FROM {$wpdb->term_taxonomy} as tt
                INNER JOIN {$wpdb->terms} as t ON (tt.term_id = t.term_id)
                WHERE taxonomy = 'category' AND t.name LIKE %s
                ORDER BY 2
                LIMIT 20",
                    '%' . $wpdb->esc_like($queryText) . '%'
                )
            );

            wp_cache_set($cacheKey, $queryResult, $cacheGroup);
        }

        $ajax->sendJson($queryResult);
    }
}

Youez - 2016 - github.com/yon3zu
LinuXploit