diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 5397dfc..2643dd1 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -23,7 +23,7 @@ jobs: matrix: # Notes regarding supported versions in WP: # The base matrix only contains the PHP versions which are supported on all supported WP versions. - php: ['7.1', '7.2', '7.3', '7.4'] + php: ['8.0', '7.3', '7.4'] wp: ['latest'] experimental: [false] @@ -31,20 +31,17 @@ jobs: # Complement the builds run via the matrix with high/low WP builds for PHP 7.4 and 8.0. # PHP 8.0 is sort of supported since WP 5.6. # PHP 7.4 is supported since WP 5.3. - - php: '8.0' + - php: '8.3' wp: 'latest' experimental: true - - php: '8.0' - wp: '5.6' - experimental: true - - php: '7.4' - wp: '5.3' + - php: '8.2' + wp: 'latest' experimental: true - - php: '7.1' - wp: '5.1' + - php: '8.1' + wp: 'latest' experimental: true - - php: '7.4' - wp: 'nightly' + - php: '8.0' + wp: '5.9' experimental: true name: "PHP ${{ matrix.php }} - WP ${{ matrix.wp }}" @@ -88,13 +85,15 @@ jobs: id: set_phpunit run: | if [[ "${{ matrix.php }}" > "7.4" ]]; then - echo '::set-output name=PHPUNIT::7.5.*' + echo "PHPUNIT=8.5.*" >> $GITHUB_ENV else - echo '::set-output name=PHPUNIT::5.7.*||6.*||7.5.*' + echo "PHPUNIT=5.7.*||6.*||7.5.*" >> $GITHUB_ENV fi - name: 'Composer: set up PHPUnit' - run: composer require --no-update phpunit/phpunit:"${{ steps.set_phpunit.outputs.PHPUNIT }}" + env: + PHPUNIT: ${{ env.PHPUNIT }} + run: composer require --no-update phpunit/phpunit:"${{ env.PHPUNIT }}" # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-composer-dependencies diff --git a/better-search.php b/better-search.php index 18dee07..89884f7 100644 --- a/better-search.php +++ b/better-search.php @@ -29,48 +29,32 @@ } /** - * Holds the version of Contextual Related Posts. + * Holds the version of Better Search. * * @since 2.9.3 - * - * @var string Contextual Related Posts Version. */ -if ( ! defined( 'BETTER_SEARCH_VERSION' ) ) { - define( 'BETTER_SEARCH_VERSION', '3.2.2' ); -} +define( 'BETTER_SEARCH_VERSION', '3.3.0' ); /** * Holds the filesystem directory path (with trailing slash) for Better Search * * @since 2.2.0 - * - * @var string Plugin folder path */ -if ( ! defined( 'BETTER_SEARCH_PLUGIN_DIR' ) ) { - define( 'BETTER_SEARCH_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -} +define( 'BETTER_SEARCH_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); /** * Holds the filesystem directory path (with trailing slash) for Better Search * * @since 2.2.0 - * - * @var string Plugin folder URL */ -if ( ! defined( 'BETTER_SEARCH_PLUGIN_URL' ) ) { - define( 'BETTER_SEARCH_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); -} +define( 'BETTER_SEARCH_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); /** * Holds the filesystem directory path (with trailing slash) for Better Search * * @since 2.2.0 - * - * @var string Plugin Root File - */ -if ( ! defined( 'BETTER_SEARCH_PLUGIN_FILE' ) ) { - define( 'BETTER_SEARCH_PLUGIN_FILE', __FILE__ ); -} + */ +define( 'BETTER_SEARCH_PLUGIN_FILE', __FILE__ ); /** * Global variable holding the current database version of Better Search @@ -82,83 +66,48 @@ global $bsearch_db_version; $bsearch_db_version = '1.0'; -/* - * ----------------------------------------------------------------------------* - * Include files - *---------------------------------------------------------------------------- +// Load the autoloader. +require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/autoloader.php'; + +/** + * The code that runs during plugin activation. + * + * @since 3.3.0 + * + * @param bool $network_wide Whether the plugin is being activated network-wide. */ +function activate_bsearch( $network_wide ) { + \WebberZone\Better_Search\Admin\Activator::activation_hook( $network_wide ); +} +register_activation_hook( __FILE__, __NAMESPACE__ . '\activate_bsearch' ); - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/register-settings.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/default-settings.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/activation.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/class-better-search.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/class-better-search-query.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/main-functions.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/general-template.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/l10n.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/template-redirect.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/utilities.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/media.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/wp-filters.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/modules/tracker.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/modules/cache.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/modules/class-better-search-heatmap.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/modules/class-bsearch-search-box.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/modules/heatmap.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/modules/shortcode.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/deprecated.php'; +/** + * The main function responsible for returning the one true WebberZone Snippetz instance to functions everywhere. + * + * @since 3.3.0 + */ +function load_bsearch() { + \WebberZone\Better_Search\Main::get_instance(); +} +add_action( 'plugins_loaded', __NAMESPACE__ . '\load_bsearch' ); /* - *----------------------------------------------------------------------------* - * Dashboard and Administrative Functionality + *---------------------------------------------------------------------------- + * Include files *---------------------------------------------------------------------------- */ -if ( is_admin() ) { - - /** - * Load the admin pages if we're in the Admin. - */ - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/admin.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/settings-page.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/save-settings.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/help-tab.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/tools.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/admin-dashboard.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/class-better-search-statistics.php'; - require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/class-better-search-statistics-table.php'; - -} +require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/options-api.php'; +require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/class-better-search.php'; +require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/class-better-search-query.php'; +require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/functions.php'; +require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/general-template.php'; +require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/heatmap.php'; /** * Declare $bsearch_settings global so that it can be accessed in every function * - * @since 1.3 + * @since 1.3 */ global $bsearch_settings; $bsearch_settings = bsearch_get_settings(); - - -/** - * Get Settings. - * - * Retrieves all plugin settings - * - * @since 2.2.0 - * - * @return array Better Search settings - */ -function bsearch_get_settings() { - - $settings = get_option( 'bsearch_settings' ); - - /** - * Settings array - * - * Retrieves all plugin settings - * - * @since 1.2.0 - * @param array $settings Settings array - */ - return apply_filters( 'bsearch_get_settings', $settings ); -} diff --git a/composer.json b/composer.json index 1f5e718..c2fc8ce 100644 --- a/composer.json +++ b/composer.json @@ -13,11 +13,30 @@ "authors": [ { "name": "WebberZone", - "email": "plugins@webberzone.com", "role": "Developer" } ], "requires": { "php": ">=7.4" + }, + "require-dev": { + "szepeviktor/phpstan-wordpress": "^1.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.10", + "php-stubs/wordpress-stubs": "^6.2", + "wp-coding-standards/wpcs": "^2.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "yoast/phpunit-polyfills": "^1.0", + "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "scripts": { + "phpstan": "vendor/bin/phpstan analyse --memory-limit=2048M" } -} +} \ No newline at end of file diff --git a/includes/activation.php b/includes/activation.php deleted file mode 100644 index 829777e..0000000 --- a/includes/activation.php +++ /dev/null @@ -1,187 +0,0 @@ -get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - " - SELECT blog_id FROM $wpdb->blogs - WHERE archived = '0' AND spam = '0' AND deleted = '0' - " - ); - foreach ( $blog_ids as $blog_id ) { - switch_to_blog( $blog_id ); - bsearch_single_activate(); - } - - // Switch back to the current blog. - restore_current_blog(); - - } else { - bsearch_single_activate(); - } -} -register_activation_hook( BETTER_SEARCH_PLUGIN_FILE, 'bsearch_install' ); - - -/** - * Create tables to store pageviews. - * - * @since 2.0.0 - */ -function bsearch_single_activate() { - global $wpdb, $bsearch_db_version; - - $bsearch_settings = bsearch_get_settings(); - $charset_collate = $wpdb->get_charset_collate(); - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - - // Create full text index. - $wpdb->hide_errors(); - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch (post_title, post_content);' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch_title (post_title);' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch_content (post_content);' );// phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->show_errors(); - - // Create the tables. - $table_name = $wpdb->prefix . 'bsearch'; - $table_name_daily = $wpdb->prefix . 'bsearch_daily'; - - if ( $wpdb->get_var( "show tables like '$table_name'" ) !== $table_name ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared - - $sql = "CREATE TABLE {$table_name} ( - accessedid int NOT NULL AUTO_INCREMENT, - searchvar VARCHAR(100) NOT NULL, - cntaccess int NOT NULL, - PRIMARY KEY (accessedid) - ) $charset_collate;"; - - dbDelta( $sql ); - - $wpdb->hide_errors(); - $wpdb->query( 'CREATE INDEX IDX_searhvar ON ' . $table_name . ' (searchvar)' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->show_errors(); - - update_option( 'bsearch_db_version', $bsearch_db_version ); - } - - if ( $wpdb->get_var( "show tables like '$table_name_daily'" ) !== $table_name_daily ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared - - $sql = "CREATE TABLE {$table_name_daily} ( - accessedid int NOT NULL AUTO_INCREMENT, - searchvar VARCHAR(100) NOT NULL, - cntaccess int NOT NULL, - dp_date date NOT NULL, - PRIMARY KEY (accessedid) - ) $charset_collate;"; - - dbDelta( $sql ); - - $wpdb->hide_errors(); - $wpdb->query( 'CREATE INDEX IDX_searhvar ON ' . $table_name_daily . ' (searchvar)' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->show_errors(); - - update_option( 'bsearch_db_version', $bsearch_db_version ); - } - - // Upgrade table code. - $installed_ver = get_option( 'bsearch_db_version' ); - - if ( $installed_ver != $bsearch_db_version ) { //phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - - $sql = "CREATE TABLE {$table_name} ( - accessedid int NOT NULL AUTO_INCREMENT, - searchvar VARCHAR(100) NOT NULL, - cntaccess int NOT NULL, - PRIMARY KEY (accessedid) - ) $charset_collate;"; - - dbDelta( $sql ); - - $wpdb->hide_errors(); - $wpdb->query( 'ALTER ' . $table_name . ' DROP INDEX IDX_searhvar ' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->query( 'CREATE INDEX IDX_searhvar ON ' . $table_name . ' (searchvar)' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->show_errors(); - - $sql = "DROP TABLE $table_name_daily"; - $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - - $sql = "CREATE TABLE {$table_name_daily} ( - accessedid int NOT NULL AUTO_INCREMENT, - searchvar VARCHAR(100) NOT NULL, - cntaccess int NOT NULL, - dp_date date NOT NULL, - PRIMARY KEY (accessedid) - ) $charset_collate;"; - - dbDelta( $sql ); - - $wpdb->hide_errors(); - $wpdb->query( 'ALTER ' . $table_name_daily . ' DROP INDEX IDX_searhvar ' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->query( 'CREATE INDEX IDX_searhvar ON ' . $table_name_daily . ' (searchvar)' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange - $wpdb->show_errors(); - - update_option( 'bsearch_db_version', $bsearch_db_version ); - } -} - - -/** - * Fired when a new site is activated with a WPMU environment. - * - * @since 2.0.0 - * - * @param int $blog_id ID of the new blog. - */ -function bsearch_activate_new_site( $blog_id ) { - - if ( 1 !== did_action( 'wpmu_new_blog' ) ) { - return; - } - - switch_to_blog( $blog_id ); - bsearch_single_activate(); - restore_current_blog(); -} -add_action( 'wpmu_new_blog', 'bsearch_activate_new_site' ); - - -/** - * Fired when a site is deleted in a WPMU environment. - * - * @since 2.0.0 - * - * @param array $tables Tables in the blog. - */ -function bsearch_on_delete_blog( $tables ) { - global $wpdb; - - $tables[] = $wpdb->prefix . 'bsearch'; - $tables[] = $wpdb->prefix . 'bsearch_daily'; - - return $tables; -} -add_filter( 'wpmu_drop_tables', 'bsearch_on_delete_blog' ); diff --git a/includes/admin/admin-dashboard.php b/includes/admin/admin-dashboard.php deleted file mode 100644 index 8939299..0000000 --- a/includes/admin/admin-dashboard.php +++ /dev/null @@ -1,76 +0,0 @@ - $daily, - ) - ); - - $output .= '
'; - - if ( $daily ) { - $output .= '' . __( 'View all daily popular searches', 'top-10' ) . ''; - } else { - $output .= '' . __( 'View all popular searches', 'top-10' ) . ''; - } - - $output .= '
'; - $output .= bsearch_get_credit_link(); - - return $output; -} - -/** - * Dashboard for Better Search. - * - * @since 1.0 - */ -function bsearch_pop_dashboard() { - echo bsearch_dashboard_widget( false ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Dashboard for Daily Better Search. - * - * @since 1.0 - */ -function bsearch_pop_daily_dashboard() { - echo bsearch_dashboard_widget( true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Add the dashboard widgets. - * - * @since 1.3.3 - */ -function bsearch_dashboard_setup() { - wp_add_dashboard_widget( 'bsearch_pop_dashboard', __( 'Popular Searches', 'better-search' ), 'bsearch_pop_dashboard' ); - wp_add_dashboard_widget( 'bsearch_pop_daily_dashboard', __( 'Daily Popular Searches', 'better-search' ), 'bsearch_pop_daily_dashboard' ); -} -add_action( 'wp_dashboard_setup', 'bsearch_dashboard_setup' ); diff --git a/includes/admin/admin.php b/includes/admin/admin.php deleted file mode 100644 index 96da718..0000000 --- a/includes/admin/admin.php +++ /dev/null @@ -1,187 +0,0 @@ -parent_base === 'bsearch_options_page' ) { - - $text = sprintf( - /* translators: 1: Better Search website, 2: Plugin reviews link. */ - __( 'Thank you for using Better Search! Please rate us on WordPress.org', 'better-search' ), - 'https://webberzone.com/better-search', - 'https://wordpress.org/support/plugin/better-search/reviews/#new-post' - ); - - return str_replace( '', '', $footer_text ) . ' | ' . $text . ''; - - } else { - - return $footer_text; - - } -} -add_filter( 'admin_footer_text', 'bsearch_admin_footer' ); - - -/** - * Adding WordPress plugin action links. - * - * @version 1.9.2 - * - * @param array $links Action links. - * @return array Links array with our settings link added. - */ -function bsearch_plugin_actions_links( $links ) { - - return array_merge( - array( - 'settings' => '' . __( 'Settings', 'better-search' ) . '', - ), - $links - ); -} -add_filter( 'plugin_action_links_' . plugin_basename( BETTER_SEARCH_PLUGIN_FILE ), 'bsearch_plugin_actions_links' ); - - -/** - * Add links to the plugin action row. - * - * @since 1.5 - * - * @param array $links Action links. - * @param array $file Plugin file name. - * @return array Links array with our links added - */ -function bsearch_plugin_actions( $links, $file ) { - $plugin = plugin_basename( BETTER_SEARCH_PLUGIN_FILE ); - - if ( $file === $plugin ) { - $links[] = '' . __( 'Support', 'better-search' ) . ''; - $links[] = '' . __( 'Donate', 'better-search' ) . ''; - $links[] = '' . __( 'Contribute', 'better-search' ) . ''; - } - return $links; -} -add_filter( 'plugin_row_meta', 'bsearch_plugin_actions', 10, 2 ); - - -/** - * Enqueue Admin JS - * - * @since 2.5.0 - * - * @param string $hook The current admin page. - */ -function bsearch_load_admin_scripts( $hook ) { - - global $bsearch_settings_page, $bsearch_settings_tools_help, $bsearch_settings_popular_searches, $bsearch_settings_popular_searches_daily; - - wp_register_script( - 'better-search-admin-js', - BETTER_SEARCH_PLUGIN_URL . 'includes/admin/js/admin-scripts.min.js', - array( 'jquery', 'jquery-ui-tabs', 'jquery-ui-datepicker', 'wp-color-picker' ), - BETTER_SEARCH_VERSION, - true - ); - wp_register_script( - 'better-search-suggest-js', - BETTER_SEARCH_PLUGIN_URL . 'includes/admin/js/better-search-suggest.min.js', - array( 'jquery', 'jquery-ui-autocomplete' ), - BETTER_SEARCH_VERSION, - true - ); - - if ( in_array( $hook, array( $bsearch_settings_page, $bsearch_settings_tools_help, $bsearch_settings_popular_searches, $bsearch_settings_popular_searches_daily ), true ) ) { - - wp_enqueue_script( 'better-search-admin-js' ); - wp_enqueue_script( 'better-search-suggest-js' ); - wp_enqueue_script( 'plugin-install' ); - add_thickbox(); - wp_enqueue_style( 'wp-color-picker' ); - - wp_enqueue_code_editor( - array( - 'type' => 'text/html', - 'codemirror' => array( - 'indentUnit' => 2, - 'tabSize' => 2, - ), - ) - ); - wp_localize_script( - 'better-search-admin-js', - 'bsearch_admin_data', - array( - 'security' => wp_create_nonce( 'bsearch-admin' ), - ) - ); - - } - - // Only enqueue the styles if this is a popular posts page. - if ( in_array( $hook, array( $bsearch_settings_popular_searches, $bsearch_settings_popular_searches_daily ), true ) ) { - wp_enqueue_style( - 'bsearch-admin-ui-css', - BETTER_SEARCH_PLUGIN_URL . 'includes/admin/css/better-search-admin.min.css', - false, - BETTER_SEARCH_VERSION, - false - ); - } -} -add_action( 'admin_enqueue_scripts', 'bsearch_load_admin_scripts' ); diff --git a/includes/admin/class-activator.php b/includes/admin/class-activator.php new file mode 100644 index 0000000..73973cb --- /dev/null +++ b/includes/admin/class-activator.php @@ -0,0 +1,180 @@ + 0, + 'spam' => 0, + 'deleted' => 0, + ) + ); + + foreach ( $sites as $site ) { + switch_to_blog( (int) $site->blog_id ); + self::single_activate(); + } + + // Switch back to the current blog. + restore_current_blog(); + + } else { + self::single_activate(); + } + } + + /** + * Fired for each blog when the plugin is activated. + * + * @since 2.0.0 + */ + public static function single_activate() { + global $wpdb, $bsearch_db_version; + + $charset_collate = $wpdb->get_charset_collate(); + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + // Create FULLTEXT indexes. + $wpdb->hide_errors(); + $wpdb->query( 'START TRANSACTION' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch (post_title, post_content);' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch_title (post_title);' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch_content (post_content);' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange + $wpdb->query( 'COMMIT' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->show_errors(); + + $table_name = $wpdb->base_prefix . 'bsearch'; + $table_name_daily = $wpdb->base_prefix . 'bsearch_daily'; + + if ( $wpdb->get_var( "show tables like '$table_name'" ) != $table_name ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery + + $sql = 'CREATE TABLE ' . $table_name . // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange + " ( + accessedid int NOT NULL AUTO_INCREMENT, + searchvar VARCHAR(100) NOT NULL, + cntaccess int NOT NULL, + PRIMARY KEY (accessedid) + ) $charset_collate;"; + + dbDelta( $sql ); + + $wpdb->hide_errors(); + $wpdb->query( 'CREATE INDEX IDX_searhvar ON ' . $table_name . ' (searchvar)' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange + $wpdb->show_errors(); + + update_option( 'bsearch_db_version', $bsearch_db_version ); + } + + if ( $wpdb->get_var( "show tables like '$table_name_daily'" ) != $table_name_daily ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery + + $sql = 'CREATE TABLE ' . $table_name_daily . // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange + " ( + accessedid int NOT NULL AUTO_INCREMENT, + searchvar VARCHAR(100) NOT NULL, + cntaccess int NOT NULL, + dp_date date NOT NULL, + PRIMARY KEY (accessedid) + ) $charset_collate;"; + + dbDelta( $sql ); + + $wpdb->hide_errors(); + $wpdb->query( 'CREATE INDEX IDX_searhvar ON ' . $table_name_daily . ' (searchvar)' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange + $wpdb->show_errors(); + + update_option( 'bsearch_db_version', $bsearch_db_version ); + } + } + + + /** + * Fired when a new site is activated with a WPMU environment. + * + * @since 2.0.0 + * + * @param int|\WP_Site $blog WordPress 5.1 passes a WP_Site object. + */ + public static function activate_new_site( $blog ) { + + if ( ! is_plugin_active_for_network( plugin_basename( BETTER_SEARCH_PLUGIN_FILE ) ) ) { + return; + } + + if ( ! is_int( $blog ) ) { + $blog = $blog->id; + } + + switch_to_blog( $blog ); + self::single_activate(); + restore_current_blog(); + } + + /** + * Fired when a site is deleted in a WPMU environment. + * + * @since 2.0.0 + * + * @param array $tables Tables in the blog. + */ + public static function on_delete_blog( $tables ) { + global $wpdb; + + $tables[] = $wpdb->prefix . 'bsearch'; + $tables[] = $wpdb->prefix . 'bsearch_daily'; + + return $tables; + } + + /** + * Function to call install function if needed. + * + * @since 1.9 + */ + public static function update_db_check() { + global $bsearch_db_version, $network_wide; + + if ( get_site_option( 'bsearch_db_version' ) != $bsearch_db_version ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual + self::activation_hook( $network_wide ); + } + } +} diff --git a/includes/admin/class-admin.php b/includes/admin/class-admin.php new file mode 100644 index 0000000..dd60707 --- /dev/null +++ b/includes/admin/class-admin.php @@ -0,0 +1,226 @@ +hooks(); + + // Initialise admin classes. + $this->admin_dashboard = new Dashboard(); + $this->statistics = new Statistics(); + $this->settings = new Settings\Settings(); + $this->activator = new Activator(); + $this->tools_page = new Tools_Page(); + $this->dashboard_widgets = new Dashboard_Widgets(); + $this->cache = new Cache(); + } + + /** + * Run the hooks. + * + * @since 3.3.0 + */ + public function hooks() { + add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); + } + + /** + * Enqueue scripts in admin area. + * + * @since 3.0.0 + */ + public function admin_enqueue_scripts() { + + // Register charj.js, luxon and chartjs-adapter-luxon. + wp_register_script( + 'better-search-chartjs', + BETTER_SEARCH_PLUGIN_URL . 'includes/admin/js/chart.min.js', + array(), + BETTER_SEARCH_VERSION, + true + ); + wp_register_script( + 'better-search-luxon', + BETTER_SEARCH_PLUGIN_URL . 'includes/admin/js/luxon.min.js', + array(), + BETTER_SEARCH_VERSION, + true + ); + wp_register_script( + 'better-search-chartjs-adapter-luxon', + BETTER_SEARCH_PLUGIN_URL . 'includes/admin/js/chartjs-adapter-luxon.min.js', + array( 'better-search-chartjs', 'better-search-luxon' ), + BETTER_SEARCH_VERSION, + true + ); + wp_register_script( + 'better-search-chartjs-plugin-datalabels', + BETTER_SEARCH_PLUGIN_URL . 'includes/admin/js/chartjs-plugin-datalabels.min.js', + array( 'better-search-chartjs' ), + BETTER_SEARCH_VERSION, + true + ); + wp_register_script( + 'better-search-chart-data-js', + BETTER_SEARCH_PLUGIN_URL . 'includes/admin/js/chart-data.min.js', + array( 'jquery', 'better-search-chartjs', 'better-search-chartjs-adapter-luxon', 'better-search-luxon', 'better-search-chartjs-plugin-datalabels' ), + BETTER_SEARCH_VERSION, + true + ); + + wp_register_script( + 'better-search-admin-js', + BETTER_SEARCH_PLUGIN_URL . 'includes/admin/js/admin-scripts.min.js', + array( 'jquery', 'jquery-ui-tabs', 'jquery-ui-datepicker' ), + BETTER_SEARCH_VERSION, + true + ); + wp_localize_script( + 'better-search-admin-js', + 'better_search_admin', + array( + 'nonce' => wp_create_nonce( 'better_search_admin_nonce' ), + ) + ); + wp_register_style( + 'better-search-admin-ui-css', + BETTER_SEARCH_PLUGIN_URL . 'includes/admin/css/better-search-admin.min.css', + array(), + BETTER_SEARCH_VERSION + ); + } + + /** + * Display admin sidebar. + * + * @since 3.3.0 + */ + public static function display_admin_sidebar() { + require_once BETTER_SEARCH_PLUGIN_DIR . 'includes/admin/settings/sidebar.php'; + } +} diff --git a/includes/admin/class-better-search-statistics.php b/includes/admin/class-better-search-statistics.php deleted file mode 100644 index 3004d0b..0000000 --- a/includes/admin/class-better-search-statistics.php +++ /dev/null @@ -1,153 +0,0 @@ - -
-

- -
-
-
-
-
- - pop_search_obj->prepare_items( $args ); - - $this->pop_search_obj->search_box( __( 'Search Table', 'better-search' ), 'better-search' ); - - $this->pop_search_obj->display(); - ?> -
-
-
-
-
- -
-
-
-
-
-
- __( 'Popular Searches per page', 'better-search' ), - 'default' => 20, - 'option' => 'pop_searches_per_page', - ); - add_screen_option( $option, $args ); - $this->pop_search_obj = new Better_Search_Statistics_Table(); - } - - /** Singleton instance */ - public static function get_instance() { - if ( ! isset( self::$instance ) ) { - self::$instance = new self(); - } - return self::$instance; - } -} - -/** - * Function to initialise stats page. - * - * @since 2.4.0 - */ -function bsearch_stats_page() { - Better_Search_Statistics::get_instance(); -} -add_action( 'plugins_loaded', 'bsearch_stats_page' ); diff --git a/includes/admin/class-dashboard-widgets.php b/includes/admin/class-dashboard-widgets.php new file mode 100644 index 0000000..f380879 --- /dev/null +++ b/includes/admin/class-dashboard-widgets.php @@ -0,0 +1,109 @@ + $daily, + ) + ); + + $output .= '
'; + + if ( $daily ) { + $output .= sprintf( + '%s', + admin_url( 'admin.php?page=bsearch_popular_searches&orderby=daily_count&order=desc' ), + __( 'View all daily popular searches', 'better-search' ) + ); + } else { + $output .= sprintf( + '%s', + admin_url( 'admin.php?page=bsearch_popular_searches' ), + __( 'View all popular searches', 'better-search' ) + ); + } + $output .= '
'; + $output .= Helpers::get_credit_link(); + + return $output; + } + + + /** + * Widget for Popular Searches. + * + * @since 3.3.0 + */ + public static function widget() { + echo self::display( false ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + + /** + * Widget for Daily Popular Searches. + * + * @since 3.3.0 + */ + public static function widget_daily() { + echo self::display( true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} diff --git a/includes/admin/class-dashboard.php b/includes/admin/class-dashboard.php new file mode 100644 index 0000000..8813094 --- /dev/null +++ b/includes/admin/class-dashboard.php @@ -0,0 +1,490 @@ + +
+

+ + + +
+
+
+
+ + +
+ + + 'better-search-chart-submit', + 'onclick' => 'updateChart(); return false;', + ) + ); + ?> +
+
+ +
+ +
+ +

+ + +
+ + get_tabs() as $tab_id => $tab_name ) : ?> + +
+ + display_popular_searches( $tab_name ); + echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + ?> +
+ +
+ + 'desc', + ); + $daily = ( isset( $tab_name['daily'] ) ) ? $tab_name['daily'] : true; + + if ( $daily ) { + $query_args['orderby'] = 'daily_count'; + + if ( ! empty( $tab_name['from_date'] ) ) { + $query_args['search-date-filter-from'] = gmdate( 'd+M+Y', strtotime( $tab_name['from_date'] ) ); + } + if ( ! empty( $tab_name['to_date'] ) ) { + $query_args['search-date-filter-to'] = gmdate( 'd+M+Y', strtotime( $tab_name['to_date'] ) ); + } + } else { + $query_args['orderby'] = 'total_count'; + } + $url = add_query_arg( $query_args, admin_url( 'admin.php?page=bsearch_popular_searches' ) ); + + ?> + + » + +
+ +
+ + + +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ +
+ + parent_id = add_menu_page( + esc_html__( 'Better Search Dashboard', 'better-search' ), + esc_html__( 'Better Search', 'better-search' ), + 'manage_options', + 'bsearch_dashboard', + array( $this, 'render_page' ), + 'dashicons-search' + ); + + add_submenu_page( + 'bsearch_dashboard', + esc_html__( 'Better Search Dashboard', 'better-search' ), + esc_html__( 'Dashboard', 'better-search' ), + 'manage_options', + 'bsearch_dashboard', + array( $this, 'render_page' ) + ); + + add_action( 'load-' . $this->parent_id, array( $this, 'help_tabs' ) ); + } + + /** + * Enqueue scripts in admin area. + * + * @since 3.3.0 + * + * @param string $hook The current admin page. + */ + public function admin_enqueue_scripts( $hook ) { + + if ( $hook === $this->parent_id ) { + wp_enqueue_script( 'moment' ); + wp_enqueue_script( 'better-search-chartjs' ); + wp_enqueue_script( 'better-search-chart-datalabels-js' ); + wp_enqueue_script( 'better-search-chartjs-adapter-moment-js' ); + wp_enqueue_script( 'better-search-chart-data-js' ); + wp_enqueue_script( 'better-search-admin-js' ); + wp_localize_script( + 'better-search-chart-data-js', + 'bsearch_chart_data', + array( + 'security' => wp_create_nonce( 'bsearch-dashboard' ), + 'datasetlabel' => __( 'Searches', 'better-search' ), + 'charttitle' => __( 'Daily Searches', 'better-search' ), + ) + ); + wp_enqueue_style( 'better-search-admin-ui-css' ); + } + } + + /** + * Function to add an action to search for tags using Ajax. + * + * @since 3.3.0 + */ + public function get_chart_data() { + global $wpdb; + + if ( ! current_user_can( 'manage_options' ) ) { + wp_die(); + } + check_ajax_referer( 'bsearch-dashboard', 'security' ); + + $blog_id = get_current_blog_id(); + + // Add date selector. + $to_date = isset( $_REQUEST['to_date'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['to_date'] ) ) : current_time( 'd M Y' ); + $from_date = isset( $_REQUEST['from_date'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['from_date'] ) ) : gmdate( 'd M Y', strtotime( '-1 month' ) ); + + $post_date_from = gmdate( 'Y-m-d', strtotime( $from_date ) ); + $post_date_to = gmdate( 'Y-m-d', strtotime( $to_date ) ); + + $sql = $wpdb->prepare( + " SELECT SUM(cntaccess) AS searches, DATE(dp_date) as date + FROM {$wpdb->prefix}bsearch_daily + WHERE DATE(dp_date) >= DATE(%s) + AND DATE(dp_date) <= DATE(%s) + GROUP BY date + ORDER BY date ASC + ", + $post_date_from, + $post_date_to, + ); + + $result = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + + $data = array(); + foreach ( $result as $row ) { + $data[] = $row; + } + + echo wp_json_encode( $data ); + wp_die(); + } + + + /** + * Array containing the settings' sections. + * + * @since 3.3.0 + * + * @return array Settings array + */ + public function get_tabs() { + $tabs = array( + 'today' => array( + 'title' => __( 'Today', 'better-search' ), + 'from_date' => current_time( 'd M Y' ), + 'to_date' => current_time( 'd M Y' ), + ), + 'yesterday' => array( + 'title' => __( 'Yesterday', 'better-search' ), + 'from_date' => gmdate( 'd M Y', strtotime( '-1 day' ) ), + 'to_date' => gmdate( 'd M Y', strtotime( '-1 day' ) ), + ), + 'lastweek' => array( + 'title' => __( 'Last 7 days', 'better-search' ), + 'from_date' => gmdate( 'd M Y', strtotime( '-1 week' ) ), + 'to_date' => current_time( 'd M Y' ), + ), + 'lastfortnight' => array( + 'title' => __( 'Last 14 days', 'better-search' ), + 'from_date' => gmdate( 'd M Y', strtotime( '-2 weeks' ) ), + 'to_date' => current_time( 'd M Y' ), + ), + 'lastmonth' => array( + 'title' => __( 'Last 30 days', 'better-search' ), + 'from_date' => gmdate( 'd M Y', strtotime( '-30 days' ) ), + 'to_date' => current_time( 'd M Y' ), + ), + 'overall' => array( + 'title' => __( 'All time', 'better-search' ), + 'daily' => false, + ), + ); + + return $tabs; + } + + /** + * Get popular searches for a date range. + * + * @since 3.3.0 + * + * @param string|array $args { + * Optional. Array or string of Query parameters. + * + * @type bool $daily Set to true to get the daily/custom period searches. False for overall. + * @type string $from_date From date. A date/time string. + * @type int $number Number of searches to fetch. + * @type string $to_date To date. A date/time string. + * } + * @return string HTML table with popular searches. + */ + public function display_popular_searches( $args = array() ) { + $output = ''; + + $defaults = array( + 'daily' => true, + 'from_date' => null, + 'number' => 20, + 'to_date' => null, + ); + $args = wp_parse_args( $args, $defaults ); + + $results = $this->get_popular_searches( $args ); + + ob_start(); + if ( $results ) : + ?> + + + searches ); + ?> + + + + + + +
+ name ); ?> +
+ + + + + + + + true, + 'from_date' => null, + 'number' => 20, + 'offset' => 0, + 'to_date' => null, + ); + $args = wp_parse_args( $args, $defaults ); + + $table_name = Helpers::get_bsearch_table( $args['daily'] ); + + // Fields to return. + $fields[] = "{$table_name}.searchvar as name"; + $fields[] = ( $args['daily'] ) ? "SUM({$table_name}.cntaccess) as searches" : "{$table_name}.cntaccess as searches"; + + $fields = implode( ', ', $fields ); + + // Create the base WHERE clause. + $where = " AND {$table_name}.searchvar != '' "; + + if ( isset( $args['from_date'] ) ) { + $from_date = gmdate( 'Y-m-d', strtotime( $args['from_date'] ) ); + $where .= $wpdb->prepare( " AND DATE({$table_name}.dp_date) >= DATE(%s) ", $from_date ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + if ( isset( $args['to_date'] ) ) { + $to_date = gmdate( 'Y-m-d', strtotime( $args['to_date'] ) ); + $where .= $wpdb->prepare( " AND DATE({$table_name}.dp_date) <= DATE(%s) ", $to_date ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + // Create the base GROUP BY clause. + if ( $args['daily'] ) { + $groupby = " {$table_name}.searchvar "; + } + + // Create the base ORDER BY clause. + $orderby = ' searches DESC, name ASC '; + $orderby = " ORDER BY {$orderby} "; + + // Create the base LIMITS clause. + $limits = $wpdb->prepare( ' LIMIT %d, %d ', $args['offset'], $args['number'] ); + + if ( ! empty( $groupby ) ) { + $groupby = " GROUP BY {$groupby} "; + } + + $sql = "SELECT DISTINCT $fields FROM {$table_name} $join WHERE 1=1 $where $groupby $orderby $limits"; + + $result = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + + return $result; + } + + /** + * Generates the help tabs. + * + * @since 3.3.0 + */ + public function help_tabs() { + + $screen = get_current_screen(); + + $screen->set_help_sidebar( + /* translators: 1: Support link. */ + '

' . sprintf( __( 'For more information or how to get support visit the WebberZone support site.', 'better-search' ), esc_url( 'https://webberzone.com/support/' ) ) . '

' . + /* translators: 1: Forum link. */ + '

' . sprintf( __( 'Support queries should be posted in the WordPress.org support forums.', 'better-search' ), esc_url( 'https://wordpress.org/support/plugin/better-search' ) ) . '

' . + '

' . sprintf( + /* translators: 1: Github Issues link, 2: Github page. */ + __( 'Post an issue on GitHub (bug reports only).', 'better-search' ), + esc_url( 'https://github.com/WebberZone/better-search/issues' ), + esc_url( 'https://github.com/WebberZone/better-search' ) + ) . '

' + ); + + $screen->add_help_tab( + array( + 'id' => 'bsearch-tools-general', + 'title' => __( 'General', 'better-search' ), + 'content' => + '

' . __( 'This screen provides some tools that help maintain certain features of Better Search.', 'better-search' ) . '

' . + '

' . __( 'Clear the cache, reset the popular posts tables plus some miscellaneous fixes for older versions of Better Search.', 'better-search' ) . '

', + ) + ); + } +} diff --git a/includes/admin/class-better-search-statistics-table.php b/includes/admin/class-statistics-table.php similarity index 79% rename from includes/admin/class-better-search-statistics-table.php rename to includes/admin/class-statistics-table.php index 68102f1..cceb733 100644 --- a/includes/admin/class-better-search-statistics-table.php +++ b/includes/admin/class-statistics-table.php @@ -3,21 +3,14 @@ * Better Search Display statistics table. * * @package Better_Search - * @subpackage Better_Search_Statistics_Table - * @author Ajay D'Souza - * @license GPL-2.0+ - * @link https://webberzone.com - * @copyright 2008-2019 Ajay D'Souza */ -/**** If this file is called directly, abort. ****/ -if ( ! defined( 'WPINC' ) ) { - die; -} +namespace WebberZone\Better_Search\Admin; +use WebberZone\Better_Search\Util\Helpers; -if ( ! class_exists( 'WP_List_Table' ) ) { - require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; +if ( ! defined( 'WPINC' ) ) { + die; } /** @@ -25,11 +18,9 @@ * * Display the popular search terms in a tabular format. * - * @extends WP_List_Table - * - * @since 2.4.0 + * @since 3.3.0 */ -class Better_Search_Statistics_Table extends WP_List_Table { +class Statistics_Table extends \WP_List_Table { /** * Class constructor. @@ -56,14 +47,10 @@ public function get_popular_searches( $per_page = 20, $page_number = 1, $args = global $wpdb; - $from_date = bsearch_get_from_date( - isset( $args['search-date-filter-from'] ) ? $args['search-date-filter-from'] : null, - 1 - ); - $to_date = bsearch_get_from_date( - isset( $args['search-date-filter-to'] ) ? $args['search-date-filter-to'] : null, - 1 - ); + $from_date = isset( $args['search-date-filter-from'] ) ? $args['search-date-filter-from'] : current_time( 'd M Y' ); + $from_date = gmdate( 'Y-m-d', strtotime( $from_date ) ); + $to_date = isset( $args['search-date-filter-to'] ) ? $args['search-date-filter-to'] : current_time( 'd M Y' ); + $to_date = gmdate( 'Y-m-d', strtotime( $to_date ) ); /* Start creating the SQL */ $table_name_daily = $wpdb->prefix . 'bsearch_daily AS bsd'; @@ -100,36 +87,40 @@ public function get_popular_searches( $per_page = 20, $page_number = 1, $args = // Create the base GROUP BY clause. $groupby = ' title '; - // Create the base ORDER BY clause. - $orderby = ' total_count DESC '; - + // Create the ORDER BY clause. + $orderby = ''; if ( ! empty( $_REQUEST['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $orderby = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } elseif ( ! empty( $args['orderby'] ) ) { + $orderby = $args['orderby']; + } + if ( $orderby ) { if ( ! in_array( $orderby, array( 'title', 'daily_count', 'total_count' ) ) ) { //phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict $orderby = ' total_count '; } + $order = ''; if ( ! empty( $_REQUEST['order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $order = sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } elseif ( ! empty( $args['order'] ) ) { + $order = $args['order']; + } - if ( in_array( $order, array( 'asc', 'ASC', 'desc', 'DESC' ) ) ) { //phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict - $orderby .= ' ' . $order; - } else { - $orderby .= ' DESC'; - } + if ( $order && in_array( $order, array( 'asc', 'ASC', 'desc', 'DESC' ), true ) ) { + $orderby .= " {$order}"; + } else { + $orderby .= ' DESC'; } + } else { + $orderby = ' total_count DESC '; } // Create the base LIMITS clause. $limits = $wpdb->prepare( ' LIMIT %d, %d ', ( $page_number - 1 ) * $per_page, $per_page ); - if ( ! empty( $groupby ) ) { - $groupby = " GROUP BY {$groupby} "; - } - if ( ! empty( $orderby ) ) { - $orderby = " ORDER BY {$orderby} "; - } + $groupby = " GROUP BY {$groupby} "; + $orderby = " ORDER BY {$orderby} "; $sql = "SELECT $fields FROM {$table_name} $join WHERE 1=1 $where $groupby $orderby $limits"; @@ -169,8 +160,8 @@ public static function delete_search_entry( $id ) { /** * Returns the count of records in the database. * - * @param string $args Array of arguments. - * @return null|string null|string + * @param array $args Array of arguments. + * @return int Number of records. */ public function record_count( $args = null ) { @@ -182,7 +173,7 @@ public function record_count( $args = null ) { $sql .= $wpdb->prepare( ' WHERE bst.searchvar LIKE %s ', '%' . $wpdb->esc_like( $args['search'] ) . '%' ); } - return $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + return intval( $wpdb->get_var( $sql ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared } /** @@ -205,7 +196,7 @@ public function column_default( $item, $column_name ) { switch ( $column_name ) { case 'total_count': case 'daily_count': - return bsearch_number_format_i18n( absint( $item[ $column_name ] ) ); + return Helpers::number_format_i18n( absint( $item[ $column_name ] ) ); default: // Show the whole array for troubleshooting purposes. return print_r( $item, true ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r @@ -303,10 +294,9 @@ public function get_bulk_actions() { /** * Handles data query and filter, sorting, and pagination. - * - * @param array $args Array of arguments. */ - public function prepare_items( $args = null ) { + public function prepare_items() { + $args = array(); $this->_column_headers = $this->get_column_info(); @@ -317,17 +307,30 @@ public function prepare_items( $args = null ) { $current_page = $this->get_pagenum(); - $total_items = self::record_count( $args ); + // If this is a search? + if ( isset( $_REQUEST['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + // If this is a post date filter? + if ( isset( $_REQUEST['search-date-filter-to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['search-date-filter-to'] = sanitize_text_field( wp_unslash( $_REQUEST['search-date-filter-to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + if ( isset( $_REQUEST['search-date-filter-from'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['search-date-filter-from'] = sanitize_text_field( wp_unslash( $_REQUEST['search-date-filter-from'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + $this->items = self::get_popular_searches( $per_page, $current_page, $args ); + $total_items = (int) self::record_count( $args ); $this->set_pagination_args( array( 'total_items' => $total_items, // WE have to calculate the total number of items. 'per_page' => $per_page, // WE have to determine how many items to show on a page. - 'total_pages' => ceil( $total_items / $per_page ), // WE have to calculate the total number of pages. + 'total_pages' => intval( ceil( $total_items / $per_page ) ), // WE have to calculate the total number of pages. ) ); - - $this->items = self::get_popular_searches( $per_page, $current_page, $args ); } /** @@ -351,7 +354,7 @@ public function process_bulk_action() { if ( ( isset( $_REQUEST['action'] ) && 'bulk-delete' === $_REQUEST['action'] ) || ( isset( $_REQUEST['action2'] ) && 'bulk-delete' === $_REQUEST['action2'] ) ) { - $delete_ids = isset( $_REQUEST['search'] ) ? array_map( 'absint', (array) wp_unslash( $_REQUEST['search'] ) ) : array(); + $delete_ids = isset( $_REQUEST['search'] ) ? array_map( 'wp_kses_post', (array) wp_unslash( $_REQUEST['search'] ) ) : array(); // Loop over the array of record IDs and delete them. foreach ( $delete_ids as $id ) { diff --git a/includes/admin/class-statistics.php b/includes/admin/class-statistics.php new file mode 100644 index 0000000..1ed2b07 --- /dev/null +++ b/includes/admin/class-statistics.php @@ -0,0 +1,155 @@ +parent_id ) { + wp_enqueue_script( 'better-search-admin-js' ); + } + } + + /** + * Admin Menu. + * + * @since 3.0.0 + */ + public function admin_menu() { + $this->parent_id = add_submenu_page( + 'bsearch_dashboard', + __( 'Better Search Popular Searches', 'better-search' ), + __( 'Popular Searches', 'better-search' ), + 'manage_options', + 'bsearch_popular_searches', + array( $this, 'render_page' ) + ); + + add_submenu_page( + 'bsearch_dashboard', + __( 'Better Search Daily Popular Searches', 'better-search' ), + __( 'Daily Popular Searches', 'better-search' ), + 'manage_options', + 'bsearch_popular_searches&orderby=daily_count&order=desc', + array( $this, 'render_page' ) + ); + + add_action( "load-{$this->parent_id}", array( $this, 'screen_option' ) ); + } + + /** + * Set screen. + * + * @param string $status Status of screen. + * @param string $option Option name. + * @param string $value Option value. + * @return string Value. + */ + public static function set_screen( $status, $option, $value ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + return $value; + } + + /** + * Plugin settings page + */ + public function render_page() { + $page = ''; + + if ( isset( $_REQUEST['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + ?> +
+

+ +
+
+
+
+
+ + pop_search_table->prepare_items(); + $this->pop_search_table->search_box( __( 'Search Table', 'better-search' ), 'better-search' ); + $this->pop_search_table->display(); + ?> +
+
+
+
+
+ +
+
+
+
+
+
+ __( 'Popular Searches per page', 'better-search' ), + 'default' => 20, + 'option' => 'pop_searches_per_page', + ); + add_screen_option( $option, $args ); + $this->pop_search_table = new Statistics_Table(); + } +} diff --git a/includes/admin/class-tools-page.php b/includes/admin/class-tools-page.php new file mode 100644 index 0000000..ee6238f --- /dev/null +++ b/includes/admin/class-tools-page.php @@ -0,0 +1,393 @@ +parent_id = add_submenu_page( + 'bsearch_dashboard', + esc_html__( 'Better Search Tools', 'better-search' ), + esc_html__( 'Tools', 'better-search' ), + 'manage_options', + 'bsearch_tools_page', + array( $this, 'render_page' ) + ); + + add_action( 'load-' . $this->parent_id, array( $this, 'help_tabs' ) ); + } + + /** + * Enqueue scripts in admin area. + * + * @since 3.3.0 + * + * @param string $hook The current admin page. + */ + public function admin_enqueue_scripts( $hook ) { + if ( $hook === $this->parent_id ) { + wp_enqueue_script( 'better-search-admin-js' ); + wp_enqueue_style( 'bsearch-admin-ui-css', ); + wp_localize_script( + 'better-search-admin-js', + 'bsearch_admin_data', + array( + 'security' => wp_create_nonce( 'bsearch-admin' ), + ) + ); + } + } + + /** + * Render the tools settings page. + * + * @since 3.3.0 + * + * @return void + */ + public function render_page() { + global $wpdb; + + /* Recreate index */ + if ( ( isset( $_POST['bsearch_recreate'] ) ) && ( check_admin_referer( 'bsearch-tools-settings' ) ) ) { + self::recreate_index(); + add_settings_error( 'bsearch-notices', '', esc_html__( 'FULLTEXT index has been recreated', 'better-search' ), 'error' ); + } + + /* Truncate overall posts table */ + if ( ( isset( $_POST['bsearch_trunc_all'] ) ) && ( check_admin_referer( 'bsearch-tools-settings' ) ) ) { + self::trunc_count( false ); + add_settings_error( 'bsearch-notices', '', esc_html__( 'Better Search popular searches table reset', 'better-search' ), 'error' ); + } + + /* Truncate daily posts table */ + if ( ( isset( $_POST['bsearch_trunc_daily'] ) ) && ( check_admin_referer( 'bsearch-tools-settings' ) ) ) { + self::trunc_count( true ); + add_settings_error( 'bsearch-notices', '', esc_html__( 'Better Search daily searches table reset', 'better-search' ), 'error' ); + } + + /* Delete old settings */ + if ( ( isset( $_POST['bsearch_delete_old_settings'] ) ) && ( check_admin_referer( 'bsearch-tools-settings' ) ) ) { + $old_settings = get_option( 'ald_bsearch_settings' ); + + if ( empty( $old_settings ) ) { + add_settings_error( 'bsearch-notices', '', esc_html__( 'Old settings key does not exist', 'autoclose' ), 'error' ); + } else { + delete_option( 'ald_bsearch_settings' ); + add_settings_error( 'bsearch-notices', '', esc_html__( 'Old settings key has been deleted', 'autoclose' ), 'updated' ); + } + } + + /* Message for successful file import */ + if ( isset( $_GET['settings_import'] ) && 'success' === $_GET['settings_import'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + add_settings_error( 'bsearch-notices', '', esc_html__( 'Settings have been imported successfully', 'better-search' ), 'updated' ); + } + + ob_start(); + ?> +
+

+ + + +
+
+
+ +
+ +

+

+ +

+

+ +

+ +

+

+ +

+

+ +

+

+

+ ALTER TABLE posts ); ?> DROP INDEX bsearch; + ALTER TABLE posts ); ?> DROP INDEX bsearch_title; + ALTER TABLE posts ); ?> DROP INDEX bsearch_content; + ALTER TABLE posts ); ?> ADD FULLTEXT bsearch_related (post_title, post_content); + ALTER TABLE posts ); ?> ADD FULLTEXT bsearch_related_title (post_title); + ALTER TABLE posts ); ?> ADD FULLTEXT bsearch_related_content (post_content); +

+ +

+

+ + +

+

+ +

+ + +
+ +
+ +

+

+ +

+

+

+ +

+ + +
+ +
+ +

+ +

+

+ +

+

+ +

+ + + +
+ +
+

+

+ +

+

+ +

+ + +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ +
+ + prefix . 'bsearch_daily' : $wpdb->prefix . 'bsearch'; + + $sql = "TRUNCATE TABLE $table_name"; + $wpdb->query( $sql ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared + } + + + /** + * Recreate FULLTEXT indices. + * + * @since 3.3.0 + */ + public static function recreate_index() { + + global $wpdb; + + $wpdb->query( 'START TRANSACTION;' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery + + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' DROP INDEX bsearch;' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' DROP INDEX bsearch_title;' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' DROP INDEX bsearch_content;' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery + + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch (post_title, post_content);' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch_title (post_title);' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery + $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch_content (post_content);' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery + + $wpdb->query( 'COMMIT;' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery + } + + /** + * Process a settings export that generates a .json file of the shop settings + * + * @since 3.3.0 + */ + public static function process_settings_export() { + + if ( empty( $_POST['bsearch_action'] ) || 'export_settings' !== $_POST['bsearch_action'] ) { + return; + } + + if ( ! isset( $_POST['bsearch_export_settings_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['bsearch_export_settings_nonce'] ), 'bsearch_export_settings_nonce' ) ) { + return; + } + + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $settings = get_option( 'bsearch_settings' ); + + ignore_user_abort( true ); + + nocache_headers(); + header( 'Content-Type: application/json; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename=bsearch-settings-export-' . gmdate( 'm-d-Y' ) . '.json' ); + header( 'Expires: 0' ); + + echo wp_json_encode( $settings ); + exit; + } + + /** + * Process a settings import from a json file + * + * @since 3.3.0 + */ + public static function process_settings_import() { + + if ( empty( $_POST['bsearch_action'] ) || 'import_settings' !== $_POST['bsearch_action'] ) { + return; + } + + if ( ! isset( $_POST['bsearch_import_settings_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['bsearch_import_settings_nonce'] ), 'bsearch_import_settings_nonce' ) ) { + return; + } + + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $filename = 'import_settings_file'; + $extension = isset( $_FILES[ $filename ]['name'] ) ? pathinfo( sanitize_file_name( wp_unslash( $_FILES[ $filename ]['name'] ) ), PATHINFO_EXTENSION ) : ''; + + if ( 'json' !== $extension ) { + wp_die( esc_html__( 'Please upload a valid .json file', 'better-search' ) ); + } + + $import_file = isset( $_FILES[ $filename ]['tmp_name'] ) ? ( wp_unslash( $_FILES[ $filename ]['tmp_name'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( empty( $import_file ) ) { + wp_die( esc_html__( 'Please upload a file to import', 'better-search' ) ); + } + + // Retrieve the settings from the file and convert the json object to an array. + $settings = (array) json_decode( file_get_contents( $import_file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + + update_option( 'bsearch_settings', $settings ); + + wp_safe_redirect( + add_query_arg( + array( + 'page' => 'bsearch_tools_page', + 'settings_import' => 'success', + ), + admin_url( 'admin.php' ) + ) + ); + exit; + } + + /** + * Generates the Tools help page. + * + * @since 3.3.0 + */ + public static function help_tabs() { + $screen = get_current_screen(); + + $screen->set_help_sidebar( + /* translators: 1: Support link. */ + '

' . sprintf( __( 'For more information or how to get support visit the WebberZone support site.', 'better-search' ), esc_url( 'https://webberzone.com/support/' ) ) . '

' . + /* translators: 1: Forum link. */ + '

' . sprintf( __( 'Support queries should be posted in the WordPress.org support forums.', 'better-search' ), esc_url( 'https://wordpress.org/support/plugin/better-search' ) ) . '

' . + '

' . sprintf( + /* translators: 1: Github Issues link, 2: Github page. */ + __( 'Post an issue on GitHub (bug reports only).', 'better-search' ), + esc_url( 'https://github.com/WebberZone/better-search/issues' ), + esc_url( 'https://github.com/WebberZone/better-search' ) + ) . '

' + ); + + $screen->add_help_tab( + array( + 'id' => 'bsearch-settings-general', + 'title' => __( 'General', 'better-search' ), + 'content' => + '

' . __( 'This screen provides some tools that help maintain certain features of Better Search.', 'better-search' ) . '

' . + '

' . __( 'Clear the cache, reset the popular posts tables plus some miscellaneous fixes for older versions of Better Search.', 'better-search' ) . '

', + ) + ); + + do_action( 'bsearch_settings_tools_help', $screen ); + } +} diff --git a/includes/admin/default-settings.php b/includes/admin/default-settings.php deleted file mode 100644 index 61a8dd5..0000000 --- a/includes/admin/default-settings.php +++ /dev/null @@ -1,620 +0,0 @@ - bsearch_settings_general(), - 'search' => bsearch_settings_search(), - 'heatmap' => bsearch_settings_heatmap(), - 'styles' => bsearch_settings_styles(), - ); - - /** - * Filters the settings array - * - * @since 2.2.0 - * - * @param array $bsearch_setings Settings array - */ - return apply_filters( 'bsearch_registered_settings', $bsearch_settings ); -} - -/** - * Retrieve the array of General settings - * - * @since 2.5.0 - * - * @return array General settings array - */ -function bsearch_settings_general() { - - $settings = array( - 'seamless' => array( - 'id' => 'seamless', - 'name' => esc_html__( 'Enable seamless integration', 'better-search' ), - 'desc' => esc_html__( "Complete integration with your theme. Enabling this option will ignore better-search-template.php. It will continue to display the search results sorted by relevance, although it won't display the percentage relevance.", 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'track_popular' => array( - 'id' => 'track_popular', - 'name' => esc_html__( 'Enable search tracking', 'better-search' ), - 'desc' => esc_html__( 'If you turn this off, then the plugin will no longer track and display the popular search terms.', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'track_admins' => array( - 'id' => 'track_admins', - 'name' => esc_html__( 'Track admin searches', 'better-search' ), - 'desc' => esc_html__( 'Disabling this option will stop searches made by admins from being tracked.', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'track_editors' => array( - 'id' => 'track_editors', - 'name' => esc_html__( 'Track editor user group searches', 'better-search' ), - 'desc' => esc_html__( 'Disabling this option will stop searches made by editors from being tracked.', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'cache' => array( - 'id' => 'cache', - 'name' => esc_html__( 'Enable cache', 'better-search' ), - 'desc' => esc_html__( 'If activated, Better Search will use the Transients API to cache the search results for 1 hour.', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - 'cache_time' => array( - 'id' => 'cache_time', - 'name' => esc_html__( 'Time to cache', 'top-10' ), - 'desc' => esc_html__( 'Enter the number of seconds to cache the output.', 'top-10' ), - 'type' => 'text', - 'options' => HOUR_IN_SECONDS, - ), - 'meta_noindex' => array( - 'id' => 'meta_noindex', - 'name' => esc_html__( 'Stop search engines from indexing search results pages', 'better-search' ), - 'desc' => esc_html__( 'This is a recommended option to turn ON. Adds noindex,follow meta tag to the head of the page', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'number_format_count' => array( - 'id' => 'number_format_count', - 'name' => esc_html__( 'Number format count', 'better-search' ), - 'desc' => esc_html__( 'Activating this option will convert the search counts into a number format based on the locale', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'show_credit' => array( - 'id' => 'show_credit', - 'name' => esc_html__( 'Link to Better Search plugin page', 'better-search' ), - 'desc' => esc_html__( 'A nofollow link to the plugin is added as an extra list item to the list of popular searches. Not mandatory, but thanks if you do it!', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - ); - - /** - * Filters the General settings array - * - * @since 2.5.0 - * - * @param array $settings General settings array - */ - return apply_filters( 'bsearch_settings_general', $settings ); -} - - -/** - * Retrieve the array of Search settings - * - * @since 2.5.0 - * - * @return array Search settings array - */ -function bsearch_settings_search() { - - $settings = array( - 'limit' => array( - 'id' => 'limit', - 'name' => esc_html__( 'Number of Search Results per page', 'better-search' ), - 'desc' => esc_html__( 'This is the maximum number of search results that will be displayed per page by default', 'better-search' ), - 'type' => 'number', - 'options' => '10', - 'size' => 'small', - ), - 'post_types' => array( - 'id' => 'post_types', - 'name' => esc_html__( 'Post types to include', 'better-search' ), - 'desc' => esc_html__( 'Select which post types you want to include in the search results', 'better-search' ), - 'type' => 'posttypes', - 'options' => 'post,page', - ), - 'use_fulltext' => array( - 'id' => 'use_fulltext', - 'name' => esc_html__( 'Enable mySQL FULLTEXT searching', 'better-search' ), - 'desc' => esc_html__( 'Disabling this option will no longer give relevancy based results', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'boolean_mode' => array( - 'id' => 'boolean_mode', - 'name' => esc_html__( 'Activate BOOLEAN mode', 'better-search' ), - /* translators: 1: Opening anchor tag, 2: Closing anchor tag, */ - 'desc' => sprintf( esc_html__( 'Limits relevancy matches but removes several limitations of NATURAL LANGUAGE mode. %1$sCheck the mySQL docs for further information on BOOLEAN indices%2$s', 'better-search' ), '', '' ), - 'type' => 'checkbox', - 'options' => false, - ), - 'weight_title' => array( - 'id' => 'weight_title', - 'name' => esc_html__( 'Weight of the title', 'better-search' ), - 'desc' => esc_html__( 'Set this to a bigger number than the next option to prioritize the post title', 'better-search' ), - 'type' => 'number', - 'options' => '10', - 'size' => 'small', - ), - 'weight_content' => array( - 'id' => 'weight_content', - 'name' => esc_html__( 'Weight of the post content', 'better-search' ), - 'desc' => esc_html__( 'Set this to a bigger number than the previous option to prioritize the post content', 'better-search' ), - 'type' => 'number', - 'options' => '1', - 'size' => 'small', - ), - 'search_excerpt' => array( - 'id' => 'search_excerpt', - 'name' => esc_html__( 'Search Excerpt', 'better-search' ), - 'desc' => esc_html__( 'Select to search the post excerpt.', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'search_taxonomies' => array( - 'id' => 'search_taxonomies', - 'name' => esc_html__( 'Search Taxonomies', 'better-search' ), - 'desc' => esc_html__( 'Select to include posts where all taxonomies match the search term(s). This includes categories, tags and custom post types.', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - 'search_meta' => array( - 'id' => 'search_meta', - 'name' => esc_html__( 'Search Meta', 'better-search' ), - 'desc' => esc_html__( 'Select to include posts where meta values match the search term(s).', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - 'search_authors' => array( - 'id' => 'search_authors', - 'name' => esc_html__( 'Search Authors', 'better-search' ), - 'desc' => esc_html__( 'Select to include posts from authors that match the search term(s).', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - 'search_comments' => array( - 'id' => 'search_comments', - 'name' => esc_html__( 'Search Comments', 'better-search' ), - 'desc' => esc_html__( 'Select to include posts where comments include the search term(s).', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - 'exclude_protected_posts' => array( - 'id' => 'exclude_protected_posts', - 'name' => esc_html__( 'Exclude password protected posts', 'better-search' ), - 'desc' => esc_html__( 'Enabling this option will remove password protected posts from the search results', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'exclude_post_ids' => array( - 'id' => 'exclude_post_ids', - 'name' => esc_html__( 'Exclude post IDs', 'better-search' ), - 'desc' => esc_html__( 'Enter a comma separated list of post/page/custom post type IDs e.g. 188,1024,50', 'better-search' ), - 'type' => 'numbercsv', - 'options' => '', - ), - 'exclude_cat_slugs' => array( - 'id' => 'exclude_cat_slugs', - 'name' => esc_html__( 'Exclude Categories', 'better-search' ), - 'desc' => esc_html__( 'Comma separated list of category slugs. The field above has an autocomplete so simply start typing in the starting letters and it will prompt you with options. Does not support custom taxonomies.', 'better-search' ), - 'type' => 'csv', - 'options' => '', - 'size' => 'large', - 'field_class' => 'category_autocomplete', - 'field_attributes' => array( - 'data-wp-taxonomy' => 'category', - ), - ), - 'exclude_categories' => array( - 'id' => 'exclude_categories', - 'name' => esc_html__( 'Exclude category IDs', 'better-search' ), - 'desc' => esc_html__( 'This is a readonly field that is automatically populated based on the above input when the settings are saved. These might differ from the IDs visible in the Categories page which use the term_id. Better Search uses the term_taxonomy_id which is unique to this taxonomy.', 'better-search' ), - 'type' => 'text', - 'options' => '', - 'readonly' => true, - ), - 'display_header' => array( - 'id' => 'display_header', - 'name' => '

' . esc_html__( 'Display options', 'better-search' ) . '

', - 'desc' => esc_html__( 'These settings allow you to customize the output of the search results page. Except for the highlight setting, these only apply when Seamless mode is off.', 'better-search' ), - 'type' => 'header', - ), - 'highlight' => array( - 'id' => 'highlight', - 'name' => esc_html__( 'Highlight search terms', 'better-search' ), - 'desc' => esc_html__( 'If enabled, the search terms are wrapped with the class bsearch_highlight on the search results page. The default stylesheet includes CSS to add some colour.', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'highlight_followed_links' => array( - 'id' => 'highlight_followed_links', - 'name' => esc_html__( 'Highlight followed links', 'better-search' ), - 'desc' => esc_html__( 'If enabled, the plugin will highlight the search terms on posts/pages when visits them from the search results page.', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'include_thumb' => array( - 'id' => 'include_thumb', - 'name' => esc_html__( 'Display thumbnail', 'better-search' ), - 'desc' => '', - 'type' => 'checkbox', - 'options' => true, - ), - 'display_relevance' => array( - 'id' => 'display_relevance', - 'name' => esc_html__( 'Display relevance', 'better-search' ), - 'desc' => '', - 'type' => 'checkbox', - 'options' => true, - ), - 'display_post_type' => array( - 'id' => 'display_post_type', - 'name' => esc_html__( 'Display post type', 'better-search' ), - 'desc' => '', - 'type' => 'checkbox', - 'options' => true, - ), - 'display_author' => array( - 'id' => 'display_author', - 'name' => esc_html__( 'Display author', 'better-search' ), - 'desc' => '', - 'type' => 'checkbox', - 'options' => true, - ), - 'display_date' => array( - 'id' => 'display_date', - 'name' => esc_html__( 'Display date', 'better-search' ), - 'desc' => '', - 'type' => 'checkbox', - 'options' => true, - ), - 'display_taxonomies' => array( - 'id' => 'display_taxonomies', - 'name' => esc_html__( 'Display taxonomies', 'better-search' ), - 'desc' => '', - 'type' => 'checkbox', - 'options' => true, - ), - 'excerpt_length' => array( - 'id' => 'excerpt_length', - 'name' => esc_html__( 'Length of excerpt (in words)', 'better-search' ), - 'desc' => '', - 'type' => 'number', - 'options' => '100', - 'size' => 'small', - ), - 'banned_header' => array( - 'id' => 'banned_header', - 'name' => '

' . esc_html__( 'Banned words options', 'better-search' ) . '

', - 'desc' => '', - 'type' => 'header', - ), - 'badwords' => array( - 'id' => 'badwords', - 'name' => esc_html__( 'Filter these words', 'better-search' ), - 'desc' => esc_html__( 'Words in this list will be stripped out of the search results. Enter these as a comma-separated list.', 'better-search' ), - 'type' => 'textarea', - 'options' => implode( ',', bsearch_get_badwords() ), - ), - 'banned_whole_words' => array( - 'id' => 'banned_whole_words', - 'name' => esc_html__( 'Filter whole words only', 'better-search' ), - 'desc' => esc_html__( 'When activated, only whole words in the search query are filtered. Partial words are ignored. e.g. grow will not ban grown or grower.', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - 'banned_stop_search' => array( - 'id' => 'banned_stop_search', - 'name' => esc_html__( 'Stop query on banned words filter', 'better-search' ), - 'desc' => esc_html__( 'When activated, this option will return no results if the search query includes any of the words in the box above. If you have seamless mode off, Better Search will display an error message. With seamless mode on, this will give a Nothing found message. You can customize it by editing your theme.', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - ); - - /** - * Filters the Search settings array - * - * @since 2.5.0 - * - * @param array $settings Search settings array - */ - return apply_filters( 'bsearch_settings_search', $settings ); -} - - -/** - * Retrieve the array of Heatmap settings - * - * @since 2.5.0 - * - * @return array Heatmap settings array - */ -function bsearch_settings_heatmap() { - - $settings = array( - 'include_heatmap' => array( - 'id' => 'include_heatmap', - 'name' => esc_html__( 'Include heatmap on the search results', 'better-search' ), - 'desc' => esc_html__( 'This option will display the heatmaps at the bottom of the search results page. Display popular searches to your visitors. Does not apply when Seamless mode is enabled.', 'better-search' ), - 'type' => 'checkbox', - 'options' => false, - ), - 'title' => array( - 'id' => 'title', - 'name' => esc_html__( 'Heading of Overall Popular Searches', 'better-search' ), - 'desc' => esc_html__( 'Displayed before the list of the searches as a the master heading', 'better-search' ), - 'type' => 'text', - 'options' => '

' . esc_html__( 'Popular searches:', 'better-search' ) . '

', - 'size' => 'large', - ), - 'title_daily' => array( - 'id' => 'title_daily', - 'name' => esc_html__( 'Heading of Daily Popular Searches', 'better-search' ), - 'desc' => esc_html__( 'Displayed before the list of the searches as a the master heading', 'better-search' ), - 'type' => 'text', - 'options' => '

' . esc_html__( 'Currently trending searches:', 'better-search' ) . '

', - 'size' => 'large', - ), - 'daily_range' => array( - 'id' => 'daily_range', - 'name' => esc_html__( 'Currently trending should contain searches of how many days?', 'better-search' ), - 'desc' => esc_html__( 'This settings allows you to change the number of days for the currently trending heatmap. This used to be called Daily popular in previous versions.', 'better-search' ), - 'type' => 'number', - 'options' => '7', - 'size' => 'small', - ), - 'heatmap_limit' => array( - 'id' => 'heatmap_limit', - 'name' => esc_html__( 'Number of search terms to display', 'better-search' ), - 'desc' => '', - 'type' => 'number', - 'options' => '20', - 'size' => 'small', - ), - 'heatmap_smallest' => array( - 'id' => 'heatmap_smallest', - 'name' => esc_html__( 'Font size of least popular search term', 'better-search' ), - 'desc' => '', - 'type' => 'number', - 'options' => '10', - 'size' => 'small', - ), - 'heatmap_largest' => array( - 'id' => 'heatmap_largest', - 'name' => esc_html__( 'Font size of most popular search term', 'better-search' ), - 'desc' => '', - 'type' => 'number', - 'options' => '20', - 'size' => 'small', - ), - 'heatmap_cold' => array( - 'id' => 'heatmap_cold', - 'name' => esc_html__( 'Color of least popular search term', 'better-search' ), - 'desc' => '', - 'type' => 'color', - 'options' => '#cccccc', - 'field_class' => 'color-field', - 'field_attributes' => array( - 'data-default-color' => '#cccccc', - ), - ), - 'heatmap_hot' => array( - 'id' => 'heatmap_hot', - 'name' => esc_html__( 'Color of most popular search term', 'better-search' ), - 'desc' => '', - 'type' => 'color', - 'options' => '#000000', - 'field_class' => 'color-field', - 'field_attributes' => array( - 'data-default-color' => '#000000', - ), - ), - 'heatmap_before' => array( - 'id' => 'heatmap_before', - 'name' => esc_html__( 'Text to include before each search term', 'better-search' ), - 'desc' => '', - 'type' => 'text', - 'options' => '', - ), - 'heatmap_after' => array( - 'id' => 'heatmap_after', - 'name' => esc_html__( 'Text to include after each search term', 'better-search' ), - 'desc' => '', - 'type' => 'text', - 'options' => ' ', - ), - 'link_new_window' => array( - 'id' => 'link_new_window', - 'name' => esc_html__( 'Open links in new window', 'better-search' ), - 'desc' => '', - 'type' => 'checkbox', - 'options' => false, - ), - 'link_nofollow' => array( - 'id' => 'link_nofollow', - 'name' => esc_html__( 'Add nofollow to links', 'better-search' ), - 'desc' => '', - 'type' => 'checkbox', - 'options' => true, - ), - ); - - /** - * Filters the Heatmap settings array - * - * @since 2.5.0 - * - * @param array $settings Heatmap settings array - */ - return apply_filters( 'bsearch_settings_heatmap', $settings ); -} - - -/** - * Retrieve the array of Styles settings - * - * @since 2.5.0 - * - * @return array Styles settings array - */ -function bsearch_settings_styles() { - - $settings = array( - 'include_styles' => array( - 'id' => 'include_styles', - 'name' => esc_html__( 'Include inbuilt styles', 'better-search' ), - 'desc' => esc_html__( 'Uncheck this to disable this plugin from adding the inbuilt styles. You will need to add your own CSS styles if you disable this option', 'better-search' ), - 'type' => 'checkbox', - 'options' => true, - ), - 'custom_css' => array( - 'id' => 'custom_css', - 'name' => esc_html__( 'Custom CSS', 'better-search' ), - /* translators: 1: Opening a tag, 2: Closing a tag, 3: Opening code tage, 4. Closing code tag. */ - 'desc' => sprintf( esc_html__( 'Do not include %3$sstyle%4$s tags. Check out the %1$sFAQ%2$s for available CSS classes to style.', 'better-search' ), '', '', '', '' ), - 'type' => 'css', - 'options' => '', - 'field_class' => 'codemirror_css', - ), - ); - - /** - * Filters the Styles settings array - * - * @since 2.5.0 - * - * @param array $settings Styles settings array - */ - return apply_filters( 'bsearch_settings_styles', $settings ); -} - -/** - * Get badwords to filter. - * - * @since 2.2.0 - * - * @return array Array containing bad words to filter - */ -function bsearch_get_badwords() { - - $badwords = array( - 'anal', - 'anus', - 'bastard', - 'beastiality', - 'bestiality', - 'bewb', - 'bitch', - 'blow', - 'blumpkin', - 'boob', - 'cawk', - 'cock', - 'choad', - 'cooter', - 'cornhole', - 'cum', - 'cunt', - 'dick', - 'dildo', - 'dong', - 'dyke', - 'douche', - 'fag', - 'faggot', - 'fart', - 'foreskin', - 'fuck', - 'fuk', - 'gangbang', - 'gook', - 'handjob', - 'homo', - 'honkey', - 'humping', - 'jiz', - 'jizz', - 'kike', - 'kunt', - 'labia', - 'muff', - 'nigger', - 'nutsack', - 'pen1s', - 'penis', - 'piss', - 'poon', - 'poop', - 'porn', - 'punani', - 'pussy', - 'queef', - 'quim', - 'rimjob', - 'rape', - 'rectal', - 'rectum', - 'semen', - 'shit', - 'slut', - 'spick', - 'spoo', - 'spooge', - 'taint', - 'titty', - 'titties', - 'twat', - 'vagina', - 'vulva', - 'wank', - 'whore', - ); - - /** - * Filters bad words array. - * - * @since 2.2.0 - * - * @param array Array containing bad words to filter - */ - return apply_filters( 'bsearch_get_badwords', $badwords ); -} diff --git a/includes/admin/help-tab.php b/includes/admin/help-tab.php deleted file mode 100644 index 1682bda..0000000 --- a/includes/admin/help-tab.php +++ /dev/null @@ -1,127 +0,0 @@ -id !== $bsearch_settings_page ) { - return; - } - - $screen->set_help_sidebar( - /* translators: 1: Support link. */ - '

' . sprintf( __( 'For more information or how to get support visit the WebberZone support site.', 'better-search' ), esc_url( 'https://webberzone.com/support/' ) ) . '

' . - /* translators: 1: Forum link. */ - '

' . sprintf( __( 'Support queries should be posted in the WordPress.org support forums.', 'better-search' ), esc_url( 'https://wordpress.org/support/plugin/better-search' ) ) . '

' . - '

' . sprintf( - /* translators: 1: Github Issues link, 2: Github page. */ - __( 'Post an issue on GitHub (bug reports only).', 'better-search' ), - esc_url( 'https://github.com/WebberZone/better-search/issues' ), - esc_url( 'https://github.com/WebberZone/better-search' ) - ) . '

' - ); - - $screen->add_help_tab( - array( - 'id' => 'bsearch-settings-general', - 'title' => __( 'General', 'better-search' ), - 'content' => - '

' . __( 'This screen provides the basic settings for configuring Better Search.', 'better-search' ) . '

' . - '

' . __( 'Enable tracking, seamless mode and the cache, configure basic tracker and uninstall settings.', 'better-search' ) . '

', - ) - ); - - $screen->add_help_tab( - array( - 'id' => 'bsearch-settings-search', - 'title' => __( 'Search', 'better-search' ), - 'content' => - '

' . __( 'This screen provides settings to tweak the search algorithm.', 'better-search' ) . '

' . - '

' . __( 'Configure number of search results, enable FULLTEXT and BOOLEAN mode, tweak the weight of title and content and block words.', 'better-search' ) . '

', - ) - ); - - $screen->add_help_tab( - array( - 'id' => 'bsearch-settings-heatmap', - 'title' => __( 'Heatmap', 'better-search' ), - 'content' => - '

' . __( 'This screen provides settings to tweak the output of the search heatmap to display popular searches.', 'better-search' ) . '

' . - '

' . __( 'Configure title of the searches, period of trending searches, color and font sizes of the heatmap.', 'better-search' ) . '

', - ) - ); - - $screen->add_help_tab( - array( - 'id' => 'bsearch-settings-styles', - 'title' => __( 'Styles', 'better-search' ), - 'content' => - '

' . __( 'This screen provides options to control the look and feel of the search page.', 'better-search' ) . '

' . - '

' . __( 'Choose for default set of styles or add your own custom CSS to tweak the display of the search results page.', 'better-search' ) . '

', - ) - ); - - do_action( 'bsearch_settings_help', $screen ); -} - -/** - * Generates the Tools help page. - * - * @since 2.2.0 - */ -function bsearch_settings_tools_help() { - global $bsearch_settings_tools_help; - - $screen = get_current_screen(); - - if ( $screen->id !== $bsearch_settings_tools_help ) { - return; - } - - $screen->set_help_sidebar( - /* translators: 1: Support link. */ - '

' . sprintf( __( 'For more information or how to get support visit the WebberZone support site.', 'better-search' ), esc_url( 'https://webberzone.com/support/' ) ) . '

' . - /* translators: 1: Forum link. */ - '

' . sprintf( __( 'Support queries should be posted in the WordPress.org support forums.', 'better-search' ), esc_url( 'https://wordpress.org/support/plugin/better-search' ) ) . '

' . - '

' . sprintf( - /* translators: 1: Github Issues link, 2: Github page. */ - __( 'Post an issue on GitHub (bug reports only).', 'better-search' ), - esc_url( 'https://github.com/WebberZone/better-search/issues' ), - esc_url( 'https://github.com/WebberZone/better-search' ) - ) . '

' - ); - - $screen->add_help_tab( - array( - 'id' => 'bsearch-settings-general', - 'title' => __( 'General', 'better-search' ), - 'content' => - '

' . __( 'This screen provides some tools that help maintain certain features of Better Search.', 'better-search' ) . '

' . - '

' . __( 'Clear the cache, reset the popular posts tables plus some miscellaneous fixes for older versions of Better Search.', 'better-search' ) . '

', - ) - ); - - do_action( 'bsearch_settings_tools_help', $screen ); -} diff --git a/includes/admin/images/twitter.jpg b/includes/admin/images/twitter.jpg deleted file mode 100644 index 7a31297..0000000 Binary files a/includes/admin/images/twitter.jpg and /dev/null differ diff --git a/includes/admin/images/x.png b/includes/admin/images/x.png new file mode 100644 index 0000000..2c2d7e8 Binary files /dev/null and b/includes/admin/images/x.png differ diff --git a/includes/admin/js/admin-scripts.js b/includes/admin/js/admin-scripts.js index e67aee8..42becac 100644 --- a/includes/admin/js/admin-scripts.js +++ b/includes/admin/js/admin-scripts.js @@ -8,14 +8,14 @@ function clearCache() { security: bsearch_admin_data.security }, function (response, textStatus, jqXHR) { - alert( response.message ); + alert(response.message); }, 'json' ); } -jQuery( document ).ready( - function($) { +jQuery(document).ready( + function ($) { // Prompt the user when they leave the page without saving the form. var formmodified = 0; @@ -24,7 +24,7 @@ jQuery( document ).ready( } function confirmExit() { - if ( formmodified == 1 ) { + if (formmodified == 1) { return true; } } @@ -33,26 +33,26 @@ jQuery( document ).ready( formmodified = 0; } - $( 'form *' ).change( confirmFormChange ); + $('form *').change(confirmFormChange); window.onbeforeunload = confirmExit; - $( "input[name='submit']" ).click( formNotModified ); - $( "input[id='search-submit']" ).click( formNotModified ); - $( "input[id='doaction']" ).click( formNotModified ); - $( "input[id='doaction2']" ).click( formNotModified ); - $( "input[name='filter_action']" ).click( formNotModified ); + $("input[name='submit']").click(formNotModified); + $("input[id='search-submit']").click(formNotModified); + $("input[id='doaction']").click(formNotModified); + $("input[id='doaction2']").click(formNotModified); + $("input[name='filter_action']").click(formNotModified); $( - function() { - $( "#post-body-content" ).tabs( + function () { + $("#post-body-content").tabs( { - create: function( event, ui ) { - $( ui.tab.find( "a" ) ).addClass( "nav-tab-active" ); + create: function (event, ui) { + $(ui.tab.find("a")).addClass("nav-tab-active"); }, - activate: function( event, ui ) { - $( ui.oldTab.find( "a" ) ).removeClass( "nav-tab-active" ); - $( ui.newTab.find( "a" ) ).addClass( "nav-tab-active" ); + activate: function (event, ui) { + $(ui.oldTab.find("a")).removeClass("nav-tab-active"); + $(ui.newTab.find("a")).addClass("nav-tab-active"); } } ); @@ -61,42 +61,42 @@ jQuery( document ).ready( // Datepicker. $( - function() { + function () { var dateFormat = 'dd M yy', - from = $( "#datepicker-from" ) - .datepicker( - { - changeMonth: true, - maxDate: 0, - dateFormat: dateFormat - } - ) - .on( - "change", - function() { - to.datepicker( "option", "minDate", getDate( this ) ); - } - ), - to = $( "#datepicker-to" ) - .datepicker( - { - changeMonth: true, - maxDate: 0, - dateFormat: dateFormat - } - ) - .on( - "change", - function() { - from.datepicker( "option", "maxDate", getDate( this ) ); - } - ); - - function getDate( element ) { + from = $("#datepicker-from") + .datepicker( + { + changeMonth: true, + maxDate: 0, + dateFormat: dateFormat + } + ) + .on( + "change", + function () { + to.datepicker("option", "minDate", getDate(this)); + } + ), + to = $("#datepicker-to") + .datepicker( + { + changeMonth: true, + maxDate: 0, + dateFormat: dateFormat + } + ) + .on( + "change", + function () { + from.datepicker("option", "maxDate", getDate(this)); + } + ); + + function getDate(element) { var date; try { - date = $.datepicker.parseDate( dateFormat, element.value ); - } catch ( error ) { + date = $.datepicker.parseDate(dateFormat, element.value); + } catch (error) { date = null; } @@ -104,64 +104,5 @@ jQuery( document ).ready( } } ); - - // Initialise CodeMirror. - $( ".codemirror_html" ).each( - function( index, element ) { - if ( $( element ).length && typeof wp.codeEditor === 'object' ) { - var editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {}; - editorSettings.codemirror = _.extend( - {}, - editorSettings.codemirror, - { - } - ); - var editor = wp.codeEditor.initialize( $( element ), editorSettings ); - editor.codemirror.on( 'change', confirmFormChange ); - } - } - ); - - $( ".codemirror_js" ).each( - function( index, element ) { - if ( $( element ).length && typeof wp.codeEditor === 'object' ) { - var editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {}; - editorSettings.codemirror = _.extend( - {}, - editorSettings.codemirror, - { - mode: 'javascript', - } - ); - var editor = wp.codeEditor.initialize( $( element ), editorSettings ); - editor.codemirror.on( 'change', confirmFormChange ); - } - } - ); - - $( ".codemirror_css" ).each( - function( index, element ) { - if ( $( element ).length && typeof wp.codeEditor === 'object' ) { - var editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {}; - editorSettings.codemirror = _.extend( - {}, - editorSettings.codemirror, - { - mode: 'css', - } - ); - var editor = wp.codeEditor.initialize( $( element ), editorSettings ); - editor.codemirror.on( 'change', confirmFormChange ); - } - } - ); - - // Initialise ColorPicker - $( '.color-field' ).each( - function ( i, element ) { - $( element ).wpColorPicker(); - } - ); - } ); diff --git a/includes/admin/js/admin-scripts.min.js b/includes/admin/js/admin-scripts.min.js index 6cd506c..da7c684 100644 --- a/includes/admin/js/admin-scripts.min.js +++ b/includes/admin/js/admin-scripts.min.js @@ -1 +1 @@ -function clearCache(){jQuery.post(ajaxurl,{action:"bsearch_clear_cache",security:bsearch_admin_data.security},(function(response,textStatus,jqXHR){alert(response.message)}),"json")}jQuery(document).ready((function($){var formmodified=0;function confirmFormChange(){formmodified=1}function confirmExit(){if(1==formmodified)return!0}function formNotModified(){formmodified=0}$("form *").change(confirmFormChange),window.onbeforeunload=confirmExit,$("input[name='submit']").click(formNotModified),$("input[id='search-submit']").click(formNotModified),$("input[id='doaction']").click(formNotModified),$("input[id='doaction2']").click(formNotModified),$("input[name='filter_action']").click(formNotModified),$((function(){$("#post-body-content").tabs({create:function(event,ui){$(ui.tab.find("a")).addClass("nav-tab-active")},activate:function(event,ui){$(ui.oldTab.find("a")).removeClass("nav-tab-active"),$(ui.newTab.find("a")).addClass("nav-tab-active")}})})),$((function(){var dateFormat="dd M yy",from=$("#datepicker-from").datepicker({changeMonth:!0,maxDate:0,dateFormat:"dd M yy"}).on("change",(function(){to.datepicker("option","minDate",getDate(this))})),to=$("#datepicker-to").datepicker({changeMonth:!0,maxDate:0,dateFormat:"dd M yy"}).on("change",(function(){from.datepicker("option","maxDate",getDate(this))}));function getDate(element){var date;try{date=$.datepicker.parseDate("dd M yy",element.value)}catch(error){date=null}return date}})),$(".codemirror_html").each((function(index,element){if($(element).length&&"object"==typeof wp.codeEditor){var editorSettings=wp.codeEditor.defaultSettings?_.clone(wp.codeEditor.defaultSettings):{},editor;editorSettings.codemirror=_.extend({},editorSettings.codemirror,{}),wp.codeEditor.initialize($(element),editorSettings).codemirror.on("change",confirmFormChange)}})),$(".codemirror_js").each((function(index,element){if($(element).length&&"object"==typeof wp.codeEditor){var editorSettings=wp.codeEditor.defaultSettings?_.clone(wp.codeEditor.defaultSettings):{},editor;editorSettings.codemirror=_.extend({},editorSettings.codemirror,{mode:"javascript"}),wp.codeEditor.initialize($(element),editorSettings).codemirror.on("change",confirmFormChange)}})),$(".codemirror_css").each((function(index,element){if($(element).length&&"object"==typeof wp.codeEditor){var editorSettings=wp.codeEditor.defaultSettings?_.clone(wp.codeEditor.defaultSettings):{},editor;editorSettings.codemirror=_.extend({},editorSettings.codemirror,{mode:"css"}),wp.codeEditor.initialize($(element),editorSettings).codemirror.on("change",confirmFormChange)}})),$(".color-field").each((function(i,element){$(element).wpColorPicker()}))})); \ No newline at end of file +function clearCache(){jQuery.post(ajaxurl,{action:"bsearch_clear_cache",security:bsearch_admin_data.security},(function(a,t,n){alert(a.message)}),"json")}jQuery(document).ready((function(a){var t=0;function n(){t=0}a("form *").change((function(){t=1})),window.onbeforeunload=function(){if(1==t)return!0},a("input[name='submit']").click(n),a("input[id='search-submit']").click(n),a("input[id='doaction']").click(n),a("input[id='doaction2']").click(n),a("input[name='filter_action']").click(n),a((function(){a("#post-body-content").tabs({create:function(t,n){a(n.tab.find("a")).addClass("nav-tab-active")},activate:function(t,n){a(n.oldTab.find("a")).removeClass("nav-tab-active"),a(n.newTab.find("a")).addClass("nav-tab-active")}})})),a((function(){var t="dd M yy",n=a("#datepicker-from").datepicker({changeMonth:!0,maxDate:0,dateFormat:t}).on("change",(function(){e.datepicker("option","minDate",c(this))})),e=a("#datepicker-to").datepicker({changeMonth:!0,maxDate:0,dateFormat:t}).on("change",(function(){n.datepicker("option","maxDate",c(this))}));function c(n){var e;try{e=a.datepicker.parseDate(t,n.value)}catch(a){e=null}return e}}))})); \ No newline at end of file diff --git a/includes/admin/js/better-search-suggest.js b/includes/admin/js/better-search-suggest.js deleted file mode 100644 index b4aa3d8..0000000 --- a/includes/admin/js/better-search-suggest.js +++ /dev/null @@ -1,128 +0,0 @@ -jQuery( document ).ready( - function($) { - // Function to add auto suggest. - $.fn.bsearchTagsSuggest = function( options ) { - var cache; - var last; - var $element = $( this ); - - options = options || {}; - - var taxonomy = options.taxonomy || $element.attr( 'data-wp-taxonomy' ) || 'category'; - delete( options.taxonomy ); - - function split( val ) { - return val.split( /,(?=(?:(?:[^"]*"){2})*[^"]*$)/ ); // Split typical CSV format, with commas and double quotes. - } - - function extractLast( term ) { - return split( term ).pop(); - } - - options = $.extend( - { - minLength: 2, - position: { - my: 'left top+2', - at: 'left bottom', - collision: 'none' - }, - source: function( request, response ) { - var term; - - if ( last === request.term ) { - response( cache ); - return; - } - - term = extractLast( request.term ); - - if ( last === request.term ) { - response( cache ); - return; - } - - $.ajax( - { - type: 'POST', - dataType: 'json', - url: ajaxurl, - data: { - action: 'bsearch_tag_search', - tax: taxonomy, - q: term - }, - } - ).done( - function( data ) { - cache = data; - response( data ); - } - ); - - last = request.term; - - }, - search: function() { - // Custom minLength. - var term = extractLast( this.value ); - - if ( term.length < 2 ) { - return false; - } - }, - focus: function( event, ui ) { - // Prevent value inserted on focus. - event.preventDefault(); - }, - select: function( event, ui ) { - var terms = split( this.value ); - var val = ui.item.value; - - if ( val.indexOf( ',' ) !== -1 ) { - val = '"' + val + '"' - } - - // Remove the last user input. - terms.pop(); - - // Add the selected item. - terms.push( val ); - - // Add placeholder to get the comma-and-space at the end. - terms.push( "" ); - this.value = terms.join( ", " ); - return false; - } - }, - options - ); - - $element.on( - "keydown", - function( event ) { - // Don't navigate away from the field on tab when selecting an item. - if ( event.keyCode === $.ui.keyCode.TAB && - $( this ).autocomplete( 'instance' ).menu.active ) { - event.preventDefault(); - } - } - ) - .autocomplete( options ); - }; - - $( '.category_autocomplete' ).each( - function ( i, element ) { - $( element ).bsearchTagsSuggest(); - } - ); - - $( '.widget-liquid-right, #customize-controls' ).on( - 'click', - '.category_autocomplete', - function() { - $( '.category_autocomplete' ).bsearchTagsSuggest(); - } - ); - } -); diff --git a/includes/admin/js/better-search-suggest.min.js b/includes/admin/js/better-search-suggest.min.js deleted file mode 100644 index 912075e..0000000 --- a/includes/admin/js/better-search-suggest.min.js +++ /dev/null @@ -1 +0,0 @@ -jQuery(document).ready((function($){$.fn.bsearchTagsSuggest=function(options){var cache,last,$element=$(this),taxonomy=(options=options||{}).taxonomy||$element.attr("data-wp-taxonomy")||"category";function split(val){return val.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)}function extractLast(term){return split(term).pop()}delete options.taxonomy,options=$.extend({minLength:2,position:{my:"left top+2",at:"left bottom",collision:"none"},source:function(request,response){var term;last!==request.term?(term=extractLast(request.term),last!==request.term?($.ajax({type:"POST",dataType:"json",url:ajaxurl,data:{action:"bsearch_tag_search",tax:taxonomy,q:term}}).done((function(data){cache=data,response(data)})),last=request.term):response(cache)):response(cache)},search:function(){var term;if(extractLast(this.value).length<2)return!1},focus:function(event,ui){event.preventDefault()},select:function(event,ui){var terms=split(this.value),val=ui.item.value;return-1!==val.indexOf(",")&&(val='"'+val+'"'),terms.pop(),terms.push(val),terms.push(""),this.value=terms.join(", "),!1}},options),$element.on("keydown",(function(event){event.keyCode===$.ui.keyCode.TAB&&$(this).autocomplete("instance").menu.active&&event.preventDefault()})).autocomplete(options)},$(".category_autocomplete").each((function(i,element){$(element).bsearchTagsSuggest()})),$(".widget-liquid-right, #customize-controls").on("click",".category_autocomplete",(function(){$(".category_autocomplete").bsearchTagsSuggest()}))})); \ No newline at end of file diff --git a/includes/admin/js/chart-data.js b/includes/admin/js/chart-data.js new file mode 100644 index 0000000..587ec89 --- /dev/null +++ b/includes/admin/js/chart-data.js @@ -0,0 +1,118 @@ +// Function to update the chart. +function updateChart() { + jQuery.post( + ajaxurl, + { + action: "bsearch_chart_data", + security: bsearch_chart_data.security, + from_date: jQuery("#datepicker-from").val(), + to_date: jQuery("#datepicker-to").val(), + }, + function (data) { + var date = []; + var searches = []; + + for (var i in data) { + date.push(data[i].date); + searches.push(data[i].searches); + } + window.bsearchChart.data.labels = date; + window.bsearchChart.data.datasets.forEach((dataset) => { + dataset.data = searches; + }); + window.bsearchChart.update(); + }, + "json" + ); +} + +jQuery(document).ready(function ($) { + $.ajax({ + type: "POST", + dataType: "json", + url: ajaxurl, + data: { + action: "bsearch_chart_data", + security: bsearch_chart_data.security, + from_date: $("#datepicker-from").val(), + to_date: $("#datepicker-to").val(), + }, + success: function (data) { + var date = []; + var searches = []; + + for (var i in data) { + date.push(data[i].date); + searches.push(data[i].searches); + } + + var ctx = $("#searches"); + var config = { + type: "bar", + data: { + labels: date, + datasets: [ + { + label: bsearch_chart_data.datasetlabel, + backgroundColor: "#70c4e1", + borderColor: "#70c4e1", + hoverBackgroundColor: "#ffbf00", + hoverBorderColor: "#ffbf00", + data: searches, + }, + ], + }, + plugins: [ChartDataLabels], + options: { + plugins: { + title: { + text: bsearch_chart_data.charttitle, + display: true, + }, + legend: { + display: false, + position: "bottom", + }, + datalabels: { + color: "#000000", + anchor: "end", + align: "top", + }, + }, + scales: { + x: { + type: "time", + time: { + tooltipFormat: "DD", + unit: "day", + displayFormats: { + day: "DD", + }, + }, + title: { + display: false, + labelString: "Date", + }, + }, + y: { + grace: "5%", + suggestedMin: 0, + display: true, + title: { + display: false, + text: bsearch_chart_data.datasetlabel, + color: "#000", + padding: { top: 30, left: 0, right: 0, bottom: 0 }, + }, + }, + }, + }, + }; + + window.bsearchChart = new Chart(ctx, config); + }, + error: function (data) { + console.log(data); + }, + }); +}); diff --git a/includes/admin/js/chart-data.min.js b/includes/admin/js/chart-data.min.js new file mode 100644 index 0000000..8420064 --- /dev/null +++ b/includes/admin/js/chart-data.min.js @@ -0,0 +1 @@ +function updateChart(){jQuery.post(ajaxurl,{action:"bsearch_chart_data",security:bsearch_chart_data.security,from_date:jQuery("#datepicker-from").val(),to_date:jQuery("#datepicker-to").val()},(function(a){var t=[],e=[];for(var r in a)t.push(a[r].date),e.push(a[r].searches);window.bsearchChart.data.labels=t,window.bsearchChart.data.datasets.forEach((a=>{a.data=e})),window.bsearchChart.update()}),"json")}jQuery(document).ready((function(a){a.ajax({type:"POST",dataType:"json",url:ajaxurl,data:{action:"bsearch_chart_data",security:bsearch_chart_data.security,from_date:a("#datepicker-from").val(),to_date:a("#datepicker-to").val()},success:function(t){var e=[],r=[];for(var o in t)e.push(t[o].date),r.push(t[o].searches);var s=a("#searches"),d={type:"bar",data:{labels:e,datasets:[{label:bsearch_chart_data.datasetlabel,backgroundColor:"#70c4e1",borderColor:"#70c4e1",hoverBackgroundColor:"#ffbf00",hoverBorderColor:"#ffbf00",data:r}]},plugins:[ChartDataLabels],options:{plugins:{title:{text:bsearch_chart_data.charttitle,display:!0},legend:{display:!1,position:"bottom"},datalabels:{color:"#000000",anchor:"end",align:"top"}},scales:{x:{type:"time",time:{tooltipFormat:"DD",unit:"day",displayFormats:{day:"DD"}},title:{display:!1,labelString:"Date"}},y:{grace:"5%",suggestedMin:0,display:!0,title:{display:!1,text:bsearch_chart_data.datasetlabel,color:"#000",padding:{top:30,left:0,right:0,bottom:0}}}}}};window.bsearchChart=new Chart(s,d)},error:function(a){}})})); \ No newline at end of file diff --git a/includes/admin/js/chart.min.js b/includes/admin/js/chart.min.js new file mode 100644 index 0000000..78c4e5d --- /dev/null +++ b/includes/admin/js/chart.min.js @@ -0,0 +1,20 @@ +/** + * Skipped minification because the original files appears to be already minified. + * Original file: /npm/chart.js@4.4.1/dist/chart.umd.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! + * Chart.js v4.4.1 + * https://www.chartjs.org + * (c) 2023 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,l,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class bt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var xt=new bt; +/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function be(t,e){return me(t).getPropertyValue(e)}const xe=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=xe[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=be(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Je(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Je(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e))return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Ze(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Je(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Ze(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const bi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,xi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(bi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(xi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hx||l(n,b,p)&&0!==r(n,b),v=()=>!x||0===r(o,p)||l(o,b,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==b&&(x=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,b=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Xi={evaluateInteractionItems:Hi,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tji(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Yi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Ui(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>Ui(t,ve(e,t),"y",i.intersect,s)}};const qi=["left","top","right","bottom"];function Ki(t,e){return t.filter((t=>t.pos===e))}function Gi(t,e){return t.filter((t=>-1===qi.indexOf(t.pos)&&t.box.axis===e))}function Zi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Ji(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!qi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function ss(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Zi(Ki(e,"left"),!0),n=Zi(Ki(e,"right")),o=Zi(Ki(e,"top"),!0),a=Zi(Ki(e,"bottom")),r=Gi(e,"x"),l=Gi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ki(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);ts(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Ji(l.concat(h),d);ss(r.fullSize,g,d,p),ss(l,g,d,p),ss(h,g,d,p)&&ss(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),os(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,os(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class rs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class ls extends rs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const hs="$chartjs",cs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ds=t=>null===t||""===t;const us=!!Se&&{passive:!0};function fs(t,e,i){t.canvas.removeEventListener(e,i,us)}function gs(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ps(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.addedNodes,s),e=e&&!gs(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function ms(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.removedNodes,s),e=e&&!gs(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const bs=new Map;let xs=0;function _s(){const t=window.devicePixelRatio;t!==xs&&(xs=t,bs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ys(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){bs.size||window.addEventListener("resize",_s),bs.set(t,e)}(t,o),a}function vs(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){bs.delete(t),bs.size||window.removeEventListener("resize",_s)}(t)}function Ms(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=cs[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t.addEventListener(e,i,us)}(s,e,n),n}class ws extends rs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[hs]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ds(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(ds(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[hs])return!1;const i=e[hs].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[hs],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:ps,detach:ms,resize:ys}[e]||Ms;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:vs,detach:vs,resize:vs}[e]||fs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=ge(t);return!(!e||!e.isConnected)}}function ks(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?ls:ws}var Ss=Object.freeze({__proto__:null,BasePlatform:rs,BasicPlatform:ls,DomPlatform:ws,_detectPlatform:ks});const Ps="transparent",Ds={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Ps),n=s.valid&&Qt(e||Ps);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Cs{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Ds[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Cs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(xt.add(this._chart,i),!0):void 0}}function As(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Ts(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function zs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Vs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Bs=t=>"reset"===t||"none"===t,Ws=(t,e)=>e?t:Object.assign({},t);class Ns{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Es(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Vs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Fs(t,"x")),o=e.yAxisID=l(i.yAxisID,Fs(t,"y")),a=e.rAxisID=l(i.rAxisID,Fs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Vs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e))this._data=function(t){const e=Object.keys(t),i=new Array(e.length);let s,n,o;for(s=0,n=e.length;s0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Ts(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ws(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Os(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Bs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Bs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Bs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function js(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for($s(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,Us=(t,e)=>Math.min(e||t,t);function Xs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ks(t){return t.drawTicks?t.tickLength:0}function Gs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Zs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class Js extends Hs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ks(t.grid)-e.padding-Gs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Gs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ks(n)+o):(t.height=this.maxHeight,t.width=Ks(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ks(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(t){return Ae(i,t,p)};let x,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)x=b(this.bottom),w=this.bottom-u,S=x-m,D=b(t.top)+m,O=t.bottom;else if("bottom"===a)x=b(this.top),D=t.top,O=b(t.bottom)-m,w=x+m,S=this.top+u;else if("left"===a)x=b(this.right),M=this.right-u,k=x-m,P=b(t.left)+m,C=t.right;else if("right"===a)x=b(this.left),P=t.left,C=b(t.right)-m,M=x+m,k=this.left+u;else if("x"===e){if("center"===a)x=b((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=x+m,S=w+u}else if("y"===e){if("center"===a)x=b((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}M=x-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}b.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return b}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class tn{constructor(){this.controllers=new Qs(Ns,"datasets",!0),this.elements=new Qs(Hs,"elements"),this.plugins=new Qs(Object,"plugins"),this.scales=new Qs(Js,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function nn(t,e){return e||!1!==t?!0===t?{}:t:null}function on(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function an(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function rn(t){if("x"===t||"y"===t||"r"===t)return t}function ln(t,...e){if(rn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&rn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function hn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function cn(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=an(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=ln(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return hn(t,"x",i[0])||hn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=x(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||an(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),x(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];x(e,[ue.scales[e.type],ue.scale])})),a}function dn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=cn(t,e)}function un(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const fn=new Map,gn=new Set;function pn(t,e){let i=fn.get(t);return i||(i=e(),fn.set(t,i),gn.add(i)),i}const mn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class bn{constructor(t){this._config=function(t){return(t=t||{}).data=un(t.data),dn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=un(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),dn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return pn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return pn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return pn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>mn(r,t,e)))),e.forEach((t=>mn(r,s,t))),e.forEach((t=>mn(r,re[n]||{},t))),e.forEach((t=>mn(r,ue,t))),e.forEach((t=>mn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),gn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=xn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||_n(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=xn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function xn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const _n=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const yn=["top","bottom","left","right","chartArea"];function vn(t,e){return"top"===t||"bottom"===t||-1===yn.indexOf(t)&&"x"===e}function Mn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function wn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function kn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Sn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Pn={},Dn=t=>{const e=Sn(t);return Object.values(Pn).filter((t=>t.canvas===e)).pop()};function Cn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}function On(t,e,i){return t.options.clip?t[i]:e[i]}class An{static defaults=ue;static instances=Pn;static overrides=re;static registry=en;static version="4.4.1";static getChart=Dn;static register(...t){en.add(...t),Tn()}static unregister(...t){en.remove(...t),Tn()}constructor(t,e){const s=this.config=new bn(e),n=Sn(t),o=Dn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ks(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new sn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Pn[this.id]=this,r&&l?(xt.listen(this,"complete",wn),xt.listen(this,"progress",kn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return en}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return xt.stop(this),this}resize(t,e){xt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=ln(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=ln(o,n),r=l(n.type,e.dtype);void 0!==n.position&&vn(n.position,a)===vn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(en.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{as.configure(this,t,t.options),as.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Mn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{as.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){Cn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;as.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t,e){const{xScale:i,yScale:s}=t;return i&&s?{left:On(i,e,"left"),right:On(i,e,"right"),top:On(s,e,"top"),bottom:On(s,e,"bottom")}:e}(t,this.chartArea),o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Ie(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&ze(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Xi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),xt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Tn(){return u(An.instances,(t=>t._plugins.invalidate()))}function Ln(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class En{static override(t){Object.assign(En.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ln()}parse(){return Ln()}format(){return Ln()}add(){return Ln()}diff(){return Ln()}startOf(){return Ln()}endOf(){return Ln()}}var Rn={_date:En};function In(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Fn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nZ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),b=g(C,h,d),x=g(C+E,c,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),b=(i.width-o)/f,x=(i.height-o)/g,_=Math.max(Math.min(b,x)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Yn=Object.freeze({__proto__:null,BarController:class extends Ns{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Fn(t,e,i,s)}parseArrayData(t,e,i,s){return Fn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=t=>{const i=t.controller.getParsed(e),n=i&&i[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!r(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(b-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);b=Math.max(Math.min(b,h),o),d=b+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(b))}if(b===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;b+=t,u-=t}return{size:u,base:b,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=b?g:{};if(i=x){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),b||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends jn{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:$n,RadarController:class extends Ns{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>b,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),x||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Un(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Xn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function qn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,b=n-p-f,{outerStart:x,outerEnd:_,innerStart:y,innerEnd:v}=Un(e,u,d,b-m),M=d-x,w=d-_,k=m+x/M,S=b-_/w,P=u+y,D=u+v,O=m+y/P,A=b-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Xn(w,S,a,r);t.arc(e.x,e.y,_,S,b+E)}const i=Xn(D,b,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Xn(D,A,a,r);t.arc(e.x,e.y,v,b+E,A+Math.PI)}const s=(b-v/u+(m+y/u))/2;if(t.arc(a,r,u,b-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Xn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Xn(M,m,a,r);if(t.lineTo(n.x,n.y),x>0){const e=Xn(M,k,a,r);t.arc(e.x,e.y,x,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Kn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u}=l,f="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,f?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let g=e.endAngle;if(o){qn(t,e,i,s,g,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,g),o||(qn(t,e,i,s,g,n),t.stroke())}function Gn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Jn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[x(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[x(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(b*m+e)/++b):(_(),t.lineTo(e,i),u=s,b=0,f=g=i),p=i}_()}function eo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?to:Qn}const io="function"==typeof Path2D;function so(t,e,i,s){io&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Gn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=eo(e);for(const r of n)Gn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class no extends Hs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a)>=O||Z(n,a,r),g=tt(o,h+u,c+u);return f&&g}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){qn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function po(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,b=!s(a),x=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!b&&!x)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),b&&x&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=b?a:M,w=x?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(b&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return x&&u&&w!==r?i.length&&V(i[i.length-1].value,r,mo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):x&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class xo extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const _o=t=>Math.floor(z(t)),yo=(t,e)=>Math.pow(10,_o(t)+e);function vo(t){return 1===t/Math.pow(10,_o(t))}function Mo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function wo(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=_o(e);let o=function(t,e){let i=_o(e-t);for(;Mo(t,e,i)>10;)i++;for(;Mo(t,e,i)<10;)i--;return Math.min(i,_o(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:vo(g),significand:u}),s}class ko extends Js{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===yo(this.min,0)?yo(this.min,-1):yo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(yo(i,-1)),o(yo(s,1)))),i<=0&&n(yo(s,-1)),s<=0&&o(yo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=wo({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function So(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Po(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Oo(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function Ao(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function To(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Lo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));To(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash),o.lineDashOffset=n.dashOffset,o.beginPath(),Lo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Ro={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Io=Object.keys(Ro);function zo(t,e){return t-e}function Fo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Vo(t,e,i,s){const n=Io.length;for(let o=Io.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function Wo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class No extends Js{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Rn._date(t.adapters.date);s.init(e),x(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Fo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Vo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Io.length-1;o>=Io.indexOf(i);o--){const i=Io[o];if(Ro[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Io[i?Io.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Io.indexOf(t)+1,i=Io.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Vo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var jo=Object.freeze({__proto__:null,CategoryScale:class extends Js{static id="category";static defaults={ticks:{callback:po}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:go(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return po.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:xo,LogarithmicScale:ko,RadialLinearScale:Eo,TimeScale:No,TimeSeriesScale:class extends No{static id="timeseries";static defaults=No.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ho(e,this.min),this._tableRange=Ho(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(Ho(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return Ho(this._table,i*this._tableRange+this._minPos,!0)}}});const $o=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Yo=$o.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Uo(t){return $o[t%$o.length]}function Xo(t){return Yo[t%Yo.length]}function qo(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof jn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Uo(e++))),e}(i,e):n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Uo(e),t.backgroundColor=Xo(e),++e}(i,e))}}function Ko(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Go={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n;if(!i.forceOverride&&(Ko(s)||(a=n)&&(a.borderColor||a.backgroundColor)||o&&Ko(o)))return;var a;const r=qo(t);s.forEach(r)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Jo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var Qo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Jo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const b=[],x=e+i-1,_=t[e].x,y=t[x].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&b.push({...t[e],x:p}),s!==u&&s!==i&&b.push({...t[s],x:p})}o>0&&i!==u&&b.push(t[i]),b.push(a),h=e,m=0,f=g=l,c=d=u=o}}return b}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Jo(t)}};function ta(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ea(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function ia(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function sa(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ea(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new no({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function na(t){return t&&!1!==t.fill}function oa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function aa(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function ra(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&da(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;na(i)&&da(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;na(s)&&"beforeDatasetDraw"===i.drawTime&&da(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const ba=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class xa extends Hs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=ba(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=_a(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=ba(o,d),b=this.isHorizontal(),x=this._computeTitleHeight();f=b?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+x,line:0}:{x:this.left+c,y:ft(n,this.top+x+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),b?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+x+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,b?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),b)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=_a(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class va extends Hs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var Ma={id:"title",_element:va,start(t,e,i){!function(t,e){const i=new va({ctx:t.ctx,options:e,chart:t});as.configure(t,i,e),as.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;as.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const wa=new WeakMap;var ka={id:"subtitle",start(t,e,i){const s=new va({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s),wa.set(t,s)},stop(t){as.removeBox(t,wa.get(t)),wa.delete(t)},beforeUpdate(t,e,i){const s=wa.get(t);as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa={average(t){if(!t.length)return!1;let e,i,s=0,n=0,o=0;for(e=0,i=t.length;e-1?t.split("\n"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,"beforeLabel",this,t))),Pa(e.lines,Fa(n,"label",this,t)),Pa(e.after,Da(Fa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,"beforeFooter",this,t),n=Fa(i,"footer",this,t),o=Fa(i,"afterFooter",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Ia(t.callbacks,e);s.push(Fa(i,"labelColor",this,e)),n.push(Fa(i,"labelPointStyle",this,e)),o.push(Fa(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Sa[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Oa(this,i),a=Object.assign({},t,e),r=Ta(this.chart,i,a),l=La(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,b,x,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,x=_+o,y=_-o):(p=d+f,m=p+o,x=_-o,y=_+o),b=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(x=u,_=x-o,p=m-o,b=m+o):(x=u+g,_=x+o,p=m+o,b=m-o),y=x),{x1:p,x2:m,x3:b,y1:x,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let b,x,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ea(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Sa[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Oa(this,t),a=Object.assign({},i,this._size),r=Ta(e,t,a),l=La(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Sa[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ba={id:"tooltip",_element:Va,positioners:Sa,afterInit(t,e,i){i&&(t.tooltip=new Va({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:za},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return An.register(Yn,jo,fo,t),An.helpers={...Wi},An._adapters=Rn,An.Animation=Cs,An.Animations=Os,An.animator=xt,An.controllers=en.controllers.items,An.DatasetController=Ns,An.Element=Hs,An.elements=fo,An.Interaction=Xi,An.layouts=as,An.platforms=Ss,An.Scale=Js,An.Ticks=ae,Object.assign(An,Yn,jo,fo,t,Ss),An.Chart=An,"undefined"!=typeof window&&(window.Chart=An),An})); +//# sourceMappingURL=chart.umd.js.map diff --git a/includes/admin/js/chartjs-adapter-luxon.js b/includes/admin/js/chartjs-adapter-luxon.js new file mode 100644 index 0000000..d212862 --- /dev/null +++ b/includes/admin/js/chartjs-adapter-luxon.js @@ -0,0 +1,7 @@ +/*! + * chartjs-adapter-luxon v1.3.1 + * https://www.chartjs.org + * (c) 2023 chartjs-adapter-luxon Contributors + * Released under the MIT license + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})})); diff --git a/includes/admin/js/chartjs-adapter-luxon.min.js b/includes/admin/js/chartjs-adapter-luxon.min.js new file mode 100644 index 0000000..5f4d1e5 --- /dev/null +++ b/includes/admin/js/chartjs-adapter-luxon.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-adapter-luxon v1.3.1 + * https://www.chartjs.org + * (c) 2023 chartjs-adapter-luxon Contributors + * Released under the MIT license + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})})); \ No newline at end of file diff --git a/includes/admin/js/chartjs-plugin-datalabels.min.js b/includes/admin/js/chartjs-plugin-datalabels.min.js new file mode 100644 index 0000000..77a0d51 --- /dev/null +++ b/includes/admin/js/chartjs-plugin-datalabels.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-plugin-datalabels v2.2.0 + * https://chartjs-plugin-datalabels.netlify.app + * (c) 2017-2022 chartjs-plugin-datalabels contributors + * Released under the MIT license + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js/helpers"),require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js/helpers","chart.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).ChartDataLabels=e(t.Chart.helpers,t.Chart)}(this,(function(t,e){"use strict";var r=function(){if("undefined"!=typeof window){if(window.devicePixelRatio)return window.devicePixelRatio;var t=window.screen;if(t)return(t.deviceXDPI||1)/(t.logicalXDPI||1)}return 1}(),a=function(e){var r,a=[];for(e=[].concat(e);e.length;)"string"==typeof(r=e.pop())?a.unshift.apply(a,r.split("\n")):Array.isArray(r)?e.push.apply(e,r):t.isNullOrUndef(e)||a.unshift(""+r);return a},o=function(t,e,r){var a,o=[].concat(e),n=o.length,i=t.font,l=0;for(t.font=r.string,a=0;ar.right&&(a|=2),er.bottom&&(a|=4),a}function u(t,e){var r,a,o=e.anchor,n=t;return e.clamp&&(n=function(t,e){for(var r,a,o,n=t.x0,i=t.y0,l=t.x1,u=t.y1,d=s(n,i,e),c=s(l,u,e);d|c&&!(d&c);)8&(r=d||c)?(a=n+(l-n)*(e.top-i)/(u-i),o=e.top):4&r?(a=n+(l-n)*(e.bottom-i)/(u-i),o=e.bottom):2&r?(o=i+(u-i)*(e.right-n)/(l-n),a=e.right):1&r&&(o=i+(u-i)*(e.left-n)/(l-n),a=e.left),r===d?d=s(n=a,i=o,e):c=s(l=a,u=o,e);return{x0:n,x1:l,y0:i,y1:u}}(n,e.area)),"start"===o?(r=n.x0,a=n.y0):"end"===o?(r=n.x1,a=n.y1):(r=(n.x0+n.x1)/2,a=(n.y0+n.y1)/2),function(t,e,r,a,o){switch(o){case"center":r=a=0;break;case"bottom":r=0,a=1;break;case"right":r=1,a=0;break;case"left":r=-1,a=0;break;case"top":r=0,a=-1;break;case"start":r=-r,a=-a;break;case"end":break;default:o*=Math.PI/180,r=Math.cos(o),a=Math.sin(o)}return{x:t,y:e,vx:r,vy:a}}(r,a,t.vx,t.vy,e.align)}var d=function(t,e){var r=(t.startAngle+t.endAngle)/2,a=Math.cos(r),o=Math.sin(r),n=t.innerRadius,i=t.outerRadius;return u({x0:t.x+a*n,y0:t.y+o*n,x1:t.x+a*i,y1:t.y+o*i,vx:a,vy:o},e)},c=function(t,e){var r=l(t,e.origin),a=r.x*t.options.radius,o=r.y*t.options.radius;return u({x0:t.x-a,y0:t.y-o,x1:t.x+a,y1:t.y+o,vx:r.x,vy:r.y},e)},h=function(t,e){var r=l(t,e.origin),a=t.x,o=t.y,n=0,i=0;return t.horizontal?(a=Math.min(t.x,t.base),n=Math.abs(t.base-t.x)):(o=Math.min(t.y,t.base),i=Math.abs(t.base-t.y)),u({x0:a,y0:o+i,x1:a+n,y1:o,vx:r.x,vy:r.y},e)},f=function(t,e){var r=l(t,e.origin);return u({x0:t.x,y0:t.y,x1:t.x+(t.width||0),y1:t.y+(t.height||0),vx:r.x,vy:r.y},e)},x=function(t){return Math.round(t*r)/r};function y(t,e){var r=e.chart.getDatasetMeta(e.datasetIndex).vScale;if(!r)return null;if(void 0!==r.xCenter&&void 0!==r.yCenter)return{x:r.xCenter,y:r.yCenter};var a=r.getBasePixel();return t.horizontal?{x:a,y:null}:{x:null,y:a}}function v(t,e,r){var a=r.backgroundColor,o=r.borderColor,n=r.borderWidth;(a||o&&n)&&(t.beginPath(),function(t,e,r,a,o,n){var i=Math.PI/2;if(n){var l=Math.min(n,o/2,a/2),s=e+l,u=r+l,d=e+a-l,c=r+o-l;t.moveTo(e,u),sr.x+r.w+2||t.y>r.y+r.h+2)},intersects:function(t){var e,r,a,o=this._points(),n=t._points(),i=[M(o[0],o[1]),M(o[0],o[3])];for(this._rotation!==t._rotation&&i.push(M(n[0],n[1]),M(n[0],n[3])),e=0;et.getProps([e],!0)[e]}),n=a.geometry(),i=$(l,a.model(),n),o._box.update(i,n,a.rotation()));(function(t,e){var r,a,o,n;for(r=t.length-1;r>=0;--r)for(o=t[r].$layout,a=r-1;a>=0&&o._visible;--a)(n=t[a].$layout)._visible&&o._box.intersects(n._box)&&e(o,n)})(t,(function(t,e){var r=t._hidable,a=e._hidable;r&&a||a?e._visible=!1:r&&(t._visible=!1)}))}(t)},lookup:function(t,e){var r,a;for(r=t.length-1;r>=0;--r)if((a=t[r].$layout)&&a._visible&&a._box.contains(e))return t[r];return null},draw:function(t,e){var r,a,o,n,i,l;for(r=0,a=e.length;re.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[n++]}};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var t=function(e){function t(){return e.apply(this,arguments)||this}return o(t,e),t}(q(Error)),Y=function(t){function e(e){return t.call(this,"Invalid DateTime: "+e.toMessage())||this}return o(e,t),e}(t),P=function(t){function e(e){return t.call(this,"Invalid Interval: "+e.toMessage())||this}return o(e,t),e}(t),H=function(t){function e(e){return t.call(this,"Invalid Duration: "+e.toMessage())||this}return o(e,t),e}(t),w=function(e){function t(){return e.apply(this,arguments)||this}return o(t,e),t}(t),J=function(t){function e(e){return t.call(this,"Invalid unit "+e)||this}return o(e,t),e}(t),u=function(e){function t(){return e.apply(this,arguments)||this}return o(t,e),t}(t),n=function(e){function t(){return e.call(this,"Zone is an abstract class")||this}return o(t,e),t}(t),t="numeric",r="short",a="long",G={year:t,month:t,day:t},$={year:t,month:r,day:t},B={year:t,month:r,day:t,weekday:r},Q={year:t,month:a,day:t},K={year:t,month:a,day:t,weekday:a},X={hour:t,minute:t},ee={hour:t,minute:t,second:t},te={hour:t,minute:t,second:t,timeZoneName:r},ne={hour:t,minute:t,second:t,timeZoneName:a},re={hour:t,minute:t,hourCycle:"h23"},ie={hour:t,minute:t,second:t,hourCycle:"h23"},oe={hour:t,minute:t,second:t,hourCycle:"h23",timeZoneName:r},ae={hour:t,minute:t,second:t,hourCycle:"h23",timeZoneName:a},ue={year:t,month:t,day:t,hour:t,minute:t},se={year:t,month:t,day:t,hour:t,minute:t,second:t},le={year:t,month:r,day:t,hour:t,minute:t},ce={year:t,month:r,day:t,hour:t,minute:t,second:t},fe={year:t,month:r,day:t,weekday:r,hour:t,minute:t},de={year:t,month:a,day:t,hour:t,minute:t,timeZoneName:r},he={year:t,month:a,day:t,hour:t,minute:t,second:t,timeZoneName:r},me={year:t,month:a,day:t,weekday:a,hour:t,minute:t,timeZoneName:a},ye={year:t,month:a,day:t,weekday:a,hour:t,minute:t,second:t,timeZoneName:a},s=function(){function e(){}var t=e.prototype;return t.offsetName=function(e,t){throw new n},t.formatOffset=function(e,t){throw new n},t.offset=function(e){throw new n},t.equals=function(e){throw new n},i(e,[{key:"type",get:function(){throw new n}},{key:"name",get:function(){throw new n}},{key:"ianaName",get:function(){return this.name}},{key:"isUniversal",get:function(){throw new n}},{key:"isValid",get:function(){throw new n}}]),e}(),ve=null,ge=function(e){function t(){return e.apply(this,arguments)||this}o(t,e);var n=t.prototype;return n.offsetName=function(e,t){return gt(e,t.format,t.locale)},n.formatOffset=function(e,t){return bt(this.offset(e),t)},n.offset=function(e){return-new Date(e).getTimezoneOffset()},n.equals=function(e){return"system"===e.type},i(t,[{key:"type",get:function(){return"system"}},{key:"name",get:function(){return(new Intl.DateTimeFormat).resolvedOptions().timeZone}},{key:"isUniversal",get:function(){return!1}},{key:"isValid",get:function(){return!0}}],[{key:"instance",get:function(){return ve=null===ve?new t:ve}}]),t}(s),pe={};var ke={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6};var we={},f=function(n){function r(e){var t=n.call(this)||this;return t.zoneName=e,t.valid=r.isValidZone(e),t}o(r,n),r.create=function(e){return we[e]||(we[e]=new r(e)),we[e]},r.resetCache=function(){we={},pe={}},r.isValidSpecifier=function(e){return this.isValidZone(e)},r.isValidZone=function(e){if(!e)return!1;try{return new Intl.DateTimeFormat("en-US",{timeZone:e}).format(),!0}catch(e){return!1}};var e=r.prototype;return e.offsetName=function(e,t){return gt(e,t.format,t.locale,this.name)},e.formatOffset=function(e,t){return bt(this.offset(e),t)},e.offset=function(e){var t,n,r,i,o,a,u,s,e=new Date(e);return isNaN(e)?NaN:(i=this.name,pe[i]||(pe[i]=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:i,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"})),a=(i=(i=pe[i]).formatToParts?function(e,t){for(var n=e.formatToParts(t),r=[],i=0;iyt(i,t,n)?(r=i+1,a=1):r=i,l({weekYear:r,weekNumber:a,weekday:o},St(e))}function Ke(e,t,n){void 0===n&&(n=1);var r,i=e.weekYear,o=e.weekNumber,a=e.weekday,n=Be(Je(i,1,t=void 0===t?4:t),n),u=M(i),o=7*o+a-n-7+t,a=(o<1?o+=M(r=i-1):uO.twoDigitCutoffYear?1900+e:2e3+e}function gt(e,t,n,r){void 0===r&&(r=null);var e=new Date(e),i={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"},r=(r&&(i.timeZone=r),l({timeZoneName:t},i)),t=new Intl.DateTimeFormat(n,r).formatToParts(e).find(function(e){return"timezonename"===e.type.toLowerCase()});return t?t.value:null}function pt(e,t){e=parseInt(e,10),Number.isNaN(e)&&(e=0),t=parseInt(t,10)||0;return 60*e+(e<0||Object.is(e,-0)?-t:t)}function kt(e){var t=Number(e);if("boolean"==typeof e||""===e||Number.isNaN(t))throw new u("Invalid unit value "+e);return t}function wt(e,t){var n,r,i={};for(n in e)h(e,n)&&null!=(r=e[n])&&(i[t(n)]=kt(r));return i}function bt(e,t){var n=Math.trunc(Math.abs(e/60)),r=Math.trunc(Math.abs(e%60)),i=0<=e?"+":"-";switch(t){case"short":return i+m(n,2)+":"+m(r,2);case"narrow":return i+n+(0e},t.isBefore=function(e){return!!this.isValid&&this.e<=e},t.contains=function(e){return!!this.isValid&&this.s<=e&&this.e>e},t.set=function(e){var e=void 0===e?{}:e,t=e.start,e=e.end;return this.isValid?l.fromDateTimes(t||this.s,e||this.e):this},t.splitAt=function(){var t=this;if(!this.isValid)return[];for(var e=arguments.length,n=new Array(e),r=0;r+this.e?this.e:s;o.push(l.fromDateTimes(a,s)),a=s,u+=1}return o},t.splitBy=function(e){var t=E.fromDurationLike(e);if(!this.isValid||!t.isValid||0===t.as("milliseconds"))return[];for(var n=this.s,r=1,i=[];n+this.e?this.e:o;i.push(l.fromDateTimes(n,o)),n=o,r+=1}return i},t.divideEqually=function(e){return this.isValid?this.splitBy(this.length()/e).slice(0,e):[]},t.overlaps=function(e){return this.e>e.s&&this.s=e.e},t.equals=function(e){return!(!this.isValid||!e.isValid)&&this.s.equals(e.s)&&this.e.equals(e.e)},t.intersection=function(e){var t;return this.isValid?(t=(this.s>e.s?this:e).s,(e=(this.ee.e?this:e).e,l.fromDateTimes(t,e)):this},l.merge=function(e){var e=e.sort(function(e,t){return e.s-t.s}).reduce(function(e,t){var n=e[0],e=e[1];return e?e.overlaps(t)||e.abutsStart(t)?[n,e.union(t)]:[n.concat([e]),t]:[n,t]},[[],null]),t=e[0],e=e[1];return e&&t.push(e),t},l.xor=function(e){for(var t,n=null,r=0,i=[],e=e.map(function(e){return[{time:e.s,type:"s"},{time:e.e,type:"e"}]}),o=R((t=Array.prototype).concat.apply(t,e).sort(function(e,t){return e.time-t.time}));!(a=o()).done;)var a=a.value,n=1===(r+="s"===a.type?1:-1)?a.time:(n&&+n!=+a.time&&i.push(l.fromDateTimes(n,a.time)),null);return l.merge(i)},t.difference=function(){for(var t=this,e=arguments.length,n=new Array(e),r=0;rthis.valueOf())?this:e,r?e:this,t,n),r?e.negate():e):E.invalid("created by diffing an invalid DateTime")},t.diffNow=function(e,t){return void 0===e&&(e="milliseconds"),void 0===t&&(t={}),this.diff(k.now(),e,t)},t.until=function(e){return this.isValid?Mn.fromDateTimes(this,e):this},t.hasSame=function(e,t,n){var r;return!!this.isValid&&(r=e.valueOf(),(e=this.setZone(e.zone,{keepLocalTime:!0})).startOf(t,n)<=r)&&r<=e.endOf(t,n)},t.equals=function(e){return this.isValid&&e.isValid&&this.valueOf()===e.valueOf()&&this.zone.equals(e.zone)&&this.loc.equals(e.loc)},t.toRelative=function(e){var t,n,r,i;return this.isValid?(t=(e=void 0===e?{}:e).base||k.fromObject({},{zone:this.zone}),n=e.padding?thisthis.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset)}},{key:"isInLeapYear",get:function(){return ft(this.year)}},{key:"daysInMonth",get:function(){return dt(this.year,this.month)}},{key:"daysInYear",get:function(){return this.isValid?M(this.year):NaN}},{key:"weeksInWeekYear",get:function(){return this.isValid?yt(this.weekYear):NaN}},{key:"weeksInLocalWeekYear",get:function(){return this.isValid?yt(this.localWeekYear,this.loc.getMinDaysInFirstWeek(),this.loc.getStartOfWeek()):NaN}}],[{key:"DATE_SHORT",get:function(){return G}},{key:"DATE_MED",get:function(){return $}},{key:"DATE_MED_WITH_WEEKDAY",get:function(){return B}},{key:"DATE_FULL",get:function(){return Q}},{key:"DATE_HUGE",get:function(){return K}},{key:"TIME_SIMPLE",get:function(){return X}},{key:"TIME_WITH_SECONDS",get:function(){return ee}},{key:"TIME_WITH_SHORT_OFFSET",get:function(){return te}},{key:"TIME_WITH_LONG_OFFSET",get:function(){return ne}},{key:"TIME_24_SIMPLE",get:function(){return re}},{key:"TIME_24_WITH_SECONDS",get:function(){return ie}},{key:"TIME_24_WITH_SHORT_OFFSET",get:function(){return oe}},{key:"TIME_24_WITH_LONG_OFFSET",get:function(){return ae}},{key:"DATETIME_SHORT",get:function(){return ue}},{key:"DATETIME_SHORT_WITH_SECONDS",get:function(){return se}},{key:"DATETIME_MED",get:function(){return le}},{key:"DATETIME_MED_WITH_SECONDS",get:function(){return ce}},{key:"DATETIME_MED_WITH_WEEKDAY",get:function(){return fe}},{key:"DATETIME_FULL",get:function(){return de}},{key:"DATETIME_FULL_WITH_SECONDS",get:function(){return he}},{key:"DATETIME_HUGE",get:function(){return me}},{key:"DATETIME_HUGE_WITH_SECONDS",get:function(){return ye}}]),k}(Symbol.for("nodejs.util.inspect.custom"));function yr(e){if(W.isDateTime(e))return e;if(e&&e.valueOf&&y(e.valueOf()))return W.fromJSDate(e);if(e&&"object"==typeof e)return W.fromObject(e);throw new u("Unknown datetime argument: "+e+", of type "+typeof e)}return e.DateTime=W,e.Duration=E,e.FixedOffsetZone=d,e.IANAZone=f,e.Info=In,e.Interval=Mn,e.InvalidZone=Le,e.Settings=O,e.SystemZone=ge,e.VERSION="3.4.4",e.Zone=s,Object.defineProperty(e,"__esModule",{value:!0}),e}({}); \ No newline at end of file diff --git a/includes/admin/js/luxon.min.js b/includes/admin/js/luxon.min.js new file mode 100644 index 0000000..766510e --- /dev/null +++ b/includes/admin/js/luxon.min.js @@ -0,0 +1 @@ +var luxon=function(e){"use strict";function t(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[n++]}};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var f=function(e){function t(){return e.apply(this,arguments)||this}return i(t,e),t}(function(e){var t="function"==typeof Map?new Map:void 0;return function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return s(e,arguments,o(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),a(n,e)}(e)}(Error)),d=function(e){function t(t){return e.call(this,"Invalid DateTime: "+t.toMessage())||this}return i(t,e),t}(f),h=function(e){function t(t){return e.call(this,"Invalid Interval: "+t.toMessage())||this}return i(t,e),t}(f),m=function(e){function t(t){return e.call(this,"Invalid Duration: "+t.toMessage())||this}return i(t,e),t}(f),y=function(e){function t(){return e.apply(this,arguments)||this}return i(t,e),t}(f),v=function(e){function t(t){return e.call(this,"Invalid unit "+t)||this}return i(t,e),t}(f),g=function(e){function t(){return e.apply(this,arguments)||this}return i(t,e),t}(f),p=function(e){function t(){return e.call(this,"Zone is an abstract class")||this}return i(t,e),t}(f),k={year:f="numeric",month:f,day:f},w={year:f,month:Dt="short",day:f},b={year:f,month:Dt,day:f,weekday:Dt},S={year:f,month:xt="long",day:f},O={year:f,month:xt,day:f,weekday:xt},T={hour:f,minute:f},N={hour:f,minute:f,second:f},D={hour:f,minute:f,second:f,timeZoneName:Dt},M={hour:f,minute:f,second:f,timeZoneName:xt},I={hour:f,minute:f,hourCycle:"h23"},V={hour:f,minute:f,second:f,hourCycle:"h23"},E={hour:f,minute:f,second:f,hourCycle:"h23",timeZoneName:Dt},x={hour:f,minute:f,second:f,hourCycle:"h23",timeZoneName:xt},C={year:f,month:f,day:f,hour:f,minute:f},F={year:f,month:f,day:f,hour:f,minute:f,second:f},Z={year:f,month:Dt,day:f,hour:f,minute:f},W={year:f,month:Dt,day:f,hour:f,minute:f,second:f},L={year:f,month:Dt,day:f,weekday:Dt,hour:f,minute:f},j={year:f,month:xt,day:f,hour:f,minute:f,timeZoneName:Dt},z={year:f,month:xt,day:f,hour:f,minute:f,second:f,timeZoneName:Dt},A={year:f,month:xt,day:f,weekday:xt,hour:f,minute:f,timeZoneName:xt},q={year:f,month:xt,day:f,weekday:xt,hour:f,minute:f,second:f,timeZoneName:xt},_=function(){function e(){}var t=e.prototype;return t.offsetName=function(e,t){throw new p},t.formatOffset=function(e,t){throw new p},t.offset=function(e){throw new p},t.equals=function(e){throw new p},n(e,[{key:"type",get:function(){throw new p}},{key:"name",get:function(){throw new p}},{key:"ianaName",get:function(){return this.name}},{key:"isUniversal",get:function(){throw new p}},{key:"isValid",get:function(){throw new p}}]),e}(),U=null,R=function(e){function t(){return e.apply(this,arguments)||this}i(t,e);var r=t.prototype;return r.offsetName=function(e,t){return it(e,t.format,t.locale)},r.formatOffset=function(e,t){return ut(this.offset(e),t)},r.offset=function(e){return-new Date(e).getTimezoneOffset()},r.equals=function(e){return"system"===e.type},n(t,[{key:"type",get:function(){return"system"}},{key:"name",get:function(){return(new Intl.DateTimeFormat).resolvedOptions().timeZone}},{key:"isUniversal",get:function(){return!1}},{key:"isValid",get:function(){return!0}}],[{key:"instance",get:function(){return U=null===U?new t:U}}]),t}(_),Y={},P={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6},H={},J=function(e){function t(n){var r=e.call(this)||this;return r.zoneName=n,r.valid=t.isValidZone(n),r}i(t,e),t.create=function(e){return H[e]||(H[e]=new t(e)),H[e]},t.resetCache=function(){H={},Y={}},t.isValidSpecifier=function(e){return this.isValidZone(e)},t.isValidZone=function(e){if(!e)return!1;try{return new Intl.DateTimeFormat("en-US",{timeZone:e}).format(),!0}catch(e){return!1}};var r=t.prototype;return r.offsetName=function(e,t){return it(e,t.format,t.locale,this.name)},r.formatOffset=function(e,t){return ut(this.offset(e),t)},r.offset=function(e){var t,n,r,i,o,a,s,u;e=new Date(e);return isNaN(e)?NaN:(i=this.name,Y[i]||(Y[i]=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:i,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"})),a=(i=(i=Y[i]).formatToParts?function(e,t){for(var n=e.formatToParts(t),r=[],i=0;int(a,t,n)?(i=a+1,o=1):i=a,r({weekYear:i,weekNumber:o,weekday:u},lt(e))}function xe(e,t,n){void 0===n&&(n=1);var i,o=e.weekYear,a=e.weekNumber,s=e.weekday,u=(n=Ve(De(o,1,t=void 0===t?4:t),n),Ke(o));(a=7*a+s-n-7+t)<1?a+=Ke(i=o-1):ube.twoDigitCutoffYear?1900+e:2e3+e}function it(e,t,n,i){void 0===i&&(i=null);e=new Date(e);var o={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"};i&&(o.timeZone=i),i=r({timeZoneName:t},o),t=new Intl.DateTimeFormat(n,i).formatToParts(e).find((function(e){return"timezonename"===e.type.toLowerCase()}));return t?t.value:null}function ot(e,t){return e=parseInt(e,10),Number.isNaN(e)&&(e=0),t=parseInt(t,10)||0,60*e+(e<0||Object.is(e,-0)?-t:t)}function at(e){var t=Number(e);if("boolean"==typeof e||""===e||Number.isNaN(t))throw new g("Invalid unit value "+e);return t}function st(e,t){var n,r,i={};for(n in e)Re(e,n)&&null!=(r=e[n])&&(i[t(n)]=at(r));return i}function ut(e,t){var n=Math.trunc(Math.abs(e/60)),r=Math.trunc(Math.abs(e%60)),i=0<=e?"+":"-";switch(t){case"short":return i+He(n,2)+":"+He(r,2);case"narrow":return i+n+(0e},r.isBefore=function(e){return!!this.isValid&&this.e<=e},r.contains=function(e){return!!this.isValid&&this.s<=e&&this.e>e},r.set=function(e){var n=(e=void 0===e?{}:e).start;e=e.end;return this.isValid?t.fromDateTimes(n||this.s,e||this.e):this},r.splitAt=function(){var e=this;if(!this.isValid)return[];for(var n=arguments.length,r=new Array(n),i=0;i+this.e?this.e:l;a.push(t.fromDateTimes(s,l)),s=l,u+=1}return a},r.splitBy=function(e){var n=kn.fromDurationLike(e);if(!this.isValid||!n.isValid||0===n.as("milliseconds"))return[];for(var r=this.s,i=1,o=[];r+this.e?this.e:a;o.push(t.fromDateTimes(r,a)),r=a,i+=1}return o},r.divideEqually=function(e){return this.isValid?this.splitBy(this.length()/e).slice(0,e):[]},r.overlaps=function(e){return this.e>e.s&&this.s=e.e},r.equals=function(e){return!(!this.isValid||!e.isValid)&&this.s.equals(e.s)&&this.e.equals(e.e)},r.intersection=function(e){var n;return this.isValid?(n=(this.s>e.s?this:e).s,(e=(this.ee.e?this:e).e,t.fromDateTimes(n,e)):this},t.merge=function(e){e=e.sort((function(e,t){return e.s-t.s})).reduce((function(e,t){var n=e[0];return(e=e[1])?e.overlaps(t)||e.abutsStart(t)?[n,e.union(t)]:[n.concat([e]),t]:[n,t]}),[[],null]);var t=e[0];return(e=e[1])&&t.push(e),t},t.xor=function(e){for(var n,r=null,i=0,o=[],a=(e=e.map((function(e){return[{time:e.s,type:"s"},{time:e.e,type:"e"}]})),c((n=Array.prototype).concat.apply(n,e).sort((function(e,t){return e.time-t.time}))));!(s=a()).done;){var s=s.value;r=1===(i+="s"===s.type?1:-1)?s.time:(r&&+r!=+s.time&&o.push(t.fromDateTimes(r,s.time)),null)}return t.merge(o)},r.difference=function(){for(var e=this,n=arguments.length,r=new Array(n),i=0;ithis.valueOf())?this:e,i?e:this,t,n),i?e.negate():e):kn.invalid("created by diffing an invalid DateTime")},i.diffNow=function(e,n){return void 0===e&&(e="milliseconds"),void 0===n&&(n={}),this.diff(t.now(),e,n)},i.until=function(e){return this.isValid?bn.fromDateTimes(this,e):this},i.hasSame=function(e,t,n){var r;return!!this.isValid&&(r=e.valueOf(),(e=this.setZone(e.zone,{keepLocalTime:!0})).startOf(t,n)<=r)&&r<=e.endOf(t,n)},i.equals=function(e){return this.isValid&&e.isValid&&this.valueOf()===e.valueOf()&&this.zone.equals(e.zone)&&this.loc.equals(e.loc)},i.toRelative=function(e){var n,i,o,a;return this.isValid?(n=(e=void 0===e?{}:e).base||t.fromObject({},{zone:this.zone}),i=e.padding?thisthis.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset)}},{key:"isInLeapYear",get:function(){return Qe(this.year)}},{key:"daysInMonth",get:function(){return Xe(this.year,this.month)}},{key:"daysInYear",get:function(){return this.isValid?Ke(this.year):NaN}},{key:"weeksInWeekYear",get:function(){return this.isValid?nt(this.weekYear):NaN}},{key:"weeksInLocalWeekYear",get:function(){return this.isValid?nt(this.localWeekYear,this.loc.getMinDaysInFirstWeek(),this.loc.getStartOfWeek()):NaN}}],[{key:"DATE_SHORT",get:function(){return k}},{key:"DATE_MED",get:function(){return w}},{key:"DATE_MED_WITH_WEEKDAY",get:function(){return b}},{key:"DATE_FULL",get:function(){return S}},{key:"DATE_HUGE",get:function(){return O}},{key:"TIME_SIMPLE",get:function(){return T}},{key:"TIME_WITH_SECONDS",get:function(){return N}},{key:"TIME_WITH_SHORT_OFFSET",get:function(){return D}},{key:"TIME_WITH_LONG_OFFSET",get:function(){return M}},{key:"TIME_24_SIMPLE",get:function(){return I}},{key:"TIME_24_WITH_SECONDS",get:function(){return V}},{key:"TIME_24_WITH_SHORT_OFFSET",get:function(){return E}},{key:"TIME_24_WITH_LONG_OFFSET",get:function(){return x}},{key:"DATETIME_SHORT",get:function(){return C}},{key:"DATETIME_SHORT_WITH_SECONDS",get:function(){return F}},{key:"DATETIME_MED",get:function(){return Z}},{key:"DATETIME_MED_WITH_SECONDS",get:function(){return W}},{key:"DATETIME_MED_WITH_WEEKDAY",get:function(){return L}},{key:"DATETIME_FULL",get:function(){return j}},{key:"DATETIME_FULL_WITH_SECONDS",get:function(){return z}},{key:"DATETIME_HUGE",get:function(){return A}},{key:"DATETIME_HUGE_WITH_SECONDS",get:function(){return q}}]),t}(Symbol.for("nodejs.util.inspect.custom"));function fr(e){if(cr.isDateTime(e))return e;if(e&&e.valueOf&&ze(e.valueOf()))return cr.fromJSDate(e);if(e&&"object"==typeof e)return cr.fromObject(e);throw new g("Unknown datetime argument: "+e+", of type "+typeof e)}return e.DateTime=cr,e.Duration=kn,e.FixedOffsetZone=ce,e.IANAZone=J,e.Info=Sn,e.Interval=bn,e.InvalidZone=fe,e.Settings=be,e.SystemZone=R,e.VERSION="3.4.4",e.Zone=_,Object.defineProperty(e,"__esModule",{value:!0}),e}({}); \ No newline at end of file diff --git a/includes/admin/save-settings.php b/includes/admin/save-settings.php deleted file mode 100644 index 89cbe26..0000000 --- a/includes/admin/save-settings.php +++ /dev/null @@ -1,354 +0,0 @@ - $type ) { - - /** - * Skip settings that are not really settings. - * - * @since 2.2.0 - * @param array $non_setting_types Array of types which are not settings. - */ - $non_setting_types = apply_filters( 'bsearch_non_setting_types', array( 'header', 'descriptive_text' ) ); - - if ( in_array( $type, $non_setting_types, true ) ) { - continue; - } - - if ( array_key_exists( $key, $output ) ) { - - /** - * Field type filter. - * - * @since 2.2.0 - * @param array $output[$key] Setting value. - * @param array $key Setting key. - */ - $output[ $key ] = apply_filters( 'bsearch_settings_sanitize_' . $type, $output[ $key ], $key ); - } - - /** - * Field type filter for a specific key. - * - * @since 2.2.0 - * @param array $output[$key] Setting value. - * @param array $key Setting key. - */ - $output[ $key ] = apply_filters( 'bsearch_settings_sanitize' . $key, $output[ $key ], $key ); - - // Delete any key that is not present when we submit the input array. - if ( ! isset( $input[ $key ] ) ) { - unset( $output[ $key ] ); - } - } - - // Delete any settings that are no longer part of our registered settings. - if ( array_key_exists( $key, $output ) && ! array_key_exists( $key, $settings_types ) ) { - unset( $output[ $key ] ); - } - - add_settings_error( 'bsearch-notices', '', __( 'Settings updated.', 'better-search' ), 'updated' ); - - /** - * Filter the settings array before it is returned. - * - * @since 2.2.0 - * @param array $output Settings array. - * @param array $input Input settings array. - */ - return apply_filters( 'bsearch_settings_sanitize', $output, $input ); -} - - -/** - * Sanitize text fields - * - * @since 2.2.0 - * - * @param string $value The field value. - * @return string Sanitized value - */ -function bsearch_sanitize_text_field( $value ) { - return bsearch_sanitize_textarea_field( $value ); -} -add_filter( 'bsearch_settings_sanitize_text', 'bsearch_sanitize_text_field' ); - - -/** - * Sanitize number fields - * - * @since 2.2.0 - * - * @param string $value The field value. - * @return string Sanitized number. - */ -function bsearch_sanitize_number_field( $value ) { - return filter_var( $value, FILTER_SANITIZE_NUMBER_INT ); -} -add_filter( 'bsearch_settings_sanitize_number', 'bsearch_sanitize_number_field' ); - - -/** - * Sanitize CSV fields - * - * @since 2.2.0 - * - * @param string $value The field value. - * @return string Comma separated list. - */ -function bsearch_sanitize_csv_field( $value ) { - - return implode( ',', array_map( 'trim', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) ); -} -add_filter( 'bsearch_settings_sanitize_csv', 'bsearch_sanitize_csv_field' ); - - -/** - * Sanitize CSV fields which hold numbers e.g. IDs - * - * @since 2.2.0 - * - * @param string $value The field value. - * @return string Comma separated list of numbers. - */ -function bsearch_sanitize_numbercsv_field( $value ) { - - return implode( ',', array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) ) ); -} -add_filter( 'bsearch_settings_sanitize_numbercsv', 'bsearch_sanitize_numbercsv_field' ); - - -/** - * Sanitize textarea fields - * - * @since 2.2.0 - * - * @param string $value The field value. - * @return string Sanitized value - */ -function bsearch_sanitize_textarea_field( $value ) { - - global $allowedposttags; - - // We need more tags to allow for script and style. - $moretags = array( - 'script' => array( - 'type' => true, - 'src' => true, - 'async' => true, - 'defer' => true, - 'charset' => true, - 'lang' => true, - ), - 'style' => array( - 'type' => true, - 'media' => true, - 'scoped' => true, - 'lang' => true, - ), - 'link' => array( - 'rel' => true, - 'type' => true, - 'href' => true, - 'media' => true, - 'sizes' => true, - 'hreflang' => true, - ), - ); - - $allowedtags = array_merge( $allowedposttags, $moretags ); - - /** - * Filter allowed tags allowed when sanitizing text and textarea fields. - * - * @since 2.2.0 - * - * @param array $allowedtags Allowed tags array. - * @param array $value The field value. - */ - $allowedtags = apply_filters( 'bsearch_sanitize_allowed_tags', $allowedtags, $value ); - - return wp_kses( wp_unslash( $value ), $allowedtags ); -} -add_filter( 'bsearch_settings_sanitize_textarea', 'bsearch_sanitize_textarea_field' ); - - -/** - * Sanitize checkbox fields - * - * @since 2.2.0 - * - * @param string $value The field value. - * @return int 0 or 1 if checkbox is false or true. - */ -function bsearch_sanitize_checkbox_field( $value ) { - - $value = ( -1 === (int) $value ) ? 0 : 1; - - return $value; -} -add_filter( 'bsearch_settings_sanitize_checkbox', 'bsearch_sanitize_checkbox_field' ); - - -/** - * Sanitize post_types fields - * - * @since 2.2.0 - * - * @param string $value The field value. - * @return string Comma separated list of post types. - */ -function bsearch_sanitize_posttypes_field( $value ) { - - $post_types = is_array( $value ) ? array_map( 'sanitize_text_field', wp_unslash( $value ) ) : array( 'post', 'page' ); - - return implode( ',', $post_types ); -} -add_filter( 'bsearch_settings_sanitize_posttypes', 'bsearch_sanitize_posttypes_field' ); - - -/** - * Sanitize color fields - * - * @since 2.5.0 - * - * @param string $value The field value. - * @return string Hexadecimal colour value. - */ -function bsearch_sanitize_color_field( $value ) { - - $color = str_replace( '#', '', $value ); - if ( strlen( $color ) === 3 ) { - $color = $color . $color; - } - - if ( strlen( $color ) > 6 ) { - $color = substr( $color, 0, 6 ); - } - - if ( preg_match( '/^[a-f0-9]{6}$/i', $color ) ) { - $color = '#' . $color; - } else { - $color = '#000000'; - } - return $color; -} -add_filter( 'bsearch_settings_sanitize_color', 'bsearch_sanitize_color_field' ); - - -/** - * Sanitize exclude_cat_slugs to save a new entry of exclude_categories - * - * @since 2.2.0 - * - * @param array $settings Settings array. - * @return array Sanitizied settings array. - */ -function bsearch_sanitize_exclude_cat( $settings ) { - - if ( isset( $settings['exclude_cat_slugs'] ) ) { - - $exclude_cat_slugs = array_unique( str_getcsv( $settings['exclude_cat_slugs'] ) ); - - foreach ( $exclude_cat_slugs as $cat_name ) { - $cat = get_term_by( 'name', $cat_name, 'category' ); - if ( isset( $cat->term_taxonomy_id ) ) { - $exclude_categories[] = $cat->term_taxonomy_id; - $exclude_categories_slugs[] = $cat->name; - } - } - $settings['exclude_categories'] = isset( $exclude_categories ) ? join( ',', $exclude_categories ) : ''; - $settings['exclude_cat_slugs'] = isset( $exclude_categories_slugs ) ? bsearch_str_putcsv( $exclude_categories_slugs ) : ''; - - } - - return $settings; -} -add_filter( 'bsearch_settings_sanitize', 'bsearch_sanitize_exclude_cat' ); - - -/** - * Delete cache when saving settings. - * - * @since 2.2.0 - * - * @param array $settings Settings array. - * @return array Sanitizied settings array. - */ -function bsearch_sanitize_cache( $settings ) { - - // Delete the cache. - bsearch_cache_delete(); - - return $settings; -} -add_filter( 'bsearch_settings_sanitize', 'bsearch_sanitize_cache' ); diff --git a/includes/admin/settings-page.php b/includes/admin/settings-page.php deleted file mode 100644 index a217d46..0000000 --- a/includes/admin/settings-page.php +++ /dev/null @@ -1,679 +0,0 @@ - -
-

- - - -
-
-
- - - -
- - - - $tab_name ) : ?> - -
- - -
-

- "return confirm('{$confirm}');", - ) - ); - ?> -

-
- - - -
- -
- -
- -
- -
- -
-
-
-
- -
- - __( 'General', 'better-search' ), - 'search' => __( 'Search', 'better-search' ), - 'heatmap' => __( 'Heatmap', 'better-search' ), - 'styles' => __( 'Styles', 'better-search' ), - ); - - /** - * Filter the array containing the settings' sections. - * - * @since 2.2.0 - * - * @param array $bsearch_settings_sections Settings array - */ - return apply_filters( 'bsearch_settings_sections', $bsearch_settings_sections ); -} - - -/** - * Miscellaneous callback funcion - * - * @since 2.2.0 - * - * @param array $args Arguments passed by the setting. - * @return void - */ -function bsearch_missing_callback( $args ) { - /* translators: %s: Setting ID. */ - printf( esc_html__( 'The callback function used for the %s setting is missing.', 'better-search' ), esc_html( $args['id'] ) ); -} - - -/** - * Header Callback - * - * Renders the header. - * - * @since 2.2.0 - * - * @param array $args Arguments passed by the setting. - * @return void - */ -function bsearch_header_callback( $args ) { - - $html = '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** - * After Settings Output filter - * - * @since 2.2.0 - * @param string $html HTML string. - * @param array $args Arguments array. - */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Display text fields. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_text_callback( $args ) { - - // First, we read the options collection. - global $bsearch_settings; - - if ( isset( $bsearch_settings[ $args['id'] ] ) ) { - $value = $bsearch_settings[ $args['id'] ]; - } else { - $value = isset( $args['options'] ) ? $args['options'] : ''; - } - - $size = sanitize_html_class( ( isset( $args['size'] ) && ! is_null( $args['size'] ) ) ? $args['size'] : 'regular' ); - - $class = sanitize_html_class( $args['field_class'] ); - - $disabled = ! empty( $args['disabled'] ) ? ' disabled="disabled"' : ''; - $readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : ''; - - $attributes = $disabled . $readonly; - - foreach ( (array) $args['field_attributes'] as $attribute => $val ) { - $attributes .= sprintf( ' %1$s="%2$s"', $attribute, esc_attr( $val ) ); - } - - $html = sprintf( '', sanitize_key( $args['id'] ), $class . ' ' . $size . '-text', esc_attr( stripslashes( $value ) ), $attributes ); - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Display csv fields. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_csv_callback( $args ) { - - bsearch_text_callback( $args ); -} - - -/** - * Display CSV fields of numbers. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_numbercsv_callback( $args ) { - - bsearch_csv_callback( $args ); -} - - -/** - * Display color fields. - * - * @since 2.5.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_color_callback( $args ) { - - bsearch_text_callback( $args ); -} - - -/** - * Display textarea. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_textarea_callback( $args ) { - - // First, we read the options collection. - global $bsearch_settings; - - if ( isset( $bsearch_settings[ $args['id'] ] ) ) { - $value = $bsearch_settings[ $args['id'] ]; - } else { - $value = isset( $args['options'] ) ? $args['options'] : ''; - } - - $class = sanitize_html_class( $args['field_class'] ); - - $html = sprintf( '', sanitize_key( $args['id'] ), esc_textarea( stripslashes( $value ) ), 'large-text ' . $class ); - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Display CSS fields. - * - * @since 2.5.1 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_css_callback( $args ) { - - bsearch_textarea_callback( $args ); -} - - -/** - * Display checboxes. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_checkbox_callback( $args ) { - - // First, we read the options collection. - global $bsearch_settings; - - $checked = isset( $bsearch_settings[ $args['id'] ] ) ? checked( 1, $bsearch_settings[ $args['id'] ], false ) : checked( 1, bsearch_get_option( $args['id'] ), false ); - $default = isset( $args['options'] ) ? $args['options'] : ''; - $set = isset( $bsearch_settings[ $args['id'] ] ) ? $bsearch_settings[ $args['id'] ] : bsearch_get_option( $args['id'] ); - - $html = sprintf( '', sanitize_key( $args['id'] ) ); - $html .= sprintf( '', sanitize_key( $args['id'] ), $checked ); - $html .= ( $set <> $default ) ? ' ' . esc_html__( 'Modified from default setting', 'better-search' ) . '' : ''; - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Multicheck Callback - * - * Renders multiple checkboxes. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_multicheck_callback( $args ) { - global $bsearch_settings; - $html = ''; - - if ( ! empty( $args['options'] ) ) { - $html .= sprintf( '', $args['id'] ); - - foreach ( $args['options'] as $key => $option ) { - if ( isset( $bsearch_settings[ $args['id'] ][ $key ] ) ) { - $enabled = $key; - } else { - $enabled = null; - } - - $html .= sprintf( ' ', sanitize_key( $args['id'] ), sanitize_key( $key ), esc_attr( $key ), checked( $key, $enabled, false ) ); - $html .= sprintf( '
', sanitize_key( $args['id'] ), sanitize_key( $key ), $option ); - } - - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - } - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Radio Callback - * - * Renders radio boxes. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_radio_callback( $args ) { - global $bsearch_settings; - $html = ''; - - foreach ( $args['options'] as $key => $option ) { - $checked = false; - - if ( isset( $bsearch_settings[ $args['id'] ] ) && $bsearch_settings[ $args['id'] ] === $key ) { - $checked = true; - } elseif ( isset( $args['default'] ) && $args['default'] === $key && ! isset( $bsearch_settings[ $args['id'] ] ) ) { - $checked = true; - } - - $html .= sprintf( ' ', sanitize_key( $args['id'] ), $key, checked( true, $checked, false ) ); - $html .= sprintf( '
', sanitize_key( $args['id'] ), $key, $option ); - } - - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Radio callback with description. - * - * Renders radio boxes with each item having it separate description. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_radiodesc_callback( $args ) { - global $bsearch_settings; - $html = ''; - - foreach ( $args['options'] as $option ) { - $checked = false; - - if ( isset( $bsearch_settings[ $args['id'] ] ) && $bsearch_settings[ $args['id'] ] === $option['id'] ) { - $checked = true; - } elseif ( isset( $args['default'] ) && $args['default'] === $option['id'] && ! isset( $bsearch_settings[ $args['id'] ] ) ) { - $checked = true; - } - - $html .= sprintf( ' ', sanitize_key( $args['id'] ), $option['id'], checked( true, $checked, false ) ); - $html .= sprintf( '', sanitize_key( $args['id'] ), $option['id'], $option['name'] ); - $html .= ': ' . wp_kses_post( $option['description'] ) . '
'; - } - - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Callback for thumbnail sizes - * - * Renders list of radio boxes with various thumbnail sizes. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_thumbsizes_callback( $args ) { - global $bsearch_settings; - $html = ''; - - if ( ! isset( $args['options']['bsearch_thumbnail'] ) ) { - $args['options']['bsearch_thumbnail'] = array( - 'name' => 'bsearch_thumbnail', - 'width' => bsearch_get_option( 'thumb_width', 150 ), - 'height' => bsearch_get_option( 'thumb_height', 150 ), - 'crop' => bsearch_get_option( 'thumb_crop', true ), - ); - } - - foreach ( $args['options'] as $option ) { - $checked = false; - - if ( isset( $bsearch_settings[ $args['id'] ] ) && $bsearch_settings[ $args['id'] ] === $option['name'] ) { - $checked = true; - } elseif ( isset( $args['default'] ) && $args['default'] === $option['name'] && ! isset( $bsearch_settings[ $args['id'] ] ) ) { - $checked = true; - } - - $html .= sprintf( ' ', sanitize_key( $args['id'] ), $option['name'], checked( true, $checked, false ) ); - $html .= sprintf( '
', sanitize_key( $args['id'] ), $option['name'], $option['name'] . ' (' . $option['width'] . 'x' . $option['height'] . ')' ); - } - - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Number Callback - * - * Renders number fields. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_number_callback( $args ) { - global $bsearch_settings; - - if ( isset( $bsearch_settings[ $args['id'] ] ) ) { - $value = $bsearch_settings[ $args['id'] ]; - } else { - $value = isset( $args['options'] ) ? $args['options'] : ''; - } - - $max = isset( $args['max'] ) ? $args['max'] : 999999; - $min = isset( $args['min'] ) ? $args['min'] : 0; - $step = isset( $args['step'] ) ? $args['step'] : 1; - - $size = ( isset( $args['size'] ) && ! is_null( $args['size'] ) ) ? $args['size'] : 'regular'; - - $html = sprintf( '', esc_attr( $step ), esc_attr( $max ), esc_attr( $min ), sanitize_html_class( $size ) . '-text', sanitize_key( $args['id'] ), esc_attr( stripslashes( $value ) ) ); - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Select Callback - * - * Renders select fields. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_select_callback( $args ) { - global $bsearch_settings; - - if ( isset( $bsearch_settings[ $args['id'] ] ) ) { - $value = $bsearch_settings[ $args['id'] ]; - } else { - $value = isset( $args['default'] ) ? $args['default'] : ''; - } - - if ( isset( $args['chosen'] ) ) { - $chosen = 'class="bsearch-chosen"'; - } else { - $chosen = ''; - } - - $html = sprintf( ''; - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Descriptive text callback. - * - * Renders descriptive text onto the settings field. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_descriptive_text_callback( $args ) { - $html = '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Display csv fields. - * - * @since 2.2.0 - * - * @param array $args Array of arguments. - * @return void - */ -function bsearch_posttypes_callback( $args ) { - - global $bsearch_settings; - $html = ''; - - if ( isset( $bsearch_settings[ $args['id'] ] ) ) { - $options = $bsearch_settings[ $args['id'] ]; - } else { - $options = isset( $args['options'] ) ? $args['options'] : ''; - } - - // If post_types is empty or contains a query string then use parse_str else consider it comma-separated. - if ( is_array( $options ) ) { - $post_types = $options; - } elseif ( ! is_array( $options ) && false === strpos( $options, '=' ) ) { - $post_types = explode( ',', $options ); - } else { - parse_str( $options, $post_types ); - } - - $wp_post_types = get_post_types( - array( - 'public' => true, - ) - ); - $posts_types_inc = array_intersect( $wp_post_types, $post_types ); - - foreach ( $wp_post_types as $wp_post_type ) { - - $html .= sprintf( ' ', sanitize_key( $args['id'] ), esc_attr( $wp_post_type ), checked( true, in_array( $wp_post_type, $posts_types_inc, true ), false ) ); - $html .= sprintf( '
', sanitize_key( $args['id'] ), $wp_post_type ); - - } - - $html .= '

' . wp_kses_post( $args['desc'] ) . '

'; - - /** This filter has been defined in settings-page.php */ - echo apply_filters( 'bsearch_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - - -/** - * Function to add an action to search for tags using Ajax. - * - * @since 2.1.0 - * - * @return void - */ -function bsearch_tags_search() { - - if ( ! isset( $_REQUEST['tax'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - wp_die( 0 ); - } - - $taxonomy = sanitize_key( $_REQUEST['tax'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $tax = get_taxonomy( $taxonomy ); - if ( ! $tax ) { - wp_die( 0 ); - } - - if ( ! current_user_can( $tax->cap->assign_terms ) ) { - wp_die( -1 ); - } - - $s = isset( $_REQUEST['q'] ) ? wp_unslash( $_REQUEST['q'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended - - $comma = _x( ',', 'tag delimiter' ); - if ( ',' !== $comma ) { - $s = str_replace( $comma, ',', $s ); - } - if ( false !== strpos( $s, ',' ) ) { - $s = explode( ',', $s ); - $s = $s[ count( $s ) - 1 ]; - } - $s = trim( $s ); - - /** This filter has been defined in /wp-admin/includes/ajax-actions.php */ - $term_search_min_chars = (int) apply_filters( 'term_search_min_chars', 2, $tax, $s ); - - /* - * Require $term_search_min_chars chars for matching (default: 2) - * ensure it's a non-negative, non-zero integer. - */ - if ( ( 0 === $term_search_min_chars ) || ( strlen( $s ) < $term_search_min_chars ) ) { - wp_die(); - } - - $results = get_terms( - array( - 'taxonomy' => $taxonomy, - 'name__like' => $s, - 'fields' => 'names', - 'hide_empty' => false, - ) - ); - - echo wp_json_encode( $results ); - wp_die(); -} -add_action( 'wp_ajax_nopriv_bsearch_tag_search', 'bsearch_tags_search' ); -add_action( 'wp_ajax_bsearch_tag_search', 'bsearch_tags_search' ); diff --git a/includes/admin/settings/class-metabox-api.php b/includes/admin/settings/class-metabox-api.php new file mode 100644 index 0000000..bb26dea --- /dev/null +++ b/includes/admin/settings/class-metabox-api.php @@ -0,0 +1,374 @@ + '', + 'prefix' => '', + 'post_type' => '', + 'title' => '', + 'registered_settings' => array(), + 'checkbox_modified_text' => '', + ); + + $args = wp_parse_args( $args, $defaults ); + + foreach ( $args as $name => $value ) { + $this->$name = $value; + } + + add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) ); + add_action( "save_post_{$this->post_type}", array( $this, 'save' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); + } + + /** + * Function to add the metabox. + */ + public function add_meta_boxes() { + add_meta_box( + $this->prefix . '_metabox_id', + $this->title, + array( $this, 'html' ), + $this->post_type, + 'advanced', + 'high' + ); + } + + /** + * Enqueue scripts and styles. + * + * @param string $hook The current admin page. + */ + public function admin_enqueue_scripts( $hook ) { + if ( in_array( $hook, array( 'post.php', 'post-new.php' ), true ) || get_current_screen()->post_type === $this->post_type ) { + self::enqueue_scripts_styles(); + } + } + + /** + * Enqueues all scripts, styles, settings, and templates necessary to use the Settings API. + */ + public static function enqueue_scripts_styles() { + + $minimize = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; + + wp_enqueue_style( 'wp-color-picker' ); + + wp_enqueue_media(); + wp_enqueue_script( 'wp-color-picker' ); + wp_enqueue_script( 'jquery' ); + wp_enqueue_script( 'jquery-ui-autocomplete' ); + wp_enqueue_script( 'jquery-ui-tabs' ); + + wp_enqueue_code_editor( + array( + 'type' => 'text/html', + 'codemirror' => array( + 'indentUnit' => 2, + 'tabSize' => 2, + ), + ) + ); + + wp_enqueue_script( + 'wz-admin-js', + plugins_url( 'js/admin-scripts' . $minimize . '.js', __FILE__ ), + array( 'jquery' ), + self::VERSION, + true + ); + wp_enqueue_script( + 'wz-codemirror-js', + plugins_url( 'js/apply-codemirror' . $minimize . '.js', __FILE__ ), + array( 'jquery' ), + self::VERSION, + true + ); + wp_enqueue_script( + 'wz-taxonomy-suggest-js', + plugins_url( 'js/taxonomy-suggest' . $minimize . '.js', __FILE__ ), + array( 'jquery' ), + self::VERSION, + true + ); + wp_enqueue_script( + 'wz-media-selector-js', + plugins_url( 'js/media-selector' . $minimize . '.js', __FILE__ ), + array( 'jquery' ), + self::VERSION, + true + ); + } + + /** + * Function to save the metabox. + * + * @param int|string $post_id Post ID. + */ + public function save( $post_id ) { + + $post_meta = array(); + + // Bail if we're doing an auto save. + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + + // If our nonce isn't there, or we can't verify it, bail. + if ( ! isset( $_POST[ $this->prefix . '_meta_box_nonce' ] ) || ! wp_verify_nonce( sanitize_key( $_POST[ $this->prefix . '_meta_box_nonce' ] ), $this->prefix . '_meta_box' ) ) { + return; + } + + // If our current user can't edit this post, bail. + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return; + } + + if ( empty( $_POST[ $this->settings_key ] ) ) { + return; + } + + $settings_sanitize = new Settings_Sanitize(); + + $posted = $_POST[ $this->settings_key ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash + + foreach ( $this->registered_settings as $setting ) { + $id = $setting['id']; + $type = isset( $setting['type'] ) ? $setting['type'] : 'text'; + + /** + * Skip settings that are not really settings. + * + * @param array $non_setting_types Array of types which are not settings. + */ + $non_setting_types = apply_filters( $this->prefix . '_metabox_non_setting_types', array( 'header', 'descriptive_text' ) ); + + if ( in_array( $type, $non_setting_types, true ) ) { + continue; + } + + if ( isset( $posted[ $id ] ) ) { + $value = $posted[ $id ]; + $sanitize_callback = is_callable( array( $settings_sanitize, "sanitize_{$type}_field" ) ) ? array( $settings_sanitize, "sanitize_{$type}_field" ) : array( $settings_sanitize, 'sanitize_missing' ); + $post_meta[ $id ] = call_user_func( $sanitize_callback, $value ); + } + } + + // Run the array through a generic function that allows access to all of the settings. + $post_meta = call_user_func( array( $this, 'sanitize_post_meta' ), $post_meta ); + + /** + * Filter the post meta array which contains post-specific settings. + * + * @param array $post_meta Array of ATA metabox settings. + * @param int $post_id Post ID + */ + $post_meta = apply_filters( "{$this->prefix}_meta_key", $post_meta, $post_id ); + + // Now loop through the settings array and either save or delete the meta key. + foreach ( $this->registered_settings as $setting ) { + if ( empty( $post_meta[ $setting['id'] ] ) ) { + delete_post_meta( $post_id, "_{$this->prefix}_{$setting['id']}" ); + } + } + + foreach ( $post_meta as $setting => $value ) { + if ( empty( $post_meta[ $setting ] ) ) { + delete_post_meta( $post_id, "_{$this->prefix}_$setting" ); + } else { + update_post_meta( $post_id, "_{$this->prefix}_$setting", $value ); + } + } + } + + /** + * Function to display the metabox. + * + * @param \WP_Post $post Post object. + */ + public function html( $post ) { + // Add an nonce field so we can check for it later. + wp_nonce_field( $this->prefix . '_meta_box', $this->prefix . '_meta_box_nonce' ); + + $settings_form = new Settings_Form( + array( + 'settings_key' => $this->settings_key, + 'prefix' => $this->prefix, + 'checkbox_modified_text' => $this->checkbox_modified_text, + ) + ); + + echo ''; + foreach ( $this->registered_settings as $setting ) { + + $args = wp_parse_args( + $setting, + array( + 'id' => null, + 'name' => '', + 'desc' => '', + 'type' => null, + 'default' => '', + 'options' => '', + 'max' => null, + 'min' => null, + 'step' => null, + 'size' => null, + 'field_class' => '', + 'field_attributes' => '', + 'placeholder' => '', + ) + ); + + $id = $args['id']; + $value = get_post_meta( $post->ID, "_{$this->prefix}_{$id}", true ); + $args['value'] = ! empty( $value ) ? $value : ( isset( $args['default'] ) ? $args['default'] : $args['options'] ); + $type = isset( $args['type'] ) ? $args['type'] : 'text'; + $callback = method_exists( $settings_form, "callback_{$type}" ) ? array( $settings_form, "callback_{$type}" ) : array( $settings_form, 'callback_missing' ); + + echo ''; + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo ''; + echo ''; + } + echo '
' . $args['name'] . ''; + call_user_func( $callback, $args ); + echo '
'; + + /** + * Action triggered when displaying Top 10 meta box. + * + * @param object $post Post object. + */ + do_action( $this->prefix . '_meta_box', $post ); + } + + /** + * Sanitize Post Meta array. + * + * @param array $settings Post meta settings array. + * @return array Sanitized value. + */ + public function sanitize_post_meta( $settings ) { + + // This array holds a list of keys that will be passed through our category/tags loop to determine the ids. + $keys = array( + 'include_on_category' => array( + 'tax' => 'category', + 'ids_field' => 'include_on_category_ids', + ), + 'include_on_post_tag' => array( + 'tax' => 'post_tag', + 'ids_field' => 'include_on_post_tag_ids', + ), + ); + + foreach ( $keys as $key => $fields ) { + if ( isset( $settings[ $key ] ) ) { + $ids = array(); + $names = array(); + + $taxes = array_unique( str_getcsv( $settings[ $key ] ) ); + + foreach ( $taxes as $tax ) { + $tax_name = get_term_by( 'name', $tax, $fields['tax'] ); + + if ( isset( $tax_name->term_taxonomy_id ) ) { + $ids[] = $tax_name->term_taxonomy_id; + $names[] = $tax_name->name; + } + } + $settings[ $fields['ids_field'] ] = join( ',', $ids ); + $settings[ $key ] = \WebberZone\Better_Search\Util\Helpers::str_putcsv( $names ); + } else { + $settings[ $fields['ids_field'] ] = ''; + } + } + + return $settings; + } +} diff --git a/includes/admin/settings/class-settings-api.php b/includes/admin/settings/class-settings-api.php new file mode 100644 index 0000000..b42f8f9 --- /dev/null +++ b/includes/admin/settings/class-settings-api.php @@ -0,0 +1,951 @@ +settings_key = $settings_key; + $this->prefix = $prefix; + + $defaults = array( + 'translation_strings' => array(), + 'props' => array(), + 'settings_sections' => array(), + 'registered_settings' => array(), + 'upgraded_settings' => array(), + ); + $args = wp_parse_args( $args, $defaults ); + + $this->hooks(); + $this->set_translation_strings( $args['translation_strings'] ); + $this->set_props( $args['props'] ); + $this->set_sections( $args['settings_sections'] ); + $this->set_registered_settings( $args['registered_settings'] ); + $this->set_upgraded_settings( $args['upgraded_settings'] ); + } + + /** + * Adds the functions to the appropriate WordPress hooks. + */ + public function hooks() { + add_action( 'admin_menu', array( $this, 'admin_menu' ), 11 ); + add_action( 'admin_init', array( $this, 'admin_init' ) ); + add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); + } + + /** + * Sets properties. + * + * @param array|string $args { + * Array or string of arguments. Default is blank array. + * + * @type array $menus Array of admin menus. See add_custom_menu_page() for more info. + * @type string $default_tab Default tab. + * @type string $admin_footer_text Admin footer text. + * @type string $help_sidebar Help sidebar. + * @type array $help_tabs Help tabs. + * } + */ + public function set_props( $args ) { + + $defaults = array( + 'menus' => array(), + 'default_tab' => 'general', + 'admin_footer_text' => '', + 'help_sidebar' => '', + 'help_tabs' => array(), + ); + + $args = wp_parse_args( $args, $defaults ); + + foreach ( $args as $name => $value ) { + $this->$name = $value; + } + } + + /** + * Sets translation strings. + * + * @param array $strings { + * Array of translation strings. + * + * @type string $page_title Page title. + * @type string $menu_title Menu title. + * @type string $page_header Page header. + * @type string $reset_message Reset message. + * @type string $success_message Success message. + * @type string $save_changes Save changes button label. + * @type string $reset_settings Reset settings button label. + * @type string $reset_button_confirm Reset button confirmation message. + * @type string $checkbox_modified Checkbox modified label. + * } + * + * @return void + */ + public function set_translation_strings( $strings ) { + + // Args prefixed with an underscore are reserved for internal use. + $defaults = array( + 'page_header' => '', + 'reset_message' => __( 'Settings have been reset to their default values. Reload this page to view the updated settings.' ), + 'success_message' => __( 'Settings updated.' ), + 'save_changes' => __( 'Save Changes' ), + 'reset_settings' => __( 'Reset all settings' ), + 'reset_button_confirm' => __( 'Do you really want to reset all these settings to their default values?' ), + 'checkbox_modified' => __( 'Modified from default setting' ), + ); + + $strings = wp_parse_args( $strings, $defaults ); + + $this->translation_strings = $strings; + } + + /** + * Set settings sections + * + * @param array $sections Setting sections array in the format of: id => Title. + * @return object Class object. + */ + public function set_sections( $sections ) { + $this->settings_sections = (array) $sections; + + return $this; + } + + /** + * Add a single section + * + * @param array $section New Section. + * @return object Object of the class instance. + */ + public function add_section( $section ) { + $this->settings_sections[] = $section; + + return $this; + } + + /** + * Set the settings fields for registered settings. + * + * @param array $registered_settings { + * Array of settings in format id => attributes. + * @type string $section Section title. + * @type string $id Field ID. + * @type string $name Field name. + * @type string $desc Field description. + * @type string $type Field type. + * @type string $options Field default option(s). + * @type string $max Field max. Applicable for numbers. + * @type string $min Field min. Applicable for numbers. + * @type string $step Field step. Applicable for numbers. + * @type string $size Field size. Applicable for text and textarea. + * @type string $field_class CSS class. + * @type array $field_attributes HTML Attributes in the form of attribute => value. + * @type string $placeholder Placeholder. Applicable for text and textarea. + * @type string $sanitize_callback Sanitize callback. + * } + * } + * } + * @return object Object of the class instance. + */ + public function set_registered_settings( $registered_settings ) { + $this->registered_settings = (array) $registered_settings; + + return $this; + } + + /** + * Set the settings fields for settings to upgrade. + * + * @param array $upgraded_settings Settings array. + * @return object Object of the class instance. + */ + public function set_upgraded_settings( $upgraded_settings = array() ) { + $this->upgraded_settings = (array) $upgraded_settings; + + return $this; + } + + /** + * Add a menu page to the WordPress admin area. + * + * @param array $menu Array of settings for the menu page. + */ + public function add_custom_menu_page( $menu ) { + $defaults = array( + + // Modes: submenu, management, options, theme, plugins, users, dashboard, posts, media, links, pages, comments. + 'type' => 'submenu', + + // Submenu default settings. + 'parent_slug' => 'options-general.php', + 'page_title' => '', + 'menu_title' => '', + 'capability' => 'manage_options', + 'menu_slug' => '', + 'function' => array( $this, 'plugin_settings' ), + + // Menu default settings. + 'icon_url' => 'dashicons-admin-generic', + 'position' => null, + + ); + $menu = wp_parse_args( $menu, $defaults ); + + switch ( $menu['type'] ) { + case 'submenu': + $menu_page = add_submenu_page( + $menu['parent_slug'], + $menu['page_title'], + $menu['menu_title'], + $menu['capability'], + $menu['menu_slug'], + $menu['function'], + $menu['position'] + ); + break; + case 'management': + case 'options': + case 'theme': + case 'plugins': + case 'users': + case 'dashboard': + case 'posts': + case 'media': + case 'links': + case 'pages': + case 'comments': + $f = 'add_' . $menu['type'] . '_page'; + if ( function_exists( $f ) ) { + $menu_page = $f( + $menu['page_title'], + $menu['menu_title'], + $menu['capability'], + $menu['menu_slug'], + $menu['function'], + $menu['position'] + ); + } + break; + default: + $menu_page = add_menu_page( + $menu['page_title'], + $menu['menu_title'], + $menu['capability'], + $menu['menu_slug'], + $menu['function'], + $menu['icon_url'], + $menu['position'] + ); + break; + } + + return $menu_page; + } + + + /** + * Add admin menu. + */ + public function admin_menu() { + global ${$this->prefix . '_menu_pages'}; + + foreach ( $this->menus as $menu ) { + $menu_page = $this->add_custom_menu_page( $menu ); + + $this->menu_pages[ $menu['menu_slug'] ] = $menu_page; + if ( isset( $menu['settings_page'] ) && $menu['settings_page'] ) { + $this->settings_page = $menu_page; + } + } + ${$this->prefix . '_menu_pages'} = $this->menu_pages; + + // Load the settings contextual help. + add_action( 'load-' . $this->settings_page, array( $this, 'settings_help' ) ); + } + + /** + * Enqueue scripts and styles. + * + * @param string $hook The current admin page. + */ + public function admin_enqueue_scripts( $hook ) { + + if ( $hook === $this->settings_page ) { + self::enqueue_scripts_styles(); + } + } + + /** + * Enqueues all scripts, styles, settings, and templates necessary to use the Settings API. + */ + public static function enqueue_scripts_styles() { + + $minimize = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; + + wp_enqueue_style( 'wp-color-picker' ); + + wp_enqueue_media(); + wp_enqueue_script( 'wp-color-picker' ); + wp_enqueue_script( 'jquery' ); + wp_enqueue_script( 'jquery-ui-autocomplete' ); + wp_enqueue_script( 'jquery-ui-tabs' ); + + wp_enqueue_code_editor( + array( + 'type' => 'text/html', + 'codemirror' => array( + 'indentUnit' => 2, + 'tabSize' => 2, + ), + ) + ); + + wp_enqueue_script( + 'wz-admin-js', + plugins_url( 'js/admin-scripts' . $minimize . '.js', __FILE__ ), + array( 'jquery' ), + self::VERSION, + true + ); + wp_enqueue_script( + 'wz-codemirror-js', + plugins_url( 'js/apply-codemirror' . $minimize . '.js', __FILE__ ), + array( 'jquery' ), + self::VERSION, + true + ); + wp_enqueue_script( + 'wz-taxonomy-suggest-js', + plugins_url( 'js/taxonomy-suggest' . $minimize . '.js', __FILE__ ), + array( 'jquery' ), + self::VERSION, + true + ); + } + + /** + * Initialize and registers the settings sections and fields to WordPress + * + * Usually this should be called at `admin_init` hook. + * + * This public function gets the initiated settings sections and fields. Then + * registers them to WordPress and ready for use. + */ + public function admin_init() { + + $settings_key = $this->settings_key; + + if ( false === get_option( $settings_key ) ) { + add_option( $settings_key, $this->settings_defaults() ); + } + + $this->settings_form = new Settings_Form( + array( + 'settings_key' => $settings_key, + 'prefix' => $this->prefix, + 'checkbox_modified_text' => $this->translation_strings['checkbox_modified'], + ) + ); + + foreach ( $this->registered_settings as $section => $settings ) { + + add_settings_section( + "{$settings_key}_{$section}", // ID used to identify this section and with which to register options. + '', // No title, we will handle this via a separate function. + '__return_false', // No callback function needed. We'll process this separately. + "{$settings_key}_{$section}" // Page on which these options will be added. + ); + + foreach ( $settings as $setting ) { + + $args = wp_parse_args( + $setting, + array( + 'section' => $section, + 'id' => null, + 'name' => '', + 'desc' => '', + 'type' => null, + 'default' => '', + 'options' => '', + 'max' => null, + 'min' => null, + 'step' => null, + 'size' => null, + 'field_class' => '', + 'field_attributes' => '', + 'placeholder' => '', + ) + ); + + $id = $args['id']; + $name = $args['name']; + $type = isset( $args['type'] ) ? $args['type'] : 'text'; + $callback = method_exists( $this->settings_form, "callback_{$type}" ) ? array( $this->settings_form, "callback_{$type}" ) : array( $this->settings_form, 'callback_missing' ); + + add_settings_field( + "{$settings_key}[{$id}]", // ID of the settings field. We save it within the settings array. + $name, // Label of the setting. + $callback, // Function to handle the setting. + "{$settings_key}_{$section}", // Page to display the setting. In our case it is the section as defined above. + "{$settings_key}_{$section}", // Name of the section. + $args + ); + } + } + + // Register the settings into the options table. + register_setting( $settings_key, $settings_key, array( $this, 'settings_sanitize' ) ); + } + + /** + * Flattens $this->registered_settings into $setting[id] => $setting[type] format. + * + * @return array Default settings + */ + public function get_registered_settings_types() { + + $options = array(); + + // Populate some default values. + foreach ( $this->registered_settings as $tab => $settings ) { + foreach ( $settings as $option ) { + $options[ $option['id'] ] = $option['type']; + } + } + + /** + * Filters the settings array. + * + * @param array $options Default settings. + */ + return apply_filters( $this->prefix . '_get_settings_types', $options ); + } + + + /** + * Default settings. + * + * @return array Default settings + */ + public function settings_defaults() { + + $options = array(); + + // Populate some default values. + foreach ( $this->registered_settings as $tab => $settings ) { + foreach ( $settings as $option ) { + // When checkbox is set to true, set this to 1. + if ( 'checkbox' === $option['type'] && ! empty( $option['options'] ) ) { + $options[ $option['id'] ] = 1; + } else { + $options[ $option['id'] ] = 0; + } + // If an option is set. + if ( in_array( $option['type'], array( 'textarea', 'css', 'html', 'text', 'url', 'csv', 'color', 'numbercsv', 'postids', 'posttypes', 'number', 'wysiwyg', 'file', 'password' ), true ) && isset( $option['options'] ) ) { + $options[ $option['id'] ] = $option['options']; + } + if ( in_array( $option['type'], array( 'multicheck', 'radio', 'select', 'radiodesc', 'thumbsizes' ), true ) && isset( $option['default'] ) ) { + $options[ $option['id'] ] = $option['default']; + } + } + } + + $upgraded_settings = $this->upgraded_settings; + + if ( false !== $upgraded_settings ) { + $options = array_merge( $options, $upgraded_settings ); + } + + /** + * Filters the default settings array. + * + * @param array $options Default settings. + */ + return apply_filters( $this->prefix . '_settings_defaults', $options ); + } + + + /** + * Get the default option for a specific key + * + * @param string $key Key of the option to fetch. + * @return mixed + */ + public function get_default_option( $key = '' ) { + + $default_settings = $this->settings_defaults(); + + if ( array_key_exists( $key, $default_settings ) ) { + return $default_settings[ $key ]; + } else { + return false; + } + } + + + /** + * Reset settings. + * + * @return void + */ + public function settings_reset() { + delete_option( $this->settings_key ); + } + + /** + * Sanitize the form data being submitted. + * + * @param array $input Input unclean array. + * @return array Sanitized array + */ + public function settings_sanitize( $input ) { + + // This should be set if a form is submitted, so let's save it in the $referrer variable. + if ( empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + return $input; + } + + parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + // Check if we need to set to defaults. + $reset = isset( $_POST['settings_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + if ( $reset ) { + $this->settings_reset(); + $settings = get_option( $this->settings_key ); + + add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['reset_message'], 'error' ); + + return $settings; + } + + // Get the various settings we've registered. + $settings = get_option( $this->settings_key ); + $settings_types = $this->get_registered_settings_types(); + + // Get the tab. This is also our settings' section. + $tab = isset( $referrer['tab'] ) ? $referrer['tab'] : $this->default_tab; + + $input = $input ? $input : array(); + + /** + * Filter the settings for the tab. e.g. prefix_settings_general_sanitize. + * + * @param array $input Input unclean array + */ + $input = apply_filters( $this->prefix . '_settings_' . $tab . '_sanitize', $input ); + + // Create an output array by merging the existing settings with the ones submitted. + $output = array_merge( $settings, $input ); + + // Loop through each setting being saved and pass it through a sanitization filter. + foreach ( $settings_types as $key => $type ) { + + /** + * Skip settings that are not really settings. + * + * @param array $non_setting_types Array of types which are not settings. + */ + $non_setting_types = apply_filters( $this->prefix . '_non_setting_types', array( 'header', 'descriptive_text' ) ); + + if ( in_array( $type, $non_setting_types, true ) ) { + continue; + } + + if ( array_key_exists( $key, $output ) ) { + + $sanitize_callback = $this->get_sanitize_callback( $key ); + + // If callback is set, call it. + if ( $sanitize_callback ) { + $output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ] ); + continue; + } + } + + // Delete any key that is not present when we submit the input array. + if ( ! isset( $input[ $key ] ) ) { + unset( $output[ $key ] ); + } + + // Delete any settings that are no longer part of our registered settings. + if ( array_key_exists( $key, $output ) && ! array_key_exists( $key, $settings_types ) ) { + unset( $output[ $key ] ); + } + } + + add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['success_message'], 'updated' ); + + /** + * Filter the settings array before it is returned. + * + * @param array $output Settings array. + * @param array $input Input settings array. + */ + return apply_filters( $this->prefix . '_settings_sanitize', $output, $input ); + } + + /** + * Get sanitization callback for given Settings key. + * + * @param string $key Settings key. + * + * @return mixed Callback function or false if callback isn't found. + */ + public function get_sanitize_callback( $key = '' ) { + if ( empty( $key ) ) { + return false; + } + + $settings_sanitize = new Settings_Sanitize(); + + // Iterate over registered fields and see if we can find proper callback. + foreach ( $this->registered_settings as $section => $settings ) { + foreach ( $settings as $setting ) { + if ( $setting['id'] !== $key ) { + continue; + } + + // Return the callback name. + $sanitize_callback = false; + + if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) { + $sanitize_callback = $setting['sanitize_callback']; + return $sanitize_callback; + } + + if ( is_callable( array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ) ) ) { + $sanitize_callback = array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ); + return $sanitize_callback; + } + + return $sanitize_callback; + } + } + + return false; + } + + /** + * Render the settings page. + */ + public function plugin_settings() { + ob_start(); + ?> +
+

translation_strings['page_header'] ); ?>

+ prefix . '_settings_page_header' ); ?> + +
+
+
+ + show_navigation(); ?> + show_form(); ?> + +
+ +
+ +
+ +
+ +
+
+
+
+ +
+ + settings_sections ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended + + $html = ''; + + echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Show the section settings forms + * + * This public function displays every sections in a different form + */ + public function show_form() { + ob_start(); + ?> + +
+ + settings_key ); ?> + + settings_sections as $tab_id => $tab_name ) : ?> + +
+ + prefix . '_settings_' . $tab_id, $this->prefix . '_settings_' . $tab_id ); + ?> +
+

+ translation_strings['save_changes'], + 'primary', + 'submit', + false + ); + + echo '  '; + + // Reset button. + $confirm = esc_js( $this->translation_strings['reset_button_confirm'] ); + submit_button( + $this->translation_strings['reset_settings'], + 'secondary', + 'settings_reset', + false, + array( + 'onclick' => "return confirm('{$confirm}');", + ) + ); + + echo '  '; + + /** + * Action to add more buttons in each tab. + * + * @param string $tab_id Tab ID. + * @param string $tab_name Tab name. + * @param array $settings_sections Settings sections. + */ + do_action( $this->prefix . '_settings_form_buttons', $tab_id, $tab_name, $this->settings_sections ); + ?> +

+
+ + + +
+ + admin_footer_text ) && get_current_screen()->id === $this->settings_page ) { + + $text = $this->admin_footer_text; + + return str_replace( '', '', $footer_text ) . ' | ' . $text . ''; + } else { + return $footer_text; + } + } + + /** + * Function to add the contextual help in the settings page. + */ + public function settings_help() { + $screen = get_current_screen(); + + if ( $screen->id !== $this->settings_page ) { + return; + } + + $screen->set_help_sidebar( $this->help_sidebar ); + + foreach ( $this->help_tabs as $tab ) { + $screen->add_help_tab( $tab ); + } + } +} diff --git a/includes/admin/settings/class-settings-form.php b/includes/admin/settings/class-settings-form.php new file mode 100644 index 0000000..761d5e2 --- /dev/null +++ b/includes/admin/settings/class-settings-form.php @@ -0,0 +1,710 @@ + '', + 'prefix' => '', + 'checkbox_modified_text' => '', + ); + $args = wp_parse_args( $args, $defaults ); + + foreach ( $args as $name => $value ) { + $this->$name = $value; + } + } + + /** + * Get field description for display. + * + * @param array $args settings Arguments array. + * + * @return string Description of the field. + */ + public function get_field_description( $args ) { + if ( ! empty( $args['desc'] ) ) { + $desc = '

' . wp_kses_post( $args['desc'] ) . '

'; + } else { + $desc = ''; + } + + /** + * After Settings Output filter + * + * @param string $desc Description of the field. + * @param array $args Arguments array. + */ + $desc = apply_filters( $this->prefix . '_setting_field_description', $desc, $args ); + return $desc; + } + + /** + * Get the value of a settings field. + * + * @param string $option Settings field name. + * @param string $default_value Default text if it's not found. + * @return string + */ + public function get_option( $option, $default_value = '' ) { + + $options = \get_option( $this->settings_key ); + + if ( isset( $options[ $option ] ) ) { + return $options[ $option ]; + } + + return $default_value; + } + + /** + * Miscellaneous callback funcion + * + * @param array $args Arguments array. + * @return void + */ + public function callback_missing( $args ) { + /* translators: 1: Code. */ + printf( esc_html__( 'The callback function used for the %1$s setting is missing.' ), '' . esc_attr( $args['id'] ) . '' ); + } + + /** + * Header Callback + * + * Renders the header. + * + * @param array $args Arguments passed by the setting. + * @return void + */ + public function callback_header( $args ) { + $html = $this->get_field_description( $args ); + + /** + * After Settings Output filter + * + * @param string $html HTML string. + * @param array $args Arguments array. + */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Descriptive text callback. + * + * Renders descriptive text onto the settings field. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_descriptive_text( $args ) { + $this->callback_header( $args ); + } + + /** + * Display text fields. + * + * @param array $args Array of arguments. + */ + public function callback_text( $args ) { + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + $size = sanitize_html_class( isset( $args['size'] ) ? $args['size'] : 'regular' ); + $class = sanitize_html_class( $args['field_class'] ); + $placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . $args['placeholder'] . '"'; + $disabled = ! empty( $args['disabled'] ) ? ' disabled="disabled"' : ''; + $readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : ''; + $attributes = $disabled . $readonly; + + foreach ( (array) $args['field_attributes'] as $attribute => $val ) { + $attributes .= sprintf( ' %1$s="%2$s"', $attribute, esc_attr( $val ) ); + } + + $html = sprintf( + '', + $this->settings_key, + sanitize_key( $args['id'] ), + $class . ' ' . $size . '-text', + esc_attr( stripslashes( $value ) ), + $attributes, + $placeholder + ); + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Display url fields. + * + * @param array $args Array of arguments. + */ + public function callback_url( $args ) { + $this->callback_text( $args ); + } + + /** + * Display csv fields. + * + * @param array $args Array of arguments. + */ + public function callback_csv( $args ) { + $this->callback_text( $args ); + } + + /** + * Display color fields. + * + * @param array $args Array of arguments. + */ + public function callback_color( $args ) { + $this->callback_text( $args ); + } + + /** + * Display numbercsv fields. + * + * @param array $args Array of arguments. + */ + public function callback_numbercsv( $args ) { + $this->callback_text( $args ); + } + + /** + * Display postids fields. + * + * @param array $args Array of arguments. + */ + public function callback_postids( $args ) { + $this->callback_text( $args ); + } + + /** + * Display textarea. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_textarea( $args ) { + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + $class = sanitize_html_class( $args['field_class'] ); + + $html = sprintf( + '', + $this->settings_key, + sanitize_key( $args['id'] ), + esc_textarea( stripslashes( $value ) ), + 'large-text ' . $class + ); + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Display CSS fields. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_css( $args ) { + $this->callback_textarea( $args ); + } + + /** + * Display HTML fields. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_html( $args ) { + $this->callback_textarea( $args ); + } + + /** + * Display checkboxes. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_checkbox( $args ) { + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + $checked = ! empty( $value ) ? checked( 1, $value, false ) : ''; + $default = isset( $args['options'] ) ? (int) $args['options'] : ''; + + $html = sprintf( '', $this->settings_key, sanitize_key( $args['id'] ) ); + $html .= sprintf( '', $this->settings_key, sanitize_key( $args['id'] ), $checked ); + $html .= ( (bool) $value !== (bool) $default ) ? '' . $this->checkbox_modified_text . '' : ''; + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Multicheck Callback + * + * Renders multiple checkboxes. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_multicheck( $args ) { + $html = ''; + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + + if ( ! empty( $args['options'] ) ) { + $html .= sprintf( '', $this->settings_key, $args['id'] ); + + foreach ( $args['options'] as $key => $option ) { + if ( isset( $value[ $key ] ) ) { + $enabled = $key; + } else { + $enabled = null; + } + + $html .= sprintf( + ' ', + $this->settings_key, + sanitize_key( $args['id'] ), + sanitize_key( $key ), + esc_attr( $key ), + checked( $key, $enabled, false ) + ); + $html .= sprintf( + '
', + $this->settings_key, + sanitize_key( $args['id'] ), + sanitize_key( $key ), + $option + ); + } + + $html .= $this->get_field_description( $args ); + } + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Radio Callback + * + * Renders radio boxes. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_radio( $args ) { + $html = ''; + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['default'] ); + + foreach ( $args['options'] as $key => $option ) { + $html .= sprintf( + ' ', + $this->settings_key, + sanitize_key( $args['id'] ), + $key, + checked( $value, $key, false ) + ); + $html .= sprintf( + '
', + $this->settings_key, + sanitize_key( $args['id'] ), + $key, + $option + ); + } + + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Radio callback with description. + * + * Renders radio boxes with each item having it separate description. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_radiodesc( $args ) { + $html = ''; + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['default'] ); + + foreach ( $args['options'] as $option ) { + $html .= sprintf( + ' ', + $this->settings_key, + sanitize_key( $args['id'] ), + $option['id'], + checked( $value, $option['id'], false ) + ); + $html .= sprintf( + '', + $this->settings_key, + sanitize_key( $args['id'] ), + $option['id'], + $option['name'] + ); + + $html .= ': ' . wp_kses_post( $option['description'] ) . '
'; + } + + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Radio callback with description. + * + * Renders radio boxes with each item having it separate description. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_thumbsizes( $args ) { + $html = ''; + + $thumb_size = $this->prefix . '_thumbnail'; + + if ( ! isset( $args['options'][ $thumb_size ] ) ) { + $args['options'][ $thumb_size ] = array( + 'name' => $thumb_size, + 'width' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_width', 150 ) ), + 'height' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_height', 150 ) ), + 'crop' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_crop', true ) ), + ); + } + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['default'] ); + + foreach ( $args['options'] as $name => $option ) { + $html .= sprintf( + ' ', + $this->settings_key, + sanitize_key( $args['id'] ), + $name, + checked( $value, $name, false ) + ); + $html .= sprintf( + '
', + $this->settings_key, + sanitize_key( $args['id'] ), + $name, + (int) $option['width'], + (int) $option['height'], + (bool) $option['crop'] ? ' ' . __( 'cropped' ) : '' + ); + } + + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Number Callback + * + * Renders number fields. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_number( $args ) { + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + $max = isset( $args['max'] ) ? intval( $args['max'] ) : 999999; + $min = isset( $args['min'] ) ? intval( $args['min'] ) : 0; + $step = isset( $args['step'] ) ? intval( $args['step'] ) : 1; + $size = isset( $args['size'] ) ? $args['size'] : 'regular'; + $placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . esc_attr( $args['placeholder'] ) . '"'; + + $html = sprintf( + '', + esc_attr( (string) $step ), + esc_attr( (string) $max ), + esc_attr( (string) $min ), + sanitize_html_class( $size ) . '-text', + sanitize_key( $args['id'] ), + esc_attr( stripslashes( $value ) ), + $placeholder, + $this->settings_key + ); + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Select Callback + * + * Renders select fields. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_select( $args ) { + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['default'] ); + + if ( isset( $args['chosen'] ) ) { + $chosen = 'class="chosen"'; + } else { + $chosen = ''; + } + + $html = sprintf( + ''; + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Display posttypes fields. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_posttypes( $args ) { + $html = ''; + + $options = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + + // If post_types contains a query string then parse it with wp_parse_args. + if ( is_string( $options ) && strpos( $options, '=' ) ) { + $post_types = wp_parse_args( $options ); + } else { + $post_types = wp_parse_list( $options ); + } + + $wp_post_types = get_post_types( + array( + 'public' => true, + ) + ); + $posts_types_inc = array_intersect( $wp_post_types, $post_types ); + + foreach ( $wp_post_types as $wp_post_type ) { + + $html .= sprintf( + ' ', + sanitize_key( $args['id'] ), + esc_attr( $wp_post_type ), + checked( true, in_array( $wp_post_type, $posts_types_inc, true ), false ), + $this->settings_key + ); + $html .= sprintf( '
', sanitize_key( $args['id'] ), $wp_post_type, $this->settings_key ); + + } + + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + + /** + * Display taxonomies fields. + * + * @param array $args Array of arguments. + * @return void + */ + public function callback_taxonomies( $args ) { + $html = ''; + + $options = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + + // If taxonomies contains a query string then parse it with wp_parse_args. + if ( is_string( $options ) && strpos( $options, '=' ) ) { + $taxonomies = wp_parse_args( $options ); + } else { + $taxonomies = wp_parse_list( $options ); + } + + /* Fetch taxonomies */ + $argsc = array( + 'public' => true, + ); + $output = 'objects'; + $operator = 'and'; + $wp_taxonomies = get_taxonomies( $argsc, $output, $operator ); + + $taxonomies_inc = array_intersect( wp_list_pluck( (array) $wp_taxonomies, 'name' ), $taxonomies ); + + foreach ( $wp_taxonomies as $wp_taxonomy ) { + + $html .= sprintf( + ' ', + sanitize_key( $args['id'] ), + esc_attr( $wp_taxonomy->name ), + checked( true, in_array( $wp_taxonomy->name, $taxonomies_inc, true ), false ), + $this->settings_key + ); + $html .= sprintf( + '
', + sanitize_key( $args['id'] ), + esc_attr( $wp_taxonomy->name ), + $wp_taxonomy->labels->name, + $this->settings_key + ); + + } + + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + + /** + * Displays a rich text textarea for a settings field. + * + * @param array $args Array of arguments. + */ + public function callback_wysiwyg( $args ) { + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + $size = isset( $args['size'] ) ? $args['size'] : '500px'; + + echo '
'; + + $editor_settings = array( + 'teeny' => true, + 'textarea_name' => $args['section'] . '[' . $args['id'] . ']', + 'textarea_rows' => 10, + ); + + if ( isset( $args['options'] ) && is_array( $args['options'] ) ) { + $editor_settings = array_merge( $editor_settings, $args['options'] ); + } + + wp_editor( $value, $args['section'] . '-' . $args['id'], $editor_settings ); + + echo '
'; + + echo $this->get_field_description( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Displays a file upload field for a settings field. + * + * @param array $args Array of arguments. + */ + public function callback_file( $args ) { + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + $size = sanitize_html_class( isset( $args['size'] ) ? $args['size'] : 'regular' ); + $class = sanitize_html_class( $args['field_class'] ); + $label = isset( $args['options']['button_label'] ) ? $args['options']['button_label'] : __( 'Choose File' ); + + $html = sprintf( + '', + $class . ' ' . $size . '-text file-url', + $this->settings_key, + sanitize_key( $args['id'] ), + esc_attr( $value ) + ); + $html .= ''; + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Displays a password field for a settings field. + * + * @param array $args Array of arguments. + */ + public function callback_password( $args ) { + + $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] ); + $size = sanitize_html_class( isset( $args['size'] ) ? $args['size'] : 'regular' ); + $class = sanitize_html_class( $args['field_class'] ); + + $html = sprintf( + '', + $class . ' ' . $size . '-text', + $this->settings_key, + sanitize_key( $args['id'] ), + esc_attr( $value ) + ); + $html .= $this->get_field_description( $args ); + + /** This filter has been defined in class-settings-api.php */ + echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} diff --git a/includes/admin/settings/class-settings-sanitize.php b/includes/admin/settings/class-settings-sanitize.php new file mode 100644 index 0000000..c3f5838 --- /dev/null +++ b/includes/admin/settings/class-settings-sanitize.php @@ -0,0 +1,188 @@ +sanitize_textarea_field( $value ); + } + + /** + * Sanitize number fields + * + * @param string $value The field value. + * @return string Sanitized value + */ + public function sanitize_number_field( $value ) { + return filter_var( $value, FILTER_SANITIZE_NUMBER_INT ); + } + + /** + * Sanitize CSV fields + * + * @param string $value The field value. + * @return string Sanitizied value + */ + public function sanitize_csv_field( $value ) { + return implode( ',', array_map( 'trim', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) ); + } + + /** + * Sanitize CSV fields which hold numbers + * + * @param string $value The field value. + * @return string Sanitized value + */ + public function sanitize_numbercsv_field( $value ) { + return implode( ',', array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) ) ); + } + + /** + * Sanitize CSV fields which hold post IDs + * + * @param string $value The field value. + * @return string Sanitized value + */ + public function sanitize_postids_field( $value ) { + $ids = array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) ); + + foreach ( $ids as $key => $value ) { + if ( false === get_post_status( $value ) ) { + unset( $ids[ $key ] ); + } + } + + return implode( ',', $ids ); + } + + /** + * Sanitize textarea fields + * + * @param string $value The field value. + * @return string Sanitized value + */ + public function sanitize_textarea_field( $value ) { + + global $allowedposttags; + + // We need more tags to allow for script and style. + $moretags = array( + 'script' => array( + 'type' => true, + 'src' => true, + 'async' => true, + 'defer' => true, + 'charset' => true, + ), + 'style' => array( + 'type' => true, + 'media' => true, + 'scoped' => true, + ), + 'link' => array( + 'rel' => true, + 'type' => true, + 'href' => true, + 'media' => true, + 'sizes' => true, + 'hreflang' => true, + ), + ); + + $allowedtags = array_merge( $allowedposttags, $moretags ); + + /** + * Filter allowed tags allowed when sanitizing text and textarea fields. + * + * @param array $allowedtags Allowed tags array. + */ + $allowedtags = apply_filters( 'wz_sanitize_allowed_tags', $allowedtags ); + + return wp_kses( wp_unslash( $value ), $allowedtags ); + } + + /** + * Sanitize checkbox fields + * + * @param mixed $value The field value. + * @return int Sanitized value + */ + public function sanitize_checkbox_field( $value ) { + $value = ( -1 === (int) $value ) ? 0 : 1; + + return $value; + } + + /** + * Sanitize post_types fields + * + * @param array $value The field value. + * @return string $value Sanitized value + */ + public function sanitize_posttypes_field( $value ) { + $post_types = array_map( 'sanitize_text_field', (array) wp_unslash( $value ) ); + + return implode( ',', $post_types ); + } + + /** + * Sanitize post_types fields + * + * @param array $value The field value. + * @return string $value Sanitized value + */ + public function sanitize_taxonomies_field( $value ) { + $taxonomies = array_map( 'sanitize_text_field', (array) wp_unslash( $value ) ); + + return implode( ',', $taxonomies ); + } + + /** + * Sanitize color fields. + * + * @param string $value The field value. + * @return string Sanitized value + */ + public function sanitize_color_field( $value ) { + return sanitize_hex_color( $value ); + } +} diff --git a/includes/admin/settings/class-settings.php b/includes/admin/settings/class-settings.php new file mode 100644 index 0000000..5dcc170 --- /dev/null +++ b/includes/admin/settings/class-settings.php @@ -0,0 +1,1026 @@ +settings_key = 'bsearch_settings'; + self::$prefix = 'bsearch'; + $this->menu_slug = 'bsearch_options_page'; + + add_action( 'admin_menu', array( $this, 'initialise_settings' ) ); + add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 11, 2 ); + add_filter( 'plugin_action_links_' . plugin_basename( BETTER_SEARCH_PLUGIN_FILE ), array( $this, 'plugin_actions_links' ) ); + add_filter( 'bsearch_settings_sanitize', array( $this, 'change_settings_on_save' ), 99 ); + } + + /** + * Initialise the settings API. + * + * @since 3.3.0 + */ + public function initialise_settings() { + $props = array( + 'default_tab' => 'general', + 'help_sidebar' => $this->get_help_sidebar(), + 'help_tabs' => $this->get_help_tabs(), + 'admin_footer_text' => $this->get_admin_footer_text(), + 'menus' => $this->get_menus(), + ); + + $args = array( + 'props' => $props, + 'translation_strings' => $this->get_translation_strings(), + 'settings_sections' => $this->get_settings_sections(), + 'registered_settings' => $this->get_registered_settings(), + 'upgraded_settings' => array(), + ); + + $this->settings_api = new Settings_API( $this->settings_key, self::$prefix, $args ); + } + + /** + * Array containing the translation strings. + * + * @since 1.8.0 + * + * @return array Translation strings. + */ + public function get_translation_strings() { + $strings = array( + 'page_header' => esc_html__( 'Better Search Settings', 'better-search' ), + 'reset_message' => esc_html__( 'Settings have been reset to their default values. Reload this page to view the updated settings.', 'better-search' ), + 'success_message' => esc_html__( 'Settings updated.', 'better-search' ), + 'save_changes' => esc_html__( 'Save Changes', 'better-search' ), + 'reset_settings' => esc_html__( 'Reset all settings', 'better-search' ), + 'reset_button_confirm' => esc_html__( 'Do you really want to reset all these settings to their default values?', 'better-search' ), + 'checkbox_modified' => esc_html__( 'Modified from default setting', 'better-search' ), + ); + + /** + * Filter the array containing the settings' sections. + * + * @since 1.8.0 + * + * @param array $strings Translation strings. + */ + return apply_filters( self::$prefix . '_translation_strings', $strings ); + } + + /** + * Get the admin menus. + * + * @return array Admin menus. + */ + public function get_menus() { + $menus = array(); + + // Settings menu. + $menus[] = array( + 'settings_page' => true, + 'type' => 'submenu', + 'parent_slug' => 'bsearch_dashboard', + 'page_title' => esc_html__( 'Better Search Settings', 'better-search' ), + 'menu_title' => esc_html__( 'Settings', 'better-search' ), + 'menu_slug' => $this->menu_slug, + ); + + return $menus; + } + + /** + * Array containing the settings' sections. + * + * @since 3.3.0 + * + * @return array Settings array + */ + public static function get_settings_sections() { + $settings_sections = array( + 'general' => __( 'General', 'better-search' ), + 'search' => __( 'Search', 'better-search' ), + 'heatmap' => __( 'Heatmap', 'better-search' ), + 'styles' => __( 'Styles', 'better-search' ), + ); + + /** + * Filter the array containing the settings' sections. + * + * @since 3.3.0 + * + * @param array $settings_sections Settings array + */ + return apply_filters( self::$prefix . '_settings_sections', $settings_sections ); + } + + + /** + * Retrieve the array of plugin settings + * + * @since 3.3.0 + * + * @return array Settings array + */ + public static function get_registered_settings() { + $settings = array(); + $sections = self::get_settings_sections(); + + foreach ( $sections as $section => $value ) { + $method_name = 'settings_' . $section; + if ( method_exists( __CLASS__, $method_name ) ) { + $settings[ $section ] = self::$method_name(); + } + } + + /** + * Filters the settings array + * + * @since 1.2.0 + * + * @param array $bsearch_setings Settings array + */ + return apply_filters( self::$prefix . '_registered_settings', $settings ); + } + + /** + * Retrieve the array of General settings + * + * @since 3.3.0 + * + * @return array General settings array + */ + public static function settings_general() { + $settings = array( + 'seamless' => array( + 'id' => 'seamless', + 'name' => esc_html__( 'Enable seamless integration', 'better-search' ), + 'desc' => esc_html__( "Complete integration with your theme. Enabling this option will ignore better-search-template.php. It will continue to display the search results sorted by relevance, although it won't display the percentage relevance.", 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'track_popular' => array( + 'id' => 'track_popular', + 'name' => esc_html__( 'Enable search tracking', 'better-search' ), + 'desc' => esc_html__( 'If you turn this off, then the plugin will no longer track and display the popular search terms.', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'track_admins' => array( + 'id' => 'track_admins', + 'name' => esc_html__( 'Track admin searches', 'better-search' ), + 'desc' => esc_html__( 'Disabling this option will stop searches made by admins from being tracked.', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'track_editors' => array( + 'id' => 'track_editors', + 'name' => esc_html__( 'Track editor user group searches', 'better-search' ), + 'desc' => esc_html__( 'Disabling this option will stop searches made by editors from being tracked.', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'cache' => array( + 'id' => 'cache', + 'name' => esc_html__( 'Enable cache', 'better-search' ), + 'desc' => esc_html__( 'If activated, Better Search will use the Transients API to cache the search results for 1 hour.', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + 'cache_time' => array( + 'id' => 'cache_time', + 'name' => esc_html__( 'Time to cache', 'top-10' ), + 'desc' => esc_html__( 'Enter the number of seconds to cache the output.', 'top-10' ), + 'type' => 'text', + 'options' => HOUR_IN_SECONDS, + ), + 'meta_noindex' => array( + 'id' => 'meta_noindex', + 'name' => esc_html__( 'Stop search engines from indexing search results pages', 'better-search' ), + 'desc' => esc_html__( 'This is a recommended option to turn ON. Adds noindex,follow meta tag to the head of the page', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'number_format_count' => array( + 'id' => 'number_format_count', + 'name' => esc_html__( 'Number format count', 'better-search' ), + 'desc' => esc_html__( 'Activating this option will convert the search counts into a number format based on the locale', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'show_credit' => array( + 'id' => 'show_credit', + 'name' => esc_html__( 'Link to Better Search plugin page', 'better-search' ), + 'desc' => esc_html__( 'A nofollow link to the plugin is added as an extra list item to the list of popular searches. Not mandatory, but thanks if you do it!', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + ); + + /** + * Filters the General settings array + * + * @since 2.5.0 + * + * @param array $settings General settings array + */ + return apply_filters( self::$prefix . '_settings_general', $settings ); + } + + + /** + * Retrieve the array of Search settings + * + * @since 3.3.0 + * + * @return array Search settings array + */ + public static function settings_search() { + $settings = array( + 'limit' => array( + 'id' => 'limit', + 'name' => esc_html__( 'Number of Search Results per page', 'better-search' ), + 'desc' => esc_html__( 'This is the maximum number of search results that will be displayed per page by default', 'better-search' ), + 'type' => 'number', + 'options' => '10', + 'size' => 'small', + ), + 'post_types' => array( + 'id' => 'post_types', + 'name' => esc_html__( 'Post types to include', 'better-search' ), + 'desc' => esc_html__( 'Select which post types you want to include in the search results', 'better-search' ), + 'type' => 'posttypes', + 'options' => 'post,page', + ), + 'use_fulltext' => array( + 'id' => 'use_fulltext', + 'name' => esc_html__( 'Enable mySQL FULLTEXT searching', 'better-search' ), + 'desc' => esc_html__( 'Disabling this option will no longer give relevancy based results', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'boolean_mode' => array( + 'id' => 'boolean_mode', + 'name' => esc_html__( 'Activate BOOLEAN mode', 'better-search' ), + /* translators: 1: Opening anchor tag, 2: Closing anchor tag, */ + 'desc' => sprintf( esc_html__( 'Limits relevancy matches but removes several limitations of NATURAL LANGUAGE mode. %1$sCheck the mySQL docs for further information on BOOLEAN indices%2$s', 'better-search' ), '', '' ), + 'type' => 'checkbox', + 'options' => false, + ), + 'weight_title' => array( + 'id' => 'weight_title', + 'name' => esc_html__( 'Weight of the title', 'better-search' ), + 'desc' => esc_html__( 'Set this to a bigger number than the next option to prioritize the post title', 'better-search' ), + 'type' => 'number', + 'options' => '10', + 'size' => 'small', + ), + 'weight_content' => array( + 'id' => 'weight_content', + 'name' => esc_html__( 'Weight of the post content', 'better-search' ), + 'desc' => esc_html__( 'Set this to a bigger number than the previous option to prioritize the post content', 'better-search' ), + 'type' => 'number', + 'options' => '1', + 'size' => 'small', + ), + 'search_excerpt' => array( + 'id' => 'search_excerpt', + 'name' => esc_html__( 'Search Excerpt', 'better-search' ), + 'desc' => esc_html__( 'Select to search the post excerpt.', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'search_taxonomies' => array( + 'id' => 'search_taxonomies', + 'name' => esc_html__( 'Search Taxonomies', 'better-search' ), + 'desc' => esc_html__( 'Select to include posts where all taxonomies match the search term(s). This includes categories, tags and custom post types.', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + 'search_meta' => array( + 'id' => 'search_meta', + 'name' => esc_html__( 'Search Meta', 'better-search' ), + 'desc' => esc_html__( 'Select to include posts where meta values match the search term(s).', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + 'search_authors' => array( + 'id' => 'search_authors', + 'name' => esc_html__( 'Search Authors', 'better-search' ), + 'desc' => esc_html__( 'Select to include posts from authors that match the search term(s).', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + 'search_comments' => array( + 'id' => 'search_comments', + 'name' => esc_html__( 'Search Comments', 'better-search' ), + 'desc' => esc_html__( 'Select to include posts where comments include the search term(s).', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + 'exclude_protected_posts' => array( + 'id' => 'exclude_protected_posts', + 'name' => esc_html__( 'Exclude password protected posts', 'better-search' ), + 'desc' => esc_html__( 'Enabling this option will remove password protected posts from the search results', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'exclude_post_ids' => array( + 'id' => 'exclude_post_ids', + 'name' => esc_html__( 'Exclude post IDs', 'better-search' ), + 'desc' => esc_html__( 'Enter a comma separated list of post/page/custom post type IDs e.g. 188,1024,50', 'better-search' ), + 'type' => 'numbercsv', + 'options' => '', + ), + 'exclude_cat_slugs' => array( + 'id' => 'exclude_cat_slugs', + 'name' => esc_html__( 'Exclude Categories', 'better-search' ), + 'desc' => esc_html__( 'Comma separated list of category slugs. The field above has an autocomplete so simply start typing in the starting letters and it will prompt you with options. Does not support custom taxonomies.', 'better-search' ), + 'type' => 'csv', + 'options' => '', + 'size' => 'large', + 'field_class' => 'category_autocomplete', + 'field_attributes' => array( + 'data-wp-taxonomy' => 'category', + ), + ), + 'exclude_categories' => array( + 'id' => 'exclude_categories', + 'name' => esc_html__( 'Exclude category IDs', 'better-search' ), + 'desc' => esc_html__( 'This is a readonly field that is automatically populated based on the above input when the settings are saved. These might differ from the IDs visible in the Categories page which use the term_id. Better Search uses the term_taxonomy_id which is unique to this taxonomy.', 'better-search' ), + 'type' => 'text', + 'options' => '', + 'readonly' => true, + ), + 'display_header' => array( + 'id' => 'display_header', + 'name' => '

' . esc_html__( 'Display options', 'better-search' ) . '

', + 'desc' => esc_html__( 'These settings allow you to customize the output of the search results page. Except for the highlight setting, these only apply when Seamless mode is off.', 'better-search' ), + 'type' => 'header', + ), + 'highlight' => array( + 'id' => 'highlight', + 'name' => esc_html__( 'Highlight search terms', 'better-search' ), + 'desc' => esc_html__( 'If enabled, the search terms are wrapped with the class bsearch_highlight on the search results page. The default stylesheet includes CSS to add some colour.', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'highlight_followed_links' => array( + 'id' => 'highlight_followed_links', + 'name' => esc_html__( 'Highlight followed links', 'better-search' ), + 'desc' => esc_html__( 'If enabled, the plugin will highlight the search terms on posts/pages when visits them from the search results page.', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'include_thumb' => array( + 'id' => 'include_thumb', + 'name' => esc_html__( 'Display thumbnail', 'better-search' ), + 'desc' => '', + 'type' => 'checkbox', + 'options' => true, + ), + 'display_relevance' => array( + 'id' => 'display_relevance', + 'name' => esc_html__( 'Display relevance', 'better-search' ), + 'desc' => '', + 'type' => 'checkbox', + 'options' => true, + ), + 'display_post_type' => array( + 'id' => 'display_post_type', + 'name' => esc_html__( 'Display post type', 'better-search' ), + 'desc' => '', + 'type' => 'checkbox', + 'options' => true, + ), + 'display_author' => array( + 'id' => 'display_author', + 'name' => esc_html__( 'Display author', 'better-search' ), + 'desc' => '', + 'type' => 'checkbox', + 'options' => true, + ), + 'display_date' => array( + 'id' => 'display_date', + 'name' => esc_html__( 'Display date', 'better-search' ), + 'desc' => '', + 'type' => 'checkbox', + 'options' => true, + ), + 'display_taxonomies' => array( + 'id' => 'display_taxonomies', + 'name' => esc_html__( 'Display taxonomies', 'better-search' ), + 'desc' => '', + 'type' => 'checkbox', + 'options' => true, + ), + 'excerpt_length' => array( + 'id' => 'excerpt_length', + 'name' => esc_html__( 'Length of excerpt (in words)', 'better-search' ), + 'desc' => '', + 'type' => 'number', + 'options' => '100', + 'size' => 'small', + ), + 'banned_header' => array( + 'id' => 'banned_header', + 'name' => '

' . esc_html__( 'Banned words options', 'better-search' ) . '

', + 'desc' => '', + 'type' => 'header', + ), + 'badwords' => array( + 'id' => 'badwords', + 'name' => esc_html__( 'Filter these words', 'better-search' ), + 'desc' => esc_html__( 'Words in this list will be stripped out of the search results. Enter these as a comma-separated list.', 'better-search' ), + 'type' => 'textarea', + 'options' => implode( ',', self::get_badwords() ), + ), + 'banned_whole_words' => array( + 'id' => 'banned_whole_words', + 'name' => esc_html__( 'Filter whole words only', 'better-search' ), + 'desc' => esc_html__( 'When activated, only whole words in the search query are filtered. Partial words are ignored. e.g. grow will not ban grown or grower.', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + 'banned_stop_search' => array( + 'id' => 'banned_stop_search', + 'name' => esc_html__( 'Stop query on banned words filter', 'better-search' ), + 'desc' => esc_html__( 'When activated, this option will return no results if the search query includes any of the words in the box above. If you have seamless mode off, Better Search will display an error message. With seamless mode on, this will give a Nothing found message. You can customize it by editing your theme.', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + ); + + /** + * Filters the Counter settings array + * + * @since 2.5.0 + * + * @param array $settings Counter settings array + */ + return apply_filters( self::$prefix . '_settings_counter', $settings ); + } + + + /** + * Retrieve the array of Heatmap settings + * + * @since 3.3.0 + * + * @return array Heatmap settings array + */ + public static function settings_heatmap() { + $settings = array( + 'include_heatmap' => array( + 'id' => 'include_heatmap', + 'name' => esc_html__( 'Include heatmap on the search results', 'better-search' ), + 'desc' => esc_html__( 'This option will display the heatmaps at the bottom of the search results page. Display popular searches to your visitors. Does not apply when Seamless mode is enabled.', 'better-search' ), + 'type' => 'checkbox', + 'options' => false, + ), + 'title' => array( + 'id' => 'title', + 'name' => esc_html__( 'Heading of Overall Popular Searches', 'better-search' ), + 'desc' => esc_html__( 'Displayed before the list of the searches as a the master heading', 'better-search' ), + 'type' => 'text', + 'options' => '

' . esc_html__( 'Popular searches:', 'better-search' ) . '

', + 'size' => 'large', + ), + 'title_daily' => array( + 'id' => 'title_daily', + 'name' => esc_html__( 'Heading of Daily Popular Searches', 'better-search' ), + 'desc' => esc_html__( 'Displayed before the list of the searches as a the master heading', 'better-search' ), + 'type' => 'text', + 'options' => '

' . esc_html__( 'Currently trending searches:', 'better-search' ) . '

', + 'size' => 'large', + ), + 'daily_range' => array( + 'id' => 'daily_range', + 'name' => esc_html__( 'Currently trending should contain searches of how many days?', 'better-search' ), + 'desc' => esc_html__( 'This settings allows you to change the number of days for the currently trending heatmap. This used to be called Daily popular in previous versions.', 'better-search' ), + 'type' => 'number', + 'options' => '7', + 'size' => 'small', + ), + 'heatmap_limit' => array( + 'id' => 'heatmap_limit', + 'name' => esc_html__( 'Number of search terms to display', 'better-search' ), + 'desc' => '', + 'type' => 'number', + 'options' => '20', + 'size' => 'small', + ), + 'heatmap_smallest' => array( + 'id' => 'heatmap_smallest', + 'name' => esc_html__( 'Font size of least popular search term', 'better-search' ), + 'desc' => '', + 'type' => 'number', + 'options' => '10', + 'size' => 'small', + ), + 'heatmap_largest' => array( + 'id' => 'heatmap_largest', + 'name' => esc_html__( 'Font size of most popular search term', 'better-search' ), + 'desc' => '', + 'type' => 'number', + 'options' => '20', + 'size' => 'small', + ), + 'heatmap_cold' => array( + 'id' => 'heatmap_cold', + 'name' => esc_html__( 'Color of least popular search term', 'better-search' ), + 'desc' => '', + 'type' => 'color', + 'options' => '#cccccc', + 'field_class' => 'color-field', + 'field_attributes' => array( + 'data-default-color' => '#cccccc', + ), + ), + 'heatmap_hot' => array( + 'id' => 'heatmap_hot', + 'name' => esc_html__( 'Color of most popular search term', 'better-search' ), + 'desc' => '', + 'type' => 'color', + 'options' => '#000000', + 'field_class' => 'color-field', + 'field_attributes' => array( + 'data-default-color' => '#000000', + ), + ), + 'heatmap_before' => array( + 'id' => 'heatmap_before', + 'name' => esc_html__( 'Text to include before each search term', 'better-search' ), + 'desc' => '', + 'type' => 'text', + 'options' => '', + ), + 'heatmap_after' => array( + 'id' => 'heatmap_after', + 'name' => esc_html__( 'Text to include after each search term', 'better-search' ), + 'desc' => '', + 'type' => 'text', + 'options' => ' ', + ), + 'link_new_window' => array( + 'id' => 'link_new_window', + 'name' => esc_html__( 'Open links in new window', 'better-search' ), + 'desc' => '', + 'type' => 'checkbox', + 'options' => false, + ), + 'link_nofollow' => array( + 'id' => 'link_nofollow', + 'name' => esc_html__( 'Add nofollow to links', 'better-search' ), + 'desc' => '', + 'type' => 'checkbox', + 'options' => true, + ), + ); + + /** + * Filters the List settings array + * + * @since 2.5.0 + * + * @param array $settings List settings array + */ + return apply_filters( self::$prefix . '_settings_list', $settings ); + } + + + /** + * Retrieve the array of Styles settings + * + * @since 3.3.0 + * + * @return array Styles settings array + */ + public static function settings_styles() { + $settings = array( + 'include_styles' => array( + 'id' => 'include_styles', + 'name' => esc_html__( 'Include inbuilt styles', 'better-search' ), + 'desc' => esc_html__( 'Uncheck this to disable this plugin from adding the inbuilt styles. You will need to add your own CSS styles if you disable this option', 'better-search' ), + 'type' => 'checkbox', + 'options' => true, + ), + 'custom_css' => array( + 'id' => 'custom_css', + 'name' => esc_html__( 'Custom CSS', 'better-search' ), + /* translators: 1: Opening a tag, 2: Closing a tag, 3: Opening code tage, 4. Closing code tag. */ + 'desc' => sprintf( esc_html__( 'Do not include %3$sstyle%4$s tags. Check out the %1$sFAQ%2$s for available CSS classes to style.', 'better-search' ), '', '', '', '' ), + 'type' => 'css', + 'options' => '', + 'field_class' => 'codemirror_css', + ), + ); + + /** + * Filters the Styles settings array + * + * @since 2.5.0 + * + * @param array $settings Styles settings array + */ + return apply_filters( self::$prefix . '_settings_styles', $settings ); + } + + + /** + * Get badwords to filter. + * + * @since 2.2.0 + * + * @return array Array containing bad words to filter + */ + public static function get_badwords() { + + $badwords = array( + 'anal', + 'anus', + 'bastard', + 'beastiality', + 'bestiality', + 'bewb', + 'bitch', + 'blow', + 'blumpkin', + 'boob', + 'cawk', + 'cock', + 'choad', + 'cooter', + 'cornhole', + 'cum', + 'cunt', + 'dick', + 'dildo', + 'dong', + 'dyke', + 'douche', + 'fag', + 'faggot', + 'fart', + 'foreskin', + 'fuck', + 'fuk', + 'gangbang', + 'gook', + 'handjob', + 'homo', + 'honkey', + 'humping', + 'jiz', + 'jizz', + 'kike', + 'kunt', + 'labia', + 'muff', + 'nigger', + 'nutsack', + 'pen1s', + 'penis', + 'piss', + 'poon', + 'poop', + 'porn', + 'punani', + 'pussy', + 'queef', + 'quim', + 'rimjob', + 'rape', + 'rectal', + 'rectum', + 'semen', + 'shit', + 'slut', + 'spick', + 'spoo', + 'spooge', + 'taint', + 'titty', + 'titties', + 'twat', + 'vagina', + 'vulva', + 'wank', + 'whore', + ); + + /** + * Filters bad words array. + * + * @since 2.2.0 + * + * @param array $badwords Array containing bad words to filter. + */ + return apply_filters( self::$prefix . '_get_badwords', $badwords ); + } + + + /** + * Adding WordPress plugin action links. + * + * @since 3.3.0 + * + * @param array $links Array of links. + * @return array + */ + public function plugin_actions_links( $links ) { + + return array_merge( + array( + 'settings' => '' . esc_html__( 'Settings', 'better-search' ) . '', + ), + $links + ); + } + + /** + * Add meta links on Plugins page. + * + * @since 3.3.0 + * + * @param array $links Array of Links. + * @param string $file Current file. + * @return array + */ + public function plugin_row_meta( $links, $file ) { + + if ( false !== strpos( $file, 'better-search.php' ) ) { + $new_links = array( + 'support' => '' . esc_html__( 'Support', 'better-search' ) . '', + 'donate' => '' . esc_html__( 'Donate', 'better-search' ) . '', + 'contribute' => '' . esc_html__( 'Contribute', 'better-search' ) . '', + ); + + $links = array_merge( $links, $new_links ); + } + return $links; + } + + /** + * Get the help sidebar content to display on the plugin settings page. + * + * @since 1.8.0 + */ + public function get_help_sidebar() { + $help_sidebar = + /* translators: 1: Plugin support site link. */ + '

' . sprintf( __( 'For more information or how to get support visit the support site.', 'better-search' ), esc_url( 'https://webberzone.com/support/' ) ) . '

' . + /* translators: 1: WordPress.org support forums link. */ + '

' . sprintf( __( 'Support queries should be posted in the WordPress.org support forums.', 'better-search' ), esc_url( 'https://wordpress.org/support/plugin/better-search' ) ) . '

' . + '

' . sprintf( + /* translators: 1: Github issues link, 2: Github plugin page link. */ + __( 'Post an issue on GitHub (bug reports only).', 'better-search' ), + esc_url( 'https://github.com/WebberZone/better-search/issues' ), + esc_url( 'https://github.com/WebberZone/better-search' ) + ) . '

'; + + /** + * Filter to modify the help sidebar content. + * + * @since 3.3.0 + * + * @param string $help_sidebar Help sidebar content. + */ + return apply_filters( self::$prefix . '_settings_help', $help_sidebar ); + } + + /** + * Get the help tabs to display on the plugin settings page. + * + * @since 3.3.0 + */ + public function get_help_tabs() { + $help_tabs = array( + array( + 'id' => 'bsearch-settings-general-help', + 'title' => esc_html__( 'General', 'better-search' ), + 'content' => + '

' . __( 'This screen provides the basic settings for configuring Better Search.', 'better-search' ) . '

' . + '

' . __( 'Enable tracking, seamless mode and the cache, configure basic tracker and uninstall settings.', 'better-search' ) . '

', + ), + array( + 'id' => 'bsearch-settings-search', + 'title' => __( 'Search', 'better-search' ), + 'content' => + '

' . __( 'This screen provides settings to tweak the search algorithm.', 'better-search' ) . '

' . + '

' . __( 'Configure number of search results, enable FULLTEXT and BOOLEAN mode, tweak the weight of title and content and block words.', 'better-search' ) . '

', + ), + array( + 'id' => 'bsearch-settings-heatmap', + 'title' => __( 'Heatmap', 'better-search' ), + 'content' => + '

' . __( 'This screen provides settings to tweak the output of the search heatmap to display popular searches.', 'better-search' ) . '

' . + '

' . __( 'Configure title of the searches, period of trending searches, color and font sizes of the heatmap.', 'better-search' ) . '

', + ), + array( + 'id' => 'bsearch-settings-styles', + 'title' => __( 'Styles', 'better-search' ), + 'content' => + '

' . __( 'This screen provides options to control the look and feel of the search page.', 'better-search' ) . '

' . + '

' . __( 'Choose for default set of styles or add your own custom CSS to tweak the display of the search results page.', 'better-search' ) . '

', + ), + ); + + /** + * Filter to add more help tabs. + * + * @since 3.3.0 + * + * @param array $help_tabs Associative array of help tabs. + */ + return apply_filters( self::$prefix . '_settings_help', $help_tabs ); + } + + + /** + * Add footer text on the plugin page. + * + * @since 2.0.0 + */ + public static function get_admin_footer_text() { + return sprintf( + /* translators: 1: Opening achor tag with Plugin page link, 2: Closing anchor tag, 3: Opening anchor tag with review link. */ + __( 'Thank you for using %1$sWebberZone Better_Search%2$s! Please %3$srate us%2$s on %3$sWordPress.org%2$s', 'knowledgebase' ), + '', + '', + '' + ); + } + + /** + * Modify settings when they are being saved. + * + * @since 3.3.0 + * + * @param array $settings Settings array. + * @return array Sanitized settings array. + */ + public function change_settings_on_save( $settings ) { + + // Sanitize exclude_cat_slugs to save a new entry of exclude_categories. + if ( isset( $settings['exclude_cat_slugs'] ) ) { + + $exclude_cat_slugs = array_unique( str_getcsv( $settings['exclude_cat_slugs'] ) ); + + foreach ( $exclude_cat_slugs as $cat_name ) { + $cat = get_term_by( 'name', $cat_name, 'category' ); + + // Fall back to slugs since that was the default format before v2.4.0. + if ( false === $cat ) { + $cat = get_term_by( 'slug', $cat_name, 'category' ); + } + if ( isset( $cat->term_taxonomy_id ) ) { + $exclude_categories[] = $cat->term_taxonomy_id; + $exclude_categories_slugs[] = $cat->name; + } + } + $settings['exclude_categories'] = isset( $exclude_categories ) ? join( ',', $exclude_categories ) : ''; + $settings['exclude_cat_slugs'] = isset( $exclude_categories_slugs ) ? Helpers::str_putcsv( $exclude_categories_slugs ) : ''; + + } + + // Delete the cache. + \WebberZone\Better_Search\Util\Cache::delete(); + + return $settings; + } +} diff --git a/includes/admin/settings/js/admin-scripts.js b/includes/admin/settings/js/admin-scripts.js new file mode 100644 index 0000000..9c25d89 --- /dev/null +++ b/includes/admin/settings/js/admin-scripts.js @@ -0,0 +1,73 @@ +jQuery(document).ready(function($) { + // File browser. + $('.file-browser').on('click', function (event) { + event.preventDefault(); + + var self = $(this); + + // Create the media frame. + var file_frame = wp.media.frames.file_frame = wp.media({ + title: self.data('uploader_title'), + button: { + text: self.data('uploader_button_text'), + }, + multiple: false + }); + + file_frame.on('select', function () { + attachment = file_frame.state().get('selection').first().toJSON(); + self.prev('.file-url').val(attachment.url).change(); + }); + + // Finally, open the modal + file_frame.open(); + }); + + // Prompt the user when they leave the page without saving the form. + var formmodified=0; + + function confirmFormChange() { + formmodified=1; + } + + function confirmExit() { + if ( formmodified == 1 ) { + return true; + } + } + + function formNotModified() { + formmodified = 0; + } + + $('form *').change( confirmFormChange ); + + window.onbeforeunload = confirmExit; + + $( "input[name='submit']" ).click(formNotModified); + $( "input[id='search-submit']" ).click(formNotModified); + $( "input[id='doaction']" ).click(formNotModified); + $( "input[id='doaction2']" ).click(formNotModified); + $( "input[name='filter_action']" ).click(formNotModified); + + $( function() { + $( "#post-body-content" ).tabs({ + create: function( event, ui ) { + $( ui.tab.find("a") ).addClass( "nav-tab-active" ); + }, + activate: function( event, ui ) { + $( ui.oldTab.find("a") ).removeClass( "nav-tab-active" ); + $( ui.newTab.find("a") ).addClass( "nav-tab-active" ); + } + }); + }); + + // Initialise ColorPicker. + $( '.color-field' ).each( function ( i, element ) { + $( element ).wpColorPicker(); + }); + + $('.reset-default-thumb').click(function(){ + document.getElementById("tptn_settings[thumb_default]").value = tptn_admin.thumb_default; + }); +}); diff --git a/includes/admin/settings/js/admin-scripts.min.js b/includes/admin/settings/js/admin-scripts.min.js new file mode 100644 index 0000000..8ed6fb4 --- /dev/null +++ b/includes/admin/settings/js/admin-scripts.min.js @@ -0,0 +1 @@ +jQuery(document).ready((function(t){t(".file-browser").on("click",(function(e){e.preventDefault();var n=t(this),a=wp.media.frames.file_frame=wp.media({title:n.data("uploader_title"),button:{text:n.data("uploader_button_text")},multiple:!1});a.on("select",(function(){attachment=a.state().get("selection").first().toJSON(),n.prev(".file-url").val(attachment.url).change()})),a.open()}));var e=0;function n(){e=0}t("form *").change((function(){e=1})),window.onbeforeunload=function(){if(1==e)return!0},t("input[name='submit']").click(n),t("input[id='search-submit']").click(n),t("input[id='doaction']").click(n),t("input[id='doaction2']").click(n),t("input[name='filter_action']").click(n),t((function(){t("#post-body-content").tabs({create:function(e,n){t(n.tab.find("a")).addClass("nav-tab-active")},activate:function(e,n){t(n.oldTab.find("a")).removeClass("nav-tab-active"),t(n.newTab.find("a")).addClass("nav-tab-active")}})})),t(".color-field").each((function(e,n){t(n).wpColorPicker()})),t(".reset-default-thumb").click((function(){document.getElementById("tptn_settings[thumb_default]").value=tptn_admin.thumb_default}))})); \ No newline at end of file diff --git a/includes/admin/settings/js/apply-codemirror.js b/includes/admin/settings/js/apply-codemirror.js new file mode 100644 index 0000000..0dd2478 --- /dev/null +++ b/includes/admin/settings/js/apply-codemirror.js @@ -0,0 +1,51 @@ +jQuery(document).ready(function($) { + function confirmFormChange() { + formmodified=1; + } + + // Initialise CodeMirror. + $( ".codemirror_html" ).each( function( index, element ) { + if( $( element ).length && typeof wp.codeEditor === 'object' ) { + var editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {}; + editorSettings.codemirror = _.extend( + {}, + editorSettings.codemirror, + { + } + ); + var editor = wp.codeEditor.initialize( $( element ), editorSettings ); + editor.codemirror.on( 'change', confirmFormChange ); + } + }); + + $( ".codemirror_js" ).each( function( index, element ) { + if( $( element ).length && typeof wp.codeEditor === 'object' ) { + var editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {}; + editorSettings.codemirror = _.extend( + {}, + editorSettings.codemirror, + { + mode: 'javascript', + } + ); + var editor = wp.codeEditor.initialize( $( element ), editorSettings ); + editor.codemirror.on( 'change', confirmFormChange ); + } + }); + + $( ".codemirror_css" ).each( function( index, element ) { + if( $( element ).length && typeof wp.codeEditor === 'object' ) { + var editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {}; + editorSettings.codemirror = _.extend( + {}, + editorSettings.codemirror, + { + mode: 'css', + } + ); + var editor = wp.codeEditor.initialize( $( element ), editorSettings ); + editor.codemirror.on( 'change', confirmFormChange ); + } + }); + +}); diff --git a/includes/admin/settings/js/apply-codemirror.min.js b/includes/admin/settings/js/apply-codemirror.min.js new file mode 100644 index 0000000..54934f0 --- /dev/null +++ b/includes/admin/settings/js/apply-codemirror.min.js @@ -0,0 +1 @@ +jQuery(document).ready((function(o){function e(){formmodified=1}o(".codemirror_html").each((function(r,i){if(o(i).length&&"object"==typeof wp.codeEditor){var t=wp.codeEditor.defaultSettings?_.clone(wp.codeEditor.defaultSettings):{};t.codemirror=_.extend({},t.codemirror,{}),wp.codeEditor.initialize(o(i),t).codemirror.on("change",e)}})),o(".codemirror_js").each((function(r,i){if(o(i).length&&"object"==typeof wp.codeEditor){var t=wp.codeEditor.defaultSettings?_.clone(wp.codeEditor.defaultSettings):{};t.codemirror=_.extend({},t.codemirror,{mode:"javascript"}),wp.codeEditor.initialize(o(i),t).codemirror.on("change",e)}})),o(".codemirror_css").each((function(r,i){if(o(i).length&&"object"==typeof wp.codeEditor){var t=wp.codeEditor.defaultSettings?_.clone(wp.codeEditor.defaultSettings):{};t.codemirror=_.extend({},t.codemirror,{mode:"css"}),wp.codeEditor.initialize(o(i),t).codemirror.on("change",e)}}))})); \ No newline at end of file diff --git a/includes/admin/index.php b/includes/admin/settings/js/index.php similarity index 100% rename from includes/admin/index.php rename to includes/admin/settings/js/index.php diff --git a/includes/admin/settings/js/media-selector.js b/includes/admin/settings/js/media-selector.js new file mode 100644 index 0000000..dca2dba --- /dev/null +++ b/includes/admin/settings/js/media-selector.js @@ -0,0 +1,115 @@ +jQuery(document).ready(function ($) { + function insertString(editor, str) { + + var selection = editor.getSelection(); + + if (selection.length > 0) { + editor.replaceSelection(str); + } else { + + var doc = editor.getDoc(); + var cursor = doc.getCursor(); + + var pos = { + line: cursor.line, + ch: cursor.ch + } + + doc.replaceRange(str, pos); + + } + + } + + // Media selector. + $('.insert-codemirror-media').on('click', function (event) { + event.preventDefault(); + + var self = $(this); + var editor = $('#wp-content-editor-container .CodeMirror')[0].CodeMirror; + + function attachmentHtml(props, attachment) { + var caption = attachment.caption, + options, html; + + // If captions are disabled, clear the caption. + if (!wp.media.view.settings.captions) { + delete attachment.caption; + } + + props = wp.media.string.props(props, attachment); + + options = { + id: attachment.id, + post_content: attachment.description, + post_excerpt: caption + }; + + if (props.linkUrl) { + options.url = props.linkUrl; + } + + if ('image' === attachment.type) { + html = wp.media.string.image(props); + + _.each({ + align: 'align', + size: 'image-size', + alt: 'image_alt' + }, function (option, prop) { + if (props[prop]) { + options[option] = props[prop]; + } + }); + } else if ('video' === attachment.type) { + html = wp.media.string.video(props, attachment); + } else if ('audio' === attachment.type) { + html = wp.media.string.audio(props, attachment); + } else { + html = wp.media.string.link(props); + options.post_title = props.title; + } + + return $.ajax({ + type: 'POST', + dataType: 'json', + url: ajaxurl, + data: { + action: 'send-attachment-to-editor', + nonce: wp.media.view.settings.nonce.sendToEditor, + attachment: options, + html: html, + post_id: wp.media.view.settings.post.id + }, + success: function (response) { + //mediaHtml = response.data; + } + }); + } + + // Create the media frame. + var file_frame = wp.media.frames.file_frame = wp.media({ + frame: 'post', + state: 'insert', + multiple: true + }); + + file_frame.on('insert', function () { + var selection = file_frame.state().get('selection'); + + selection.map(function (attachment) { + + var props = file_frame.state().display(attachment).toJSON(); + + $.when(attachmentHtml(props, attachment.toJSON())).done(function (response) { + mediaHtml = response.data; + insertString(editor, mediaHtml); + }); + }); + + }); + + // Finally, open the modal + file_frame.open(); + }); +}); diff --git a/includes/admin/settings/js/media-selector.min.js b/includes/admin/settings/js/media-selector.min.js new file mode 100644 index 0000000..0d05316 --- /dev/null +++ b/includes/admin/settings/js/media-selector.min.js @@ -0,0 +1 @@ +jQuery(document).ready((function(e){e(".insert-codemirror-media").on("click",(function(t){t.preventDefault();e(this);var i=e("#wp-content-editor-container .CodeMirror")[0].CodeMirror;var n=wp.media.frames.file_frame=wp.media({frame:"post",state:"insert",multiple:!0});n.on("insert",(function(){n.state().get("selection").map((function(t){var a=n.state().display(t).toJSON();e.when(function(t,i){var n,a,o=i.caption;return wp.media.view.settings.captions||delete i.caption,t=wp.media.string.props(t,i),n={id:i.id,post_content:i.description,post_excerpt:o},t.linkUrl&&(n.url=t.linkUrl),"image"===i.type?(a=wp.media.string.image(t),_.each({align:"align",size:"image-size",alt:"image_alt"},(function(e,i){t[i]&&(n[e]=t[i])}))):"video"===i.type?a=wp.media.string.video(t,i):"audio"===i.type?a=wp.media.string.audio(t,i):(a=wp.media.string.link(t),n.post_title=t.title),e.ajax({type:"POST",dataType:"json",url:ajaxurl,data:{action:"send-attachment-to-editor",nonce:wp.media.view.settings.nonce.sendToEditor,attachment:n,html:a,post_id:wp.media.view.settings.post.id},success:function(e){}})}(a,t.toJSON())).done((function(e){mediaHtml=e.data,function(e,t){if(e.getSelection().length>0)e.replaceSelection(t);else{var i=e.getDoc(),n=i.getCursor(),a={line:n.line,ch:n.ch};i.replaceRange(t,a)}}(i,mediaHtml)}))}))})),n.open()}))})); \ No newline at end of file diff --git a/includes/admin/settings/js/taxonomy-suggest.js b/includes/admin/settings/js/taxonomy-suggest.js new file mode 100644 index 0000000..31a07a1 --- /dev/null +++ b/includes/admin/settings/js/taxonomy-suggest.js @@ -0,0 +1,112 @@ +jQuery(document).ready(function($) { + // Function to add auto suggest. + $.fn.WZTagsSuggest = function( options ) { + var cache; + var last; + var $element = $( this ); + + options = options || {}; + + var taxonomy = options.taxonomy || $element.attr( 'data-wp-taxonomy' ) || 'category'; + var tag_search = options.tag_search || $element.attr( 'data-wp-action' ) || 'ata_tag_search'; + delete( options.taxonomy ); + delete( options.tag_search ); + + function split( val ) { + return val.split( /,(?=(?:(?:[^"]*"){2})*[^"]*$)/ ); // Split typical CSV format, with commas and double quotes. + } + + function extractLast( term ) { + return split( term ).pop(); + } + + options = $.extend({ + minLength: 2, + position: { + my: 'left top+2', + at: 'left bottom', + collision: 'none' + }, + source: function( request, response ) { + var term; + + if ( last === request.term ) { + response( cache ); + return; + } + + term = extractLast( request.term ); + + if ( last === request.term ) { + response( cache ); + return; + } + + $.ajax({ + type: 'POST', + dataType: 'json', + url: ajaxurl, + data: { + action: tag_search, + tax: taxonomy, + q: term + }, + }).done( function( data ) { + cache = data; + response( data ); + }); + + last = request.term; + + }, + search: function() { + // Custom minLength. + var term = extractLast( this.value ); + + if ( term.length < 2 ) { + return false; + } + }, + focus: function( event, ui ) { + // Prevent value inserted on focus. + event.preventDefault(); + }, + select: function( event, ui ) { + var terms = split( this.value ); + var val = ui.item.value; + + if ( val.indexOf(',') !== -1 ) { + val = '"' + val + '"' + } + + // Remove the last user input. + terms.pop(); + + // Add the selected item. + terms.push( val ); + + // Add placeholder to get the comma-and-space at the end. + terms.push( "" ); + this.value = terms.join( ", " ); + return false; + } + }, options ); + + $element.on( "keydown", function( event ) { + // Don't navigate away from the field on tab when selecting an item. + if ( event.keyCode === $.ui.keyCode.TAB && + $( this ).autocomplete( 'instance' ).menu.active ) { + event.preventDefault(); + } + }) + .autocomplete( options ); + }; + + $( '.category_autocomplete' ).each( function ( i, element ) { + $( element ).WZTagsSuggest(); + }); + + $('.widget-liquid-right, #customize-controls').on( 'click', '.category_autocomplete', function() { + $( '.category_autocomplete' ).WZTagsSuggest(); + }); +}); diff --git a/includes/admin/settings/js/taxonomy-suggest.min.js b/includes/admin/settings/js/taxonomy-suggest.min.js new file mode 100644 index 0000000..eb9b0c2 --- /dev/null +++ b/includes/admin/settings/js/taxonomy-suggest.min.js @@ -0,0 +1 @@ +jQuery(document).ready((function(t){t.fn.WZTagsSuggest=function(e){var o,n,a=t(this),u=(e=e||{}).taxonomy||a.attr("data-wp-taxonomy")||"category",c=e.tag_search||a.attr("data-wp-action")||"ata_tag_search";function i(t){return t.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)}function r(t){return i(t).pop()}delete e.taxonomy,delete e.tag_search,e=t.extend({minLength:2,position:{my:"left top+2",at:"left bottom",collision:"none"},source:function(e,a){var i;n!==e.term?(i=r(e.term),n!==e.term?(t.ajax({type:"POST",dataType:"json",url:ajaxurl,data:{action:c,tax:u,q:i}}).done((function(t){o=t,a(t)})),n=e.term):a(o)):a(o)},search:function(){if(r(this.value).length<2)return!1},focus:function(t,e){t.preventDefault()},select:function(t,e){var o=i(this.value),n=e.item.value;return-1!==n.indexOf(",")&&(n='"'+n+'"'),o.pop(),o.push(n),o.push(""),this.value=o.join(", "),!1}},e),a.on("keydown",(function(e){e.keyCode===t.ui.keyCode.TAB&&t(this).autocomplete("instance").menu.active&&e.preventDefault()})).autocomplete(e)},t(".category_autocomplete").each((function(e,o){t(o).WZTagsSuggest()})),t(".widget-liquid-right, #customize-controls").on("click",".category_autocomplete",(function(){t(".category_autocomplete").WZTagsSuggest()}))})); \ No newline at end of file diff --git a/includes/admin/settings/sidebar.php b/includes/admin/settings/sidebar.php new file mode 100644 index 0000000..4affa5d --- /dev/null +++ b/includes/admin/settings/sidebar.php @@ -0,0 +1,79 @@ + +
+
+

+ +
+ +
+
+ +
+

+ +
+ +
+
+
+ +
+
+

+ +
+ + +
+
+
\ No newline at end of file diff --git a/includes/admin/sidebar.php b/includes/admin/sidebar.php deleted file mode 100644 index 4b7d985..0000000 --- a/includes/admin/sidebar.php +++ /dev/null @@ -1,102 +0,0 @@ - -
-
-

-
- - -
- -
- - -
-

-
- -
- -
- -
- -
-
-

-
- - -
- -
- -
diff --git a/includes/admin/tools.php b/includes/admin/tools.php deleted file mode 100644 index e5864bc..0000000 --- a/includes/admin/tools.php +++ /dev/null @@ -1,309 +0,0 @@ - -
-

- - - -
-
-
- -
- -

-

- -

-

- -

- -

-

- -

-

- -

-

-

- ALTER TABLE posts ); ?> DROP INDEX bsearch; - ALTER TABLE posts ); ?> DROP INDEX bsearch_title; - ALTER TABLE posts ); ?> DROP INDEX bsearch_content; - ALTER TABLE posts ); ?> ADD FULLTEXT bsearch_related (post_title, post_content); - ALTER TABLE posts ); ?> ADD FULLTEXT bsearch_related_title (post_title); - ALTER TABLE posts ); ?> ADD FULLTEXT bsearch_related_content (post_content); -

- -

-

- - -

-

- -

- - -
- -
- -

-

- -

-

-

- -

- - -
- -
- -

- -

-

- -

-

- -

- - - -
- -
-

-

- -

-

- -

- - -
- -
- -
- -
- -
- -
-
-
-
- -
- - prefix . 'bsearch_daily' : $wpdb->prefix . 'bsearch'; - - $sql = "TRUNCATE TABLE $table_name"; - $wpdb->query( $sql ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared -} - - -/** - * Recreate FULLTEXT indices. - * - * @since 2.2.0 - */ -function bsearch_recreate_index() { - - global $wpdb; - - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' DROP INDEX bsearch' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' DROP INDEX bsearch_title' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' DROP INDEX bsearch_content' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery - - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch (post_title, post_content);' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch_title (post_title);' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery - $wpdb->query( 'ALTER TABLE ' . $wpdb->posts . ' ADD FULLTEXT bsearch_content (post_content);' ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery -} - - -/** - * Process a settings export that generates a .json file of the shop settings - * - * @since 2.5.0 - */ -function bsearch_process_settings_export() { - - if ( empty( $_POST['bsearch_action'] ) || 'export_settings' !== $_POST['bsearch_action'] ) { - return; - } - - if ( ! isset( $_POST['bsearch_export_settings_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['bsearch_export_settings_nonce'] ), 'bsearch_export_settings_nonce' ) ) { - return; - } - - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - $settings = get_option( 'bsearch_settings' ); - - ignore_user_abort( true ); - - nocache_headers(); - header( 'Content-Type: application/json; charset=utf-8' ); - header( 'Content-Disposition: attachment; filename=bsearch-settings-export-' . gmdate( 'm-d-Y' ) . '.json' ); - header( 'Expires: 0' ); - - echo wp_json_encode( $settings ); - exit; -} -add_action( 'admin_init', 'bsearch_process_settings_export' ); - - -/** - * Process a settings import from a json file - * - * @since 2.5.0 - */ -function bsearch_process_settings_import() { - - if ( empty( $_POST['bsearch_action'] ) || 'import_settings' !== $_POST['bsearch_action'] ) { - return; - } - - if ( ! isset( $_POST['bsearch_import_settings_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['bsearch_import_settings_nonce'] ), 'bsearch_import_settings_nonce' ) ) { - return; - } - - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - $filename = 'import_settings_file'; - $extension = isset( $_FILES[ $filename ]['name'] ) ? end( explode( '.', sanitize_file_name( wp_unslash( $_FILES[ $filename ]['name'] ) ) ) ) : ''; - - if ( 'json' !== $extension ) { - wp_die( esc_html__( 'Please upload a valid .json file', 'better-search' ) ); - } - - $import_file = isset( $_FILES[ $filename ]['tmp_name'] ) ? ( wp_unslash( $_FILES[ $filename ]['tmp_name'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - if ( empty( $import_file ) ) { - wp_die( esc_html__( 'Please upload a file to import', 'better-search' ) ); - } - - // Retrieve the settings from the file and convert the json object to an array. - $settings = (array) json_decode( file_get_contents( $import_file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents - - update_option( 'bsearch_settings', $settings ); - - wp_safe_redirect( - add_query_arg( - array( - 'page' => 'bsearch_tools_page', - 'settings_import' => 'success', - ), - admin_url( 'admin.php' ) - ) - ); - exit; -} -add_action( 'admin_init', 'bsearch_process_settings_import', 9 ); - - -/** - * Remove query variables. - * - * @since 2.5.0 - * - * @param array $args Query arguments. - * @return array Query arguments. - */ -function bsearch_add_removable_arg( $args ) { - array_push( $args, 'settings_import' ); - return $args; -} - -add_filter( 'removable_query_args', 'bsearch_add_removable_arg' ); diff --git a/includes/autoloader.php b/includes/autoloader.php new file mode 100644 index 0000000..3c0db98 --- /dev/null +++ b/includes/autoloader.php @@ -0,0 +1,47 @@ +query_args = apply_filters_ref_array( 'better_search_query_args', array( $args, &$this ) ); } @@ -876,7 +882,7 @@ public function posts_pre_query( $posts, $query ) { } } $query->found_posts = isset( $cached_data['found_posts'] ) ? $cached_data['found_posts'] : count( $posts ); - $query->max_num_pages = ceil( $query->found_posts / $query->get( 'posts_per_page' ) ); + $query->max_num_pages = intval( ceil( $query->found_posts / $query->get( 'posts_per_page' ) ) ); $this->in_cache = true; } } @@ -1024,7 +1030,7 @@ public function get_cache_key( $query, $context = 'query' ) { $cache_attr['paged'] = $query->query_vars['paged']; } - return bsearch_cache_get_key( $cache_attr, $context ); + return Cache::get_key( $cache_attr, $context ); } /** @@ -1126,17 +1132,3 @@ protected function get_search_stopwords() { } endif; - -/** - * Load Better Search function. - * - * @since 3.0.0 - * - * @param WP_Query $query The WP_Query instance (passed by reference). - */ -function bsearch_load_plugin( $query ) { - if ( $query->is_search() && bsearch_get_option( 'seamless' ) ) { - new Better_Search(); - } -} -add_action( 'parse_query', 'bsearch_load_plugin' ); diff --git a/includes/class-main.php b/includes/class-main.php new file mode 100644 index 0000000..712674c --- /dev/null +++ b/includes/class-main.php @@ -0,0 +1,276 @@ +init(); + } + + return self::$instance; + } + + /** + * A dummy constructor. + * + * @since 3.3.0 + */ + private function __construct() { + // Do nothing. + } + + /** + * Initializes the plugin. + * + * @since 3.3.0 + */ + private function init() { + $this->language = new Frontend\Language_Handler(); + $this->styles = new Frontend\Styles_Handler(); + $this->tracker = new Tracker(); + $this->shortcodes = new Frontend\Shortcodes(); + $this->display = new Frontend\Display(); + + $this->hooks(); + + if ( is_admin() ) { + $this->admin = new Admin\Admin(); + } + } + + /** + * Run the hooks. + * + * @since 3.3.0 + */ + public function hooks() { + add_action( 'init', array( $this, 'initiate_plugin' ) ); + add_action( 'widgets_init', array( $this, 'register_widgets' ) ); + add_action( 'wp_head', array( $this, 'wp_head' ) ); + add_action( 'parse_query', array( $this, 'load_seamless_mode' ) ); + add_filter( 'template_include', array( $this, 'template_include' ) ); + } + + /** + * Initialise the plugin translations and media. + * + * @since 3.3.0 + */ + public function initiate_plugin() { + Frontend\Media_Handler::add_image_sizes(); + } + + /** + * Initialise the Top 10 widgets. + * + * @since 3.3.0 + */ + public function register_widgets() { + register_widget( '\WebberZone\Better_Search\Frontend\Widgets\Search_Box' ); + register_widget( '\WebberZone\Better_Search\Frontend\Widgets\Search_Heatmap' ); + } + + /** + * Load seamless mode. + * + * @since 3.3.0 + * + * @param \WP_Query $query Query object. + */ + public function load_seamless_mode( $query ) { + if ( $query->is_search() && bsearch_get_option( 'seamless' ) ) { + new \Better_Search(); + } + } + + /** + * Displays the search results + * First checks if the theme contains a search template and uses that + * If search template is missing, generates the results below + * + * @since 3.3.0 + * + * @param string $template Search template to use. + */ + public function template_include( $template ) { + // Early return if not a search page. + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + if ( false === stripos( $request_uri, '?s=' ) + && false === stripos( $request_uri, '/search/' ) + && ! is_search() ) { + return $template; + } + + global $wp_query; + + // Early return if seamless integration mode is activated. + if ( bsearch_get_option( 'seamless' ) ) { + return $template; + } + + // If we have a 404 status, set status of 404 to false. + if ( $wp_query->is_404 ) { + $wp_query->is_404 = false; + $wp_query->is_archive = true; + } + + // Change status code to 200 OK since /search/ returns status code 404. + status_header( 200 ); + + // Add necessary code to the head. + add_action( 'wp_head', array( $this, 'wp_head' ) ); + + // Set the title. + add_filter( 'pre_get_document_title', array( $this, 'document_title' ) ); + + // Check for a template file within the parent or child theme. + $template_paths = array( + get_stylesheet_directory() . '/better-search-template.php', + get_template_directory() . '/better-search-template.php', + plugin_dir_path( __DIR__ ) . 'templates/template.php', + ); + + foreach ( $template_paths as $template_path ) { + if ( file_exists( $template_path ) ) { + return $template_path; + } + } + + return $template; + } + + /** + * Insert styles into WordPress Head. Filters `wp_head`. + * + * @since 1.0 + */ + public static function wp_head() { + + if ( is_search() ) { + // Add noindex to search results page. + if ( bsearch_get_option( 'meta_noindex' ) ) { + echo ''; + } + } + } + + + /** + * Change page title. Filters `wp_title`. + * + * @since 1.0 + * + * @param string $title Title of the page. + * @return string Filtered title of the page + */ + public static function document_title( $title ) { + + if ( ! is_search() ) { + return $title; + } + + $search_query = get_bsearch_query(); + + if ( $search_query ) { + /* translators: 1: search query, 2: title of the page */ + $bsearch_title = sprintf( __( 'Search Results for "%1$s" | %2$s', 'better-search' ), $search_query, $title ); + + /** + * Filters the title of the page + * + * @since 2.0.0 + * + * @param string $bsearch_title Title of the page set by Better Search + * @param string $title Original Title of the page + * @param string $search_query Search query + */ + return apply_filters( 'bsearch_title', $bsearch_title, $title, $search_query ); + } + + return $title; + } +} diff --git a/includes/class-tracker.php b/includes/class-tracker.php new file mode 100644 index 0000000..e9f1b67 --- /dev/null +++ b/includes/class-tracker.php @@ -0,0 +1,247 @@ + $home_url, + 'bsearch_search_query' => $search_query, + 'bsearch_rnd' => wp_rand( 1, time() ), + ); + + /** + * Filter the localize script arguments for the Better Search tracker. + * + * @since 2.2.4 + */ + $ajax_bsearch_tracker = apply_filters( 'bsearch_tracker_script_args', $ajax_bsearch_tracker ); + + wp_enqueue_script( + 'bsearch_tracker', + plugins_url( 'includes/js/better-search-tracker.min.js', BETTER_SEARCH_PLUGIN_FILE ), + array(), + BETTER_SEARCH_VERSION, + true + ); + + wp_localize_script( 'bsearch_tracker', 'ajax_bsearch_tracker', $ajax_bsearch_tracker ); + + } + } + + /** + * Function to add additional queries to query_vars. + * + * @since 2.0.0 + * + * @param array $vars Query variables array. + * @return array Query variables array with Top 10 parameters appended + */ + public static function query_vars( $vars ) { + // Add these to the list of queryvars that WP gathers. + $vars[] = 'bsearch_search_query'; + + /** + * Function to add additional queries to query_vars. + * + * @since 2.2.4 + * + * @param array $vars Updated Query variables array with Better Search queries added. + */ + return apply_filters( 'bsearch_query_vars', $vars ); + } + + /** + * Parses the WordPress object to update/display the count. + * + * @since 2.0.0 + * + * @param \WP $wp Current WordPress environment instance. + */ + public static function parse_request( $wp ) { + + if ( empty( $wp ) ) { + global $wp; + } + + if ( ! isset( $wp->query_vars ) || ! is_array( $wp->query_vars ) ) { + return; + } + + if ( array_key_exists( 'bsearch_search_query', $wp->query_vars ) && empty( $wp->query_vars['bsearch_search_query'] ) ) { + exit; + } + + if ( array_key_exists( 'bsearch_search_query', $wp->query_vars ) && ! empty( $wp->query_vars['bsearch_search_query'] ) ) { + + $search_query = rawurldecode( wp_kses( wp_unslash( $wp->query_vars['bsearch_search_query'] ), array() ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + $str = self::update_count( $search_query ); + + // If the debug parameter is set then we output $str else we send a No Content header. + if ( array_key_exists( 'bsearch_debug', $wp->query_vars ) && 1 === absint( $wp->query_vars['bsearch_debug'] ) ) { + header( 'content-type: application/x-javascript' ); + wp_send_json( $str ); + } else { + header( 'HTTP/1.0 204 No Content' ); + header( 'Cache-Control: max-age=15, s-maxage=0' ); + } + + // Stop anything else from loading as it is not needed. + exit; + + } else { + return; + } + } + + /** + * Parse the ajax response. + * + * @since 2.4.0 + */ + public static function tracker_parser() { + + $search_query = isset( $_POST['bsearch_search_query'] ) ? rawurldecode( wp_kses( wp_unslash( $_POST['bsearch_search_query'] ), array() ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing + $debug = isset( $_POST['bsearch_debug'] ) ? absint( $_POST['bsearch_debug'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing + + $str = self::update_count( $search_query ); + + // If the debug parameter is set then we output $str else we send a No Content header. + if ( 1 === $debug ) { + echo esc_html( $str ); + } else { + header( 'HTTP/1.0 204 No Content' ); + header( 'Cache-Control: max-age=15, s-maxage=0' ); + } + + wp_die(); + } + + /** + * Function to update the count in the database. + * + * @since 3.3.0 + * + * @param string $search_query Search Query. + * @return string Response on database update. + */ + public static function update_count( $search_query ) { + + global $wpdb; + + $table_name = $wpdb->prefix . 'bsearch'; + $table_name_daily = $wpdb->prefix . 'bsearch_daily'; + $search_query = str_replace( '"', '"', $search_query ); + $str = ''; + + if ( '' !== $search_query ) { + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "INSERT INTO $table_name (searchvar, cntaccess) VALUES (%s, 1) ON DUPLICATE KEY UPDATE cntaccess = cntaccess + 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $search_query + ) + ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + + // Now update daily count. + $current_date = gmdate( 'Y-m-d', ( time() + ( get_option( 'gmt_offset' ) * 3600 ) ) ); + + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "INSERT INTO $table_name_daily (searchvar, cntaccess, dp_date) VALUES (%s, 1, %s) ON DUPLICATE KEY UPDATE cntaccess = cntaccess + 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $search_query, + $current_date + ) + ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + } + + /** + * Filter the response on database update. + * + * @since 2.2.4 + * + * @param string $str Response string. + * @param string $search_query Search query. + */ + return apply_filters( 'bsearch_update_count', $str, $search_query ); + } +} diff --git a/includes/css/bsearch-styles.css b/includes/css/bsearch-styles.css index 1761570..0929f56 100644 --- a/includes/css/bsearch-styles.css +++ b/includes/css/bsearch-styles.css @@ -19,10 +19,7 @@ flex-direction: row; flex-flow: row wrap; justify-content: space-between; -} - -.bsearchform div { - padding: 2px; + gap: 10px; } .bsearchform .bsearch-form-search-field { diff --git a/includes/css/bsearch-styles.min.css b/includes/css/bsearch-styles.min.css index d014e8b..69dfb78 100644 --- a/includes/css/bsearch-styles.min.css +++ b/includes/css/bsearch-styles.min.css @@ -1 +1 @@ -.bsearch_results_page{max-width:90%;margin:20px;padding:20px}.bsearch_thumb_wrapper img{max-width:100%}.bsearch-form-container{text-align:center;margin:10px auto}.bsearchform{display:flex;flex-wrap:wrap;flex-direction:row;flex-flow:row wrap;justify-content:space-between}.bsearchform div{padding:2px}.bsearchform .bsearch-form-search-field{flex:auto}.bsearchform input[type=search],.bsearchform select{background-color:#fff;border:thin solid #999;display:inline-block;font:inherit;line-height:1.5em;padding:.5em 3.5em .5em 1em;width:100%;height:100%}.bsearchform select{background-image:linear-gradient(45deg,transparent 50%,gray 50%),linear-gradient(135deg,gray 50%,transparent 50%),linear-gradient(to right,#ccc,#ccc);background-position:calc(100% - 20px) calc(1em + 2px),calc(100% - 15px) calc(1em + 2px),calc(100% - 2.5em) .5em;background-size:5px 5px,5px 5px,1px 1.5em;background-repeat:no-repeat;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;appearance:none;-webkit-appearance:none;-moz-appearance:none}.bsearch_footer{text-align:center}.bsearch_highlight{background:#ffc}.bsearch-post{margin:30px auto}ul.bsearch_post_meta{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap}ul.bsearch_post_meta li{flex:auto;list-style-type:none;padding:2px;margin:0;text-align:left}.bsearch-entry-title{text-align:left}@media all and (max-width:600px){.bsearchform,ul.bsearch_post_meta{flex-direction:column}} \ No newline at end of file +.bsearch_results_page{max-width:90%;margin:20px;padding:20px;}.bsearch_thumb_wrapper img{max-width:100%;}.bsearch-form-container{text-align:center;margin:10px auto;}.bsearchform{display:flex;flex-wrap:wrap;flex-direction:row;flex-flow:row wrap;justify-content:space-between;gap:10px;}.bsearchform .bsearch-form-search-field{flex:auto;}.bsearchform select,.bsearchform input[type="search"]{background-color:#fff;border:thin solid #999;display:inline-block;font:inherit;line-height:1.5em;padding:.5em 3.5em .5em 1em;width:100%;height:100%;}.bsearchform select{background-image:linear-gradient(45deg,transparent 50%,gray 50%),linear-gradient(135deg,gray 50%,transparent 50%),linear-gradient(to right,#ccc,#ccc);background-position:calc(100% - 20px) calc(1em + 2px),calc(100% - 15px) calc(1em + 2px),calc(100% - 2.5em) .5em;background-size:5px 5px,5px 5px,1px 1.5em;background-repeat:no-repeat;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;appearance:none;-webkit-appearance:none;-moz-appearance:none;}.bsearch_footer{text-align:center;}.bsearch_highlight{background:#ffc;}.bsearch-post{margin:30px auto;}ul.bsearch_post_meta{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;}ul.bsearch_post_meta li{flex:auto;list-style-type:none;padding:2px;margin:0;text-align:left;}.bsearch-entry-title{text-align:left;}@media all and (max-width:600px){ul.bsearch_post_meta,.bsearchform{flex-direction:column}} \ No newline at end of file diff --git a/includes/deprecated.php b/includes/deprecated.php deleted file mode 100644 index 6df8a89..0000000 --- a/includes/deprecated.php +++ /dev/null @@ -1,1224 +0,0 @@ -Popular Searches', 'better-search' ); - $title_daily = __( '

Weekly Popular Searches

', 'better-search' ); - - // Get relevant post types. - $args = array( - 'public' => true, - '_builtin' => true, - ); - $post_types = http_build_query( get_post_types( $args ), '', '&' ); - - $custom_css = ' -#bsearchform { margin: 20px; padding: 20px; } -#heatmap { margin: 20px; padding: 20px; border: 1px dashed #ccc } -.bsearch_results_page { max-width:90%; margin: 20px; padding: 20px; } -.bsearch_footer { text-align: center; } -.bsearch_highlight { background:#ffc; } - '; - - $badwords = array( - 'anal', - 'anus', - 'bastard', - 'beastiality', - 'bestiality', - 'bewb', - 'bitch', - 'blow', - 'blumpkin', - 'boob', - 'cawk', - 'cock', - 'choad', - 'cooter', - 'cornhole', - 'cum', - 'cunt', - 'dick', - 'dildo', - 'dong', - 'dyke', - 'douche', - 'fag', - 'faggot', - 'fart', - 'foreskin', - 'fuck', - 'fuk', - 'gangbang', - 'gook', - 'handjob', - 'homo', - 'honkey', - 'humping', - 'jiz', - 'jizz', - 'kike', - 'kunt', - 'labia', - 'muff', - 'nigger', - 'nutsack', - 'pen1s', - 'penis', - 'piss', - 'poon', - 'poop', - 'porn', - 'punani', - 'pussy', - 'queef', - 'queer', - 'quim', - 'rimjob', - 'rape', - 'rectal', - 'rectum', - 'semen', - 'shit', - 'slut', - 'spick', - 'spoo', - 'spooge', - 'taint', - 'titty', - 'titties', - 'twat', - 'vagina', - 'vulva', - 'wank', - 'whore', - ); - - $bsearch_settings = array( - - /* General options */ - 'seamless' => true, // Seamless integration mode. - 'track_popular' => true, // Track the popular searches. - 'track_admins' => true, // Track Admin searches. - 'track_editors' => true, // Track Editor searches. - 'cache' => true, // Enable Cache. - 'meta_noindex' => true, // Add noindex,follow meta tag to head. - 'show_credit' => false, // Add link to plugin page of my blog in top posts list. - - /* Search options */ - 'limit' => '10', // Search results per page. - 'post_types' => $post_types, // WordPress custom post types. - - 'use_fulltext' => true, // Full text searches. - 'weight_content' => '10', // Weightage for content. - 'weight_title' => '1', // Weightage for title. - 'boolean_mode' => false, // Turn BOOLEAN mode on if true. - - 'highlight' => false, // Highlight search terms. - 'excerpt_length' => '100', // Length of excerpt in words. - 'include_thumb' => false, // Include thumbnail in search results. - 'link_new_window' => false, // Open link in new window - Includes target="_blank" to links. - 'link_nofollow' => true, // Includes rel="nofollow" to links in heatmap. - - 'badwords' => implode( ',', $badwords ), // Bad words filter. - - /* Heatmap options */ - 'include_heatmap' => false, // Include heatmap of searches in the search page. - 'title' => $title, // Title of Search Heatmap. - 'title_daily' => $title_daily, // Title of Daily Search Heatmap. - 'daily_range' => '7', // Daily Popular will contain posts of how many days? - - 'heatmap_limit' => '30', // Heatmap - Maximum number of searches to display in heatmap. - 'heatmap_smallest' => '10', // Heatmap - Smallest Font Size. - 'heatmap_largest' => '20', // Heatmap - Largest Font Size. - 'heatmap_unit' => 'pt', // Heatmap - We'll use pt for font size. - 'heatmap_cold' => 'CCCCCC', // Heatmap - cold searches. - 'heatmap_hot' => '000000', // Heatmap - hot searches. - 'heatmap_before' => '', // Heatmap - Display before each search term. - 'heatmap_after' => ' ', // Heatmap - Display after each search term. - - /* Custom styles */ - 'custom_CSS' => $custom_css, // Custom CSS. - - ); - - /* - * Filters default options for Better Search - * - * @since 2.0.0 - * - * @param array $bsearch_settings default options - */ - return apply_filters( 'bsearch_default_options', $bsearch_settings ); -} - - -/** - * Function to read options from the database. - * - * @since 1.0 - * @deprecated 3.0.0 - * - * @return array Better Search options array - */ -function bsearch_read_options() { - - _deprecated_function( __FUNCTION__, '2.2.0', 'bsearch_get_settings()' ); - - return bsearch_get_settings(); -} - - -/** - * Fetches the search results for the current search query and returns a comma separated string of IDs. - * - * @since 1.3.3 - * - * @deprecated 2.2.0 - * - * @return string Blank string or comma separated string of search results' IDs - */ -function bsearch_clause_prepare() { - global $wp_query, $wpdb; - - _deprecated_function( __FUNCTION__, '2.2.0' ); - - $search_ids = ''; - - if ( $wp_query->is_search ) { - $search_query = get_bsearch_query(); - - $matches = get_bsearch_matches( $search_query, 0 ); // Fetch the search results for the search term stored in $search_query. - - $searches = $matches[0]; // 0 index contains the search results always - - if ( $searches ) { - $search_ids = implode( ',', wp_list_pluck( $searches, 'ID' ) ); - } - } - - /** - * Filters the string of SEARCH IDs returned - * - * @since 2.0.0 - * - * @return string $search_ids Blank string or comma separated string of search results' IDs - */ - return apply_filters( 'bsearch_clause_prepare', $search_ids ); -} - - -/** - * Function to update search count. - * - * @since 1.0 - * @deprecated 2.2.4 - * - * @param string $search_query Search query. - * @return string Search tracker code - */ -function bsearch_increment_counter( $search_query ) { - - _deprecated_function( __FUNCTION__, '2.2.4' ); - - $output = ''; - - /** - * Filter the search tracker code - * - * @since 2.0.0 - * - * @param string $output Formatted output string - * @param string $search_query Search query - */ - return apply_filters( 'bsearch_increment_counter', $output, $search_query ); -} - -/** - * Function to return the header links of the results page. - * - * @since 1.2 - * @deprecated 3.0.0 - * - * @param string $search_query Search string. - * @param int $numrows Total number of results. - * @param int $limit Results per page. - * @return string Formatted header table of search results pages - */ -function get_bsearch_header( $search_query, $numrows, $limit ) { - - _deprecated_function( __FUNCTION__, '3.0.0', 'the_bsearch_header' ); - - $args = array( - 'echo' => false, - 'limit' => $limit, - 'found_posts' => $numrows, - 'search_query' => $search_query, - ); - - return the_bsearch_header( $args ); -} - - -/** - * Function to return the footer links of the results page. - * - * @since 1.2 - * @deprecated 3.0.0 - * - * @param string $search_query Search string. - * @param int $numrows Total results. - * @param int $limit Results per page. - * @return string Formatted footer of search results pages - */ -function get_bsearch_footer( $search_query, $numrows, $limit ) { - - _deprecated_function( __FUNCTION__, '3.0.0', 'get_the_posts_pagination' ); - - $args = array( - 'mid_size' => 3, - 'prev_text' => esc_html__( '« Previous', 'better-search' ), - 'next_text' => esc_html__( 'Next »', 'better-search' ), - ); - - return get_the_posts_pagination( $args ); -} - - -/** - * Function to convert the mySQL score to percentage. - * - * @since 1.2 - * @deprecated 3.0.0 - * - * @param object $search Search result object. - * @param int $score Score for the search result. - * @param int $topscore Score for the most relevant search result. - * @return int Score converted to percentage - */ -function get_bsearch_score( $search, $score, $topscore ) { - - _deprecated_function( __FUNCTION__, '3.0.0', 'the_bsearch_score' ); - - $args = array( - 'score' => $score, - 'topscore' => $topscore, - 'echo' => false, - ); - - return the_bsearch_score( $args ); -} - -/** - * Gets the search results. - * - * @since 1.2 - * @deprecated 3.0.0 - * - * @param string $search_query Search term. - * @param int|string $limit Maximum number of search results. - * @return string Search results - */ -function get_bsearch_results( $search_query = '', $limit = '' ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $bsearch_error; - - if ( ! ( $limit ) ) { - $limit = isset( $_GET['limit'] ) ? intval( $_GET['limit'] ) : bsearch_get_option( 'limit' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - } - - // Order by date or by score? - $bydate = isset( $_GET['bydate'] ) ? intval( $_GET['bydate'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - $topscore = 0; - - $matches = get_bsearch_matches( $search_query, $bydate ); // Fetch the search results for the search term stored in $search_query. - $searches = $matches[0]; // 0 index contains the search results always. - - if ( $searches ) { - $topscore = max( wp_list_pluck( (array) $searches, 'score' ) ); - $numrows = count( $searches ); - } else { - $numrows = 1; - } - - $match_range = get_bsearch_range( $numrows, $limit ); - $searches = array_slice( $searches, $match_range[0], $match_range[1] - $match_range[0] + 1 ); // Extract the elements for the page from the complete results array. - - $output = ''; - - /* Lets start printing the results */ - if ( '' !== $search_query ) { - if ( $searches ) { - $output .= get_bsearch_header( $search_query, $numrows, $limit ); - - $search_query = preg_quote( $search_query, '/' ); - $keys = explode( ' ', str_replace( array( "'", '"', '"', '\+', '\-' ), '', $search_query ) ); - - foreach ( $searches as $search ) { - $score = $search->score; - $search = get_post( $search->ID ); - $post_title = get_the_title( $search->ID ); - - /* Highlight the search terms in the title */ - if ( bsearch_get_option( 'highlight' ) ) { - $post_title = bsearch_highlight( $post_title, $keys ); - } - - $output .= '
ID ) ) . '"'; - $output .= '>'; - - $output .= '
'; - - $output .= sprintf( '

%2$s

', esc_url( get_permalink( $search->ID ) ), $post_title ); - - $output .= sprintf( '

%1$s      %2$s

', get_bsearch_score( $search, $score, $topscore ), get_bsearch_date( $search, __( 'Posted on: ', 'better-search' ) ) ); - - $output .= '
'; - - $output .= '
'; - - if ( bsearch_get_option( 'include_thumb' ) ) { - $output .= '

'; - $output .= bsearch_get_the_post_thumbnail( - array( - 'post' => $search, - 'size' => 'thumbnail', - ) - ); - $output .= '

'; - } - - $excerpt = get_bsearch_excerpt( $search->ID, bsearch_get_option( 'excerpt_length' ) ); - - /* Highlight the search terms in the excerpt */ - if ( bsearch_get_option( 'highlight' ) ) { - $excerpt = bsearch_highlight( $excerpt, $keys ); - } - - $output .= sprintf( '

%1$s

', $excerpt ); - - $output .= '
'; - $output .= '
'; - } //end of foreach loop - - $output .= get_bsearch_footer( $search_query, $numrows, $limit ); - - } else { - $output .= '

'; - $output .= __( 'No results.', 'better-search' ); - $output .= '

'; - } - } else { - $output .= '

'; - if ( '' !== $bsearch_error->get_error_message( 'bsearch_banned' ) && bsearch_get_option( 'banned_stop_search' ) ) { - foreach ( $bsearch_error->get_error_messages() as $error ) { - $output .= $error . '
'; - } - } else { - $output .= __( 'Please type in your search terms. Use descriptive words since this search is intelligent.', 'better-search' ); - } - $output .= '

'; - } - - if ( bsearch_get_option( 'show_credit' ) ) { - $output .= bsearch_get_credit_link(); - } - - /** - * Filter formatted string with search results - * - * @since 1.2 - * - * @param string $output Formatted results - * @param string $search_query Search query - * @param int $limit Number of results per page - */ - return apply_filters( 'get_bsearch_results', $output, $search_query, $limit ); -} - - -/** - * Get the matches for the search term. - * - * @since 1.2 - * @deprecated 3.0.0 - * - * @param string $search_query Search terms array. - * @param bool $bydate Sort by date flag. - * @return array Search results - */ -function get_bsearch_matches( $search_query, $bydate ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb, $bsearch_error; - - // if there are two items in $search_info, the string has been broken into separate terms that - // are listed at $search_info[1]. The cleaned-up version of $search_query is still at the zero index. - // This is when fulltext is disabled, and we search using LIKE. - $search_info = get_bsearch_terms( $search_query ); - - if ( '' !== $bsearch_error->get_error_message( 'bsearch_banned' ) && bsearch_get_option( 'banned_stop_search' ) ) { - $matches[0] = array(); - $matches['search_query'] = $search_query; - - return $matches; - } - - // Get search transient. - $search_query_transient = 'bs_' . preg_replace( '/[^A-Za-z0-9\-]/', '', str_replace( ' ', '', $search_query ) ); - - /** - * Filter name of the search transient - * - * @since 2.1.0 - * - * @param string $search_query_transient Transient name - * @param array $search_query Search query - */ - $search_query_transient = apply_filters( 'bsearch_transient_name', $search_query_transient, $search_query ); - $search_query_transient = substr( $search_query_transient, 0, 40 ); // Name of the transient limited to 40 chars. - - $matches = get_transient( $search_query_transient ); - - if ( $matches ) { - - if ( isset( $matches['search_query'] ) ) { - - if ( $matches['search_query'] === $search_query ) { - $results = $matches[0]; - - /** - * Filter array holding the search results - * - * @since 1.2 - * - * @param object $matches Search results object - * @param array $search_info Search query - */ - return apply_filters( 'get_bsearch_matches', $matches, $search_info ); - - } - } - } - - $boolean_mode = bsearch_get_option( 'boolean_mode' ); - $aggressive_search = bsearch_get_option( 'aggressive_search' ); - - // If no transient is set. - if ( ! isset( $results ) ) { - $sql = bsearch_sql_prepare( $search_info, $boolean_mode, $bydate ); - - $results = $wpdb->get_results( $sql ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - } - - // If no results are found then force BOOLEAN mode only if this isn't ON before. - if ( ! $results && ! $boolean_mode && $aggressive_search ) { - $sql = bsearch_sql_prepare( $search_info, 1, $bydate ); - - $results = $wpdb->get_results( $sql ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - } - - // If no results are found then force LIKE mode. - if ( ! $results && $aggressive_search ) { - // Strip out all the fancy characters that fulltext would use. - $search_query = addslashes_gpc( $search_query ); - $search_query = preg_replace( '/, +/', ' ', $search_query ); - $search_query = str_replace( ',', ' ', $search_query ); - $search_query = str_replace( '"', ' ', $search_query ); - $search_query = trim( $search_query ); - $search_words = explode( ' ', $search_query ); - - $s_array[0] = $search_query; // Save original query at [0]. - $s_array[1] = $search_words; // Save array of terms at [1]. - - $search_info = $s_array; - - $sql = bsearch_sql_prepare( $search_info, 0, $bydate ); - - $results = $wpdb->get_results( $sql ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - } - - $matches[0] = $results; - $matches['search_query'] = $search_query; - - if ( bsearch_get_option( 'cache' ) ) { - // Set search transient. - set_transient( $search_query_transient, $matches, bsearch_get_option( 'cache_time' ) ); - } - - /** - * Described in better-search.php - */ - return apply_filters( 'get_bsearch_matches', $matches, $search_info ); -} - - -/** - * Returns an array with the first and last indices to be displayed on the page. - * - * @since 1.2 - * @deprecated 3.0.0 - * - * @param int $numrows Total results. - * @param int $limit Results per page. - * @return array First and last indices to be displayed on the page - */ -function get_bsearch_range( $numrows, $limit ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - if ( ! ( $limit ) ) { - $limit = isset( $_GET['limit'] ) ? intval( $_GET['limit'] ) : bsearch_get_option( 'limit' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - } - $page = isset( $_GET['bpaged'] ) ? intval( wp_unslash( $_GET['bpaged'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - $last = min( $page + $limit - 1, $numrows - 1 ); - - $match_range = array( $page, $last ); - - /** - * Filter array with the first and last indices to be displayed on the page. - * - * @since 1.3 - * - * @param array $match_range First and last indices to be displayed on the page - * @param int $numrows Total results - * @param int $limit Results per page - */ - return apply_filters( 'get_bsearch_range', $match_range, $numrows, $limit ); -} - - -/** - * Returns an array with the first and last indices to be displayed on the page. - * - * @since 2.0.0 - * @deprecated 3.0.0 - * - * @param array $search_info Search query. - * @param bool $boolean_mode Set BOOLEAN mode for FULLTEXT searching. - * @param bool $bydate Sort by date. - * @return array First and last indices to be displayed on the page - */ -function bsearch_sql_prepare( $search_info, $boolean_mode, $bydate ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb; - - // Initialise some variables. - $fields = ''; - $where = ''; - $join = ''; - $groupby = ''; - $orderby = ''; - $limits = ''; - - $post_types = bsearch_post_types(); - - // Create a FULLTEXT clause only if there is no second element of the $search_info array. Use LIKE otherwise. - $use_fulltext = $search_info[2]; - - // Set BOOLEAN Mode. - $boolean_mode = ( $boolean_mode ) ? ' IN BOOLEAN MODE' : ''; - - $args = array( - 'use_fulltext' => $use_fulltext, - 'boolean_mode' => $boolean_mode, - 'bydate' => $bydate, - 'post_types' => $post_types, - ); - - $fields = bsearch_posts_fields( $search_info[0], $args ); - $join = bsearch_posts_join( $search_info[0], $args ); - $where = bsearch_posts_where( $search_info, $args ); - $orderby = bsearch_posts_orderby( $search_info[0], $args ); - $groupby = bsearch_posts_groupby( $search_info[0], $args ); - $limits = bsearch_posts_limits( $search_info[0], $args ); - - if ( ! empty( $groupby ) ) { - $groupby = 'GROUP BY ' . $groupby; - } - if ( ! empty( $orderby ) ) { - $orderby = 'ORDER BY ' . $orderby; - } - if ( ! empty( $limits ) ) { - $limits = 'LIMIT ' . $limits; - } - - $sql = "SELECT DISTINCT $fields FROM $wpdb->posts $join WHERE 1=1 $where $groupby $orderby $limits"; - - /** - * Filter MySQL string used to fetch results. - * - * @since 1.3 - * - * @param string $sql MySQL string - * @param array $search_info Search query - * @param bool $boolean_mode Set BOOLEAN mode for FULLTEXT searching - * @param bool $bydate Sort by date? - */ - return apply_filters( 'bsearch_sql_prepare', $sql, $search_info, $boolean_mode, $bydate ); -} - - -/** - * Get the MATCH field of the query - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $search_query Search query. - * @param array $args Array of arguments. - * @return string MATCH field - */ -function bsearch_posts_match_field( $search_query, $args = array() ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb; - - $weight_title = bsearch_get_option( 'weight_title' ); - $weight_content = bsearch_get_option( 'weight_content' ); - $boolean_mode = $args['boolean_mode']; - $search_query = str_replace( '"', '"', $search_query ); - - // Create the base MATCH part of the FIELDS clause. - if ( $args['use_fulltext'] ) { - $field_args = array( - $search_query, - $weight_title, - $search_query, - $weight_content, - ); - - $field_score = ", (MATCH({$wpdb->posts}.post_title) AGAINST ('%s' {$boolean_mode} ) * %d ) + "; - $field_score .= "(MATCH({$wpdb->posts}.post_content) AGAINST ('%s' {$boolean_mode} ) * %d ) "; - $field_score = $wpdb->prepare( $field_score, $field_args ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $field_score = stripslashes( $field_score ); - } else { - $field_score = ', 0 '; - } - - $field_score .= 'AS score '; - - /** This filter has been defined in class-better-search.php */ - return apply_filters( 'bsearch_posts_match_field', $field_score, $search_query, $weight_title, $weight_content, $args ); -} - - -/** - * Get the Fields clause for the Better Search query. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $search_query Search query. - * @param array $args Array of arguments. - * @return string Fields clause - */ -function bsearch_posts_fields( $search_query, $args = array() ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb; - - // Parse incomming $args into an array and merge it with $defaults. - $args = wp_parse_args( $args, bsearch_query_default_args() ); - - $fields = " {$wpdb->posts}.ID as ID"; - - $fields .= bsearch_posts_match_field( $search_query, $args ); - - /** This filter has been defined in class-better-search.php */ - return apply_filters( 'bsearch_posts_fields', $fields, $search_query, $args ); -} - - -/** - * Get the MATCH clause for the Better Search WHERE clause. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $search_query Search query. - * @param array $args Array of arguments. - * @return string MATCH clause - */ -function bsearch_posts_match( $search_query, $args = array() ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb; - - // Parse incomming $args into an array and merge it with $defaults. - $args = wp_parse_args( $args, bsearch_query_default_args() ); - - $boolean_mode = $args['boolean_mode']; - - $search_query = str_replace( '"', '"', $search_query ); - - // Construct the MATCH part of the WHERE clause. - $match = " AND MATCH ({$wpdb->posts}.post_title,{$wpdb->posts}.post_content) AGAINST ('%s' {$boolean_mode} ) "; - - $match = $wpdb->prepare( $match, $search_query ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $match = stripslashes( $match ); - - /** This filter has been defined in class-better-search.php */ - return apply_filters( 'bsearch_posts_match', $match, $search_query, $args ); -} - - -/** - * Get the WHERE clause. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param array $search_info Search query. This will have two elemnts if we're using LIKE. - * @param array $args Array of arguments. - * @return string WHERE clause - */ -function bsearch_posts_where( $search_info, $args = array() ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb, $bsearch_error; - - if ( '' !== $bsearch_error->get_error_message( 'bsearch_banned' ) && bsearch_get_option( 'banned_stop_search' ) ) { - return ' AND 1=0 '; - } - - // Parse incomming $args into an array and merge it with $defaults. - $args = wp_parse_args( $args, bsearch_query_default_args() ); - - $n = '%'; - - if ( ! $args['use_fulltext'] ) { - - $search_terms = $search_info[1]; - $no_search_terms = count( $search_terms ); - - // Create the WHERE Clause. - $where = ' AND ( '; - $where .= $wpdb->prepare( - " (({$wpdb->posts}.post_title LIKE %s) OR ({$wpdb->posts}.post_content LIKE %s)) ", - $n . $search_terms[0] . $n, - $n . $search_terms[0] . $n - ); - - for ( $i = 1; $i < $no_search_terms; $i++ ) { - $where .= $wpdb->prepare( - " AND (({$wpdb->posts}.post_title LIKE %s) OR ({$wpdb->posts}.post_content LIKE %s)) ", - $n . $search_terms[ $i ] . $n, - $n . $search_terms[ $i ] . $n - ); - } - - $where .= $wpdb->prepare( - " OR ({$wpdb->posts}.post_title LIKE %s) OR ({$wpdb->posts}.post_content LIKE %s) ", - $n . $search_terms[0] . $n, - $n . $search_terms[0] . $n - ); - - $where .= ' ) '; - - } else { - - $where = bsearch_posts_match( $search_info[0], $args ); - } - - $where .= " AND ({$wpdb->posts}.post_status = 'publish' OR {$wpdb->posts}.post_status = 'inherit')"; - - // Array of post types. - if ( $args['post_types'] ) { - $where .= " AND {$wpdb->posts}.post_type IN ('" . join( "', '", $args['post_types'] ) . "') "; - } - - /** - * Filter the WHERE clause of the query. - * - * @since 2.0.0 - * - * @param string $where The WHERE clause of the query - * @param string $search_info[0] Search query - * @param array $args Array of arguments - */ - return apply_filters( 'bsearch_posts_where', $where, $search_info[0], $args ); -} - - -/** - * Get the ORDERBY clause. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $search_query Search query. - * @param array $args Array of arguments. - * @return string ORDERBY clause - */ -function bsearch_posts_orderby( $search_query, $args = array() ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - // Parse incomming $args into an array and merge it with $defaults. - $args = wp_parse_args( $args, bsearch_query_default_args() ); - - // ORDER BY clause. - if ( $args['bydate'] || ! $args['use_fulltext'] ) { - $orderby = ' post_date DESC '; - } else { - $orderby = ' score DESC '; - } - - /** - * Filter the ORDER BY clause of the query. - * - * @since 2.0.0 - * - * @param string $orderby The ORDER BY clause of the query - * @param string $search_query Search query - * @param array $args Array of arguments - */ - return apply_filters( 'bsearch_posts_orderby', $orderby, $search_query, $args ); -} - - -/** - * Get the GROUPBY clause. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $search_query Search query. - * @param array $args Array of arguments. - * @return string GROUPBY clause - */ -function bsearch_posts_groupby( $search_query, $args = array() ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - // Parse incomming $args into an array and merge it with $defaults. - $args = wp_parse_args( $args, bsearch_query_default_args() ); - - $groupby = ''; - - /** - * Filter the GROUP BY clause of the query. - * - * @since 2.0.0 - * - * @param string $groupby The GROUP BY clause of the query - * @param string $search_query Search query - * @param array $args Array of arguments - */ - return apply_filters( 'bsearch_posts_groupby', $groupby, $search_query, $args ); -} - - -/** - * Get the JOIN clause. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $search_query Search query. - * @param array $args Array of arguments. - * @return string JOIN clause - */ -function bsearch_posts_join( $search_query, $args = array() ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - // Parse incomming $args into an array and merge it with $defaults. - $args = wp_parse_args( $args, bsearch_query_default_args() ); - - $join = ''; - - /** - * Filter the JOIN clause of the query. - * - * @since 2.0.0 - * - * @param string $join The JOIN clause of the query - * @param string $search_query Search query - * @param array $args Array of arguments - */ - return apply_filters( 'bsearch_posts_join', $join, $search_query, $args ); -} - - -/** - * Get the LIMITS clause. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $search_query Search query. - * @param array $args Array of arguments. - * @return string LIMITS clause - */ -function bsearch_posts_limits( $search_query, $args = array() ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - // Parse incomming $args into an array and merge it with $defaults. - $args = wp_parse_args( $args, bsearch_query_default_args() ); - - $limits = ''; - - /** - * Filter the LIMITS clause of the query. - * - * @since 2.0.0 - * - * @param string $limits The LIMITS clause of the query - * @param string $search_query Search query - * @param array $args Array of arguments - */ - return apply_filters( 'bsearch_posts_limits', $limits, $search_query, $args ); -} - - -/** - * Get default query arguments. - * - * @deprecated 3.0.0 - * - * @return array Default quesry arguments - */ -function bsearch_query_default_args() { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - // if there are two items in $search_info, the string has been broken into separate terms that - // are listed at $search_info[1]. The cleaned-up version of $search_query is still at the zero index. - // This is when fulltext is disabled, and we search using LIKE. - $search_info = get_bsearch_terms(); - - $args = array( - 'use_fulltext' => isset( $search_info[2] ) ? $search_info[2] : false, - 'boolean_mode' => bsearch_get_option( 'boolean_mode' ) ? ' IN BOOLEAN MODE' : '', - 'bydate' => 0, - 'post_types' => bsearch_post_types(), - ); - - /** - * Filter default query arguments. - * - * @return array Default quesry arguments - */ - return apply_filters( 'bsearch_query_default_args', $args ); -} - - -/** - * Get the Better Search post types. - * - * @deprecated 3.0.0 - * - * @return array Post types - */ -function bsearch_post_types() { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - // If post_types is empty or contains a query string then use parse_str else consider it comma-separated. - $post_types_from_db = bsearch_get_option( 'post_types' ); - - if ( ! empty( $post_types_from_db ) && is_array( $post_types_from_db ) ) { - $post_types = $post_types_from_db; - } elseif ( ! empty( $post_types_from_db ) && false === strpos( $post_types_from_db, '=' ) ) { - $post_types = explode( ',', $post_types_from_db ); - } else { - parse_str( $post_types_from_db, $post_types ); // Save post types in $post_types variable. - } - - // If post_types is empty or if we want all the post types. - if ( empty( $post_types ) || 'all' === $post_types_from_db ) { - $post_types = get_post_types( - array( - 'public' => true, - ) - ); - } - - return $post_types; -} - -/** - * Filter JOIN clause of bsearch query to add taxonomy tables. - * - * @since 2.4.0 - * @deprecated 3.0.0 - * - * @param mixed $join Join clause. - * @return string Filtered JOIN clause - */ -function bsearch_exclude_categories_join( $join ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb; - - if ( '' !== bsearch_get_option( 'exclude_categories' ) ) { - - $sql = $join; - $sql .= " LEFT JOIN $wpdb->term_relationships AS excat_tr ON ($wpdb->posts.ID = excat_tr.object_id) "; - $sql .= " LEFT JOIN $wpdb->term_taxonomy AS excat_tt ON (excat_tr.term_taxonomy_id = excat_tt.term_taxonomy_id) "; - - return $sql; - } else { - return $join; - } -} -add_filter( 'bsearch_posts_join', 'bsearch_exclude_categories_join' ); - - -/** - * Filter WHERE clause of bsearch query to exclude posts belonging to certain categories. - * - * @since 2.4.0 - * @deprecated 3.0.0 - * - * @param mixed $where WHERE clause. - * @return string Filtered WHERE clause - */ -function bsearch_exclude_categories_where( $where ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb; - - if ( '' === bsearch_get_option( 'exclude_categories' ) ) { - return $where; - } else { - - $terms = bsearch_get_option( 'exclude_categories' ); - - $sql = $where; - - $sql .= " AND $wpdb->posts.ID NOT IN ( - SELECT object_id - FROM $wpdb->term_relationships - WHERE term_taxonomy_id IN ($terms) - )"; - - return $sql; - } -} -add_filter( 'bsearch_posts_where', 'bsearch_exclude_categories_where' ); - - -/** - * Filter GROUP BY clause of bsearch query to exclude posts belonging to certain categories. - * - * @since 2.4.0 - * @deprecated 3.0.0 - * - * @param mixed $groupby GROUP BY clause. - * @return string Filtered GROUP BY clause - */ -function bsearch_exclude_categories_groupby( $groupby ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - if ( '' !== bsearch_get_option( 'exclude_categories' ) && '' !== $groupby ) { - - $sql = $groupby; - $sql .= ' excat_tt.term_taxonomy_id '; - - return $sql; - } else { - return $groupby; - } -} -add_filter( 'bsearch_posts_groupby', 'bsearch_exclude_categories_groupby' ); - - -/** - * Function to exclude protected posts. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $where WHERE clause. - * @return string Updated WHERE clause - */ -function bsearch_exclude_protected( $where ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb; - - if ( bsearch_get_option( 'exclude_protected_posts' ) ) { - $where .= " AND {$wpdb->posts}.post_password = '' "; - } - - return $where; -} -add_filter( 'bsearch_posts_where', 'bsearch_exclude_protected' ); - - -/** - * Function to exclude post IDs. - * - * @since 2.2.0 - * @deprecated 3.0.0 - * - * @param string $where WHERE clause. - * @return string Updated WHERE clause - */ -function bsearch_exclude_post_ids( $where ) { - - _deprecated_function( __FUNCTION__, '3.0.0' ); - - global $wpdb; - - $exclude_post_ids = bsearch_get_option( 'exclude_post_ids' ); - - if ( ! empty( $exclude_post_ids ) ) { - $where .= " AND {$wpdb->posts}.ID NOT IN ({$exclude_post_ids}) "; - } - - return $where; -} -add_filter( 'bsearch_posts_where', 'bsearch_exclude_post_ids' ); diff --git a/includes/frontend/class-display.php b/includes/frontend/class-display.php new file mode 100644 index 0000000..ec94424 --- /dev/null +++ b/includes/frontend/class-display.php @@ -0,0 +1,81 @@ +ID || in_array( $result->ID, $processed_ids ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict + continue; + } + + // Push the current ID into the array to ensure we're not repeating it. + array_push( $processed_ids, $result->ID ); + + // Let's get the Post using the ID. + $result = get_post( $result ); + array_push( $processed_results, $result ); + } + return $processed_results; + } + + /** + * Returns the object identifier for the current language (WPML). + * + * @since 3.3.0 + * + * @param int|string|\WP_Post $post Post object or Post ID. + * @return \WP_Post|array|null Post opbject, updated if needed. + */ + public static function object_id_cur_lang( $post ) { + + $return_original_if_missing = false; + + $post = get_post( $post ); + $current_lang = apply_filters( 'wpml_current_language', null ); + + // Polylang implementation. + if ( function_exists( 'pll_get_post' ) ) { + $translated_post = \pll_get_post( $post->ID ); + if ( $translated_post ) { + $post = get_post( $translated_post ); + } + } + + // WPML implementation. + if ( class_exists( 'SitePress' ) ) { + /** + * Filter to modify if the original language ID is returned. + * + * @since 2.2.3 + * + * @param bool $return_original_if_missing Flag to return original post ID if translated post ID is missing. + * @param int $id Post ID + */ + $return_original_if_missing = apply_filters( 'bsearch_wpml_return_original', $return_original_if_missing, $post->ID ); + + $translated_post = apply_filters( 'wpml_object_id', $post->ID, $post->post_type, $return_original_if_missing, $current_lang ); + if ( $translated_post ) { + $post = get_post( $translated_post ); + } + } + + /** + * Filters Post object for current language. + * + * @since 3.3.0 + * + * @param \WP_Post|array|null $id Post object. + */ + return apply_filters( 'bsearch_object_id_cur_lang', $post ); + } +} diff --git a/includes/frontend/class-media-handler.php b/includes/frontend/class-media-handler.php new file mode 100644 index 0000000..fbdaf90 --- /dev/null +++ b/includes/frontend/class-media-handler.php @@ -0,0 +1,646 @@ + '', + 'size' => 'thumbnail', + 'thumb_meta' => 'post-image', + 'thumb_html' => 'html', + 'thumb_default' => '', + 'thumb_default_show' => true, + 'scan_images' => false, + 'class' => self::$prefix . '_thumb', + ); + + // Parse incomming $args into an array and merge it with $defaults. + $args = wp_parse_args( $args, $defaults ); + + $result = get_post( $args['post'] ); + + if ( empty( $result ) ) { + return ''; + } + + if ( is_string( $args['size'] ) ) { + list( $args['thumb_width'], $args['thumb_height'] ) = self::get_thumb_size( $args['size'] ); + } else { + $args['thumb_width'] = $args['size'][0]; + $args['thumb_height'] = $args['size'][1]; + $args['size'] = self::get_appropriate_image_size( $args['size'][0], $args['size'][1] ); + } + + $post_title = esc_attr( $result->post_title ); + + $output = ''; + $postimage = ''; + $pick = ''; + $attachment_id = ''; + + // Let's start fetching the thumbnail. First place to look is in the post meta defined in the Settings page. + $postimage = get_post_meta( $result->ID, $args['thumb_meta'], true ); + $postimage = filter_var( $postimage, FILTER_VALIDATE_URL ); + $pick = 'meta'; + if ( $postimage ) { + $attachment_id = self::get_attachment_id_from_url( $postimage ); + + $postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] ); + if ( false !== $postthumb ) { + list( $postimage, $args['thumb_width'], $args['thumb_height'] ) = $postthumb; + $pick .= 'correct'; + } + } + + // If there is no thumbnail found, check the post thumbnail. + if ( ! $postimage ) { + if ( false !== get_post_thumbnail_id( $result->ID ) ) { + $attachment_id = ( 'attachment' === $result->post_type ) ? $result->ID : get_post_thumbnail_id( $result->ID ); + + $postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] ); + if ( false !== $postthumb ) { + list( $postimage, $args['thumb_width'], $args['thumb_height'] ) = $postthumb; + $pick = 'featured'; + } + } + $pick = 'featured'; + } + + // If there is no thumbnail found, fetch the first image in the post, if enabled. + if ( ! $postimage && $args['scan_images'] ) { + + /** + * Filters the post content that is used to scan for images. + * + * A filter function can be tapped into this to execute shortcodes, modify content, etc. + * + * @since 3.1.0 + * + * @param string $post_content Post content + * @param \WP_Post $result Post Object + */ + $post_content = apply_filters( self::$prefix . '_thumb_post_content', $result->post_content, $result ); + + preg_match_all( '//i', $post_content, $matches ); + if ( isset( $matches[1][0] ) && $matches[1][0] ) { // any image there? + $postimage = $matches[1][0]; // we need the first one only! + } + $pick = 'first'; + if ( $postimage ) { + $attachment_id = self::get_attachment_id_from_url( $postimage ); + + $postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] ); + if ( false !== $postthumb ) { + list( $postimage, $args['thumb_width'], $args['thumb_height'] ) = $postthumb; + $pick .= 'correct'; + } + } + } + + // If there is no thumbnail found, fetch the first child image. + if ( ! $postimage ) { + $postimage = self::get_first_image( $result->ID, $args['thumb_width'], $args['thumb_height'] ); // Get the first image. + $pick = 'firstchild'; + } + + // If no other thumbnail set, try to get the custom video thumbnail set by the Video Thumbnails plugin. + if ( ! $postimage ) { + $postimage = get_post_meta( $result->ID, '_video_thumbnail', true ); + $pick = 'video_thumb'; + } + + // If no thumb found and settings permit, use default thumb. + if ( ! $postimage && $args['thumb_default_show'] ) { + $postimage = $args['thumb_default']; + $pick = 'default_thumb'; + } + + // If no thumb found, use site icon. + if ( ! $postimage ) { + $postimage = get_site_icon_url( max( $args['thumb_width'], $args['thumb_height'] ) ); + $pick = 'site_icon_max'; + } + + if ( ! $postimage ) { + $postimage = get_site_icon_url( min( $args['thumb_width'], $args['thumb_height'] ) ); + $pick = 'site_icon_min'; + } + + // Hopefully, we've found a thumbnail by now. If so, run it through the custom filter, check for SSL and create the image tag. + if ( $postimage ) { + + /** + * Filters the thumbnail image URL. + * + * Use this filter to modify the thumbnail URL that is automatically created + * Before v2.1 this was used for cropping the post image using timthumb + * + * @since 2.1.0 + * @since 3.1.0 Second argument changed to $args array and third argument changed to Post object. + * + * @param string $postimage URL of the thumbnail image + * @param array $args Arguments array. + * @param \WP_Post $result Post Object + */ + $postimage = apply_filters( self::$prefix . '_thumb_url', $postimage, $args, $result ); + + if ( is_ssl() ) { + $postimage = preg_replace( '~http://~', 'https://', $postimage ); + } + + $class = self::$prefix . "_{$pick} {$args['class']} {$args['size']}"; + + if ( empty( $attachment_id ) && ! in_array( $pick, array( 'video_thumb', 'default_thumb', 'site_icon_max', 'site_icon_min' ), true ) ) { + $attachment_id = self::get_attachment_id_from_url( $postimage ); + } + + /** + * Flag to use the image's alt text as the thumbnail alt text. + * + * @since 3.3.1 + * + * @param bool $use_image_alt Flag to use the image's alt text as the thumbnail alt text. + */ + $use_image_alt = apply_filters( self::$prefix . '_thumb_use_image_alt', true ); + + /** + * Flag to use the post title as the thumbnail alt text if no alt text is found. + * + * @since 3.3.1 + * + * @param bool $alt_fallback Flag to use the post title as the thumbnail alt text if no alt text is found. + */ + $alt_fallback = apply_filters( self::$prefix . '_thumb_alt_fallback_post_title', true ); + + if ( ! empty( $attachment_id ) && $use_image_alt ) { + $alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); + } + + // If empty alt then try to get the title of the attachment. + if ( empty( $alt ) && ! empty( $attachment_id ) ) { + $alt = get_the_title( $attachment_id ); + } + + if ( empty( $alt ) ) { + $alt = $alt_fallback ? $post_title : ''; + } + + /** + * Filters the thumbnail classes and allows a filter function to add any more classes if needed. + * + * @since 2.2.0 + * + * @param string $class Thumbnail Class + */ + $attr['class'] = apply_filters( self::$prefix . '_thumb_class', $class ); + + /** + * Filters the thumbnail alt. + * + * @since 2.6.0 + * + * @param string $alt Thumbnail alt attribute + */ + $attr['alt'] = apply_filters( self::$prefix . '_thumb_alt', $alt ); + + /** + * Filters the thumbnail title. + * + * @since 2.6.0 + * + * @param string $post_title Thumbnail title attribute + */ + $attr['title'] = apply_filters( self::$prefix . '_thumb_title', $post_title ); + + $attr['thumb_html'] = $args['thumb_html']; + $attr['thumb_width'] = $args['thumb_width']; + $attr['thumb_height'] = $args['thumb_height']; + + $output .= self::get_image_html( $postimage, $attr ); + + if ( function_exists( 'wp_img_tag_add_srcset_and_sizes_attr' ) && ! empty( $attachment_id ) ) { + $output = wp_img_tag_add_srcset_and_sizes_attr( $output, self::$prefix . '_thumbnail', $attachment_id ); + } + + if ( function_exists( 'wp_img_tag_add_loading_optimization_attrs' ) ) { + $output = wp_img_tag_add_loading_optimization_attrs( $output, self::$prefix . '_thumbnail' ); + } elseif ( function_exists( 'wp_img_tag_add_loading_attr' ) ) { + $output = wp_img_tag_add_loading_attr( $output, 'crp_thumbnail' ); + } + } + + /** + * Filters post thumbnail created for Top 10. + * + * @since 1.9.10.1 + * + * @param string $output Formatted output + * @param array $args Argument list + * @param string $postimage Thumbnail URL + */ + return apply_filters( self::$prefix . '_get_the_post_thumbnail', $output, $args, $postimage ); + } + + /** + * Get an HTML img element + * + * @since 2.6.0 + * + * @param string $attachment_url Image URL. + * @param array $attr Attributes for the image markup. + * @return string HTML img element or empty string on failure. + */ + public static function get_image_html( $attachment_url, $attr = array() ) { + $get_option_callback = self::$prefix . '_get_option'; + + // If there is no url, return. + if ( ! $attachment_url ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + return ''; + } + + $default_attr = array( + 'src' => $attachment_url, + 'thumb_html' => call_user_func( $get_option_callback, 'thumb_html', 'html' ), + 'thumb_width' => call_user_func( $get_option_callback, 'thumb_width', 150 ), + 'thumb_height' => call_user_func( $get_option_callback, 'thumb_height', 150 ), + ); + + $attr = wp_parse_args( $attr, $default_attr ); + + $hwstring = self::get_image_hwstring( $attr ); + + // Generate 'srcset' and 'sizes' if not already present. + if ( empty( $attr['srcset'] ) ) { + $attachment_id = self::get_attachment_id_from_url( $attachment_url ); + $image_meta = wp_get_attachment_metadata( $attachment_id ); + + if ( is_array( $image_meta ) ) { + $size_array = array( absint( $attr['thumb_width'] ), absint( $attr['thumb_height'] ) ); + $srcset = wp_calculate_image_srcset( $size_array, $attachment_url, $image_meta, $attachment_id ); + $sizes = wp_calculate_image_sizes( $size_array, $attachment_url, $image_meta, $attachment_id ); + + if ( $srcset && ( $sizes || ! empty( $attr['sizes'] ) ) ) { + $attr['srcset'] = $srcset; + + if ( empty( $attr['sizes'] ) ) { + $attr['sizes'] = $sizes; + } + } + } + } + + // Unset attributes we don't want to display. + unset( $attr['thumb_html'] ); + unset( $attr['thumb_width'] ); + unset( $attr['thumb_height'] ); + + /** + * Filters the list of attachment image attributes. + * + * @since 2.6.0 + * + * @param array $attr Attributes for the image markup. + * @param string $attachment_url Image URL. + */ + $attr = apply_filters( self::$prefix . '_get_image_attributes', $attr, $attachment_url ); + $attr = array_map( 'esc_attr', $attr ); + + $html = ' $value ) { + if ( '' === $value ) { + continue; + } + $html .= " $name=" . '"' . $value . '"'; + } + $html .= ' />'; + + /** + * Filters the img tag. + * + * @since 2.6.0 + * + * @param string $html HTML img element or empty string on failure. + * @param string $attachment_url Image URL. + * @param array $attr Attributes for the image markup. + */ + return apply_filters( self::$prefix . '_get_image_html', $html, $attachment_url, $attr ); + } + + + /** + * Retrieve width and height attributes using given width and height values. + * + * @since 2.6.0 + * + * @param array $args Argument array. + * + * @return string Height-width string. + */ + public static function get_image_hwstring( $args = array() ) { + $get_option_callback = self::$prefix . '_get_option'; + + $default_args = array( + 'thumb_html' => call_user_func( $get_option_callback, 'thumb_html', 'html' ), + 'thumb_width' => call_user_func( $get_option_callback, 'thumb_width', 150 ), + 'thumb_height' => call_user_func( $get_option_callback, 'thumb_height', 150 ), + ); + + $args = wp_parse_args( $args, $default_args ); + + if ( 'css' === $args['thumb_html'] ) { + $thumb_html = ' style="max-width:' . $args['thumb_width'] . 'px;max-height:' . $args['thumb_height'] . 'px;" '; + } elseif ( 'html' === $args['thumb_html'] ) { + $thumb_html = ' width="' . $args['thumb_width'] . '" height="' . $args['thumb_height'] . '" '; + } else { + $thumb_html = ''; + } + + /** + * Filters the thumbnail HTML and allows a filter function to add any more HTML if needed. + * + * @since 2.2.0 + * + * @param string $thumb_html Thumbnail HTML. + * @param array $args Argument array. + */ + return apply_filters( self::$prefix . '_thumb_html', $thumb_html, $args ); + } + + + /** + * Get the first child image in the post. + * + * @since 1.9.8 + * @param mixed $postid Post ID. + * @param int $thumb_width Thumb width. + * @param int $thumb_height Thumb height. + * @return string Location of thumbnail + */ + public static function get_first_image( $postid, $thumb_width, $thumb_height ) { + $args = array( + 'numberposts' => 1, + 'order' => 'ASC', + 'post_mime_type' => 'image', + 'post_parent' => $postid, + 'post_status' => null, + 'post_type' => 'attachment', + ); + + $attachments = get_children( $args ); + + if ( $attachments ) { + foreach ( $attachments as $attachment ) { + $image_attributes = wp_get_attachment_image_src( $attachment->ID, array( $thumb_width, $thumb_height ) ) ? wp_get_attachment_image_src( $attachment->ID, array( $thumb_width, $thumb_height ) ) : wp_get_attachment_image_src( $attachment->ID, 'full' ); + + /** + * Filters first child attachment from the post. + * + * @since 1.9.10.1 + * + * @param string $image_attributes[0] URL of the image + * @param int $postid Post ID + * @param int $thumb_width Thumb width + * @param int $thumb_height Thumb height + */ + return apply_filters( self::$prefix . '_get_first_image', $image_attributes[0], $postid, $thumb_width, $thumb_height ); + } + } else { + return ''; + } + } + + + /** + * Function to get the attachment ID from the attachment URL. + * + * @since 2.1 + * + * @param string $attachment_url Attachment URL. + * @return int Attachment ID + */ + public static function get_attachment_id_from_url( $attachment_url = '' ) { + + global $wpdb; + $attachment_id = false; + + // If there is no url, return. + if ( ! $attachment_url ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + return 0; + } + + // Get the upload directory paths. + $upload_dir_paths = wp_upload_dir(); + + // Make sure the upload path base directory exists in the attachment URL, to verify that we're working with a media library image. + if ( false !== strpos( $attachment_url, $upload_dir_paths['baseurl'] ) ) { + + // If this is the URL of an auto-generated thumbnail, get the URL of the original image. + $attachment_url = preg_replace( '/-\d+x\d+(?=\.(jpg|jpeg|png|gif)$)/i', '', $attachment_url ); + + // Remove the upload path base directory from the attachment URL. + $attachment_url = str_replace( $upload_dir_paths['baseurl'] . '/', '', $attachment_url ); + + // Finally, run a custom database query to get the attachment ID from the modified attachment URL. + $attachment_id = $wpdb->get_var( $wpdb->prepare( "SELECT wposts.ID FROM $wpdb->posts wposts, $wpdb->postmeta wpostmeta WHERE wposts.ID = wpostmeta.post_id AND wpostmeta.meta_key = '_wp_attached_file' AND wpostmeta.meta_value = %s AND wposts.post_type = 'attachment'", $attachment_url ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + } + + /** + * Filter the attachment ID from the attachment URL. + * + * @since 2.1 + * + * @param int $attachment_id Attachment ID + * @param string $attachment_url Attachment URL + */ + return apply_filters( self::$prefix . '_get_attachment_id_from_url', $attachment_id, $attachment_url ); + } + + + /** + * Function to get the correct height and width of the thumbnail. + * + * @since 2.2.0 + * @since 3.1.0 First argument is a string. + * + * @param string $size Image size. + * @return array Width and height. If no width and height is found, then 150 is returned for each. + */ + public static function get_thumb_size( $size = 'thumbnail' ) { + + // Get thumbnail size. + $thumb_size_array = self::get_all_image_sizes( $size ); + + if ( isset( $thumb_size_array['width'] ) ) { + $thumb_width = $thumb_size_array['width']; + $thumb_height = $thumb_size_array['height']; + } + + if ( isset( $thumb_width ) && isset( $thumb_height ) ) { + $thumb_size = array( $thumb_width, $thumb_height ); + } else { + $thumb_size = array( 150, 150 ); + } + + /** + * Filter array of thumbnail size. + * + * @since 2.2.0 + * + * @param array $thumb_size Array with width and height of thumbnail + */ + return apply_filters( self::$prefix . '_get_thumb_size', $thumb_size ); + } + + + /** + * Get all image sizes. + * + * @since 2.0.0 + * + * @param string|int[] $size Image size. + * @return array|bool If a single size is specified, then the array with width, height and crop status + * or false if size is not found; + * If no size is specified then an Associative array of the registered image sub-sizes. + */ + public static function get_all_image_sizes( $size = '' ) { + + if ( is_array( $size ) ) { + $size = self::get_appropriate_image_size( $size[0], $size[1] ); + } + + $sizes = wp_get_registered_image_subsizes(); + + /* Get only 1 size if found */ + if ( $size ) { + if ( isset( $sizes[ $size ] ) ) { + return $sizes[ $size ]; + } else { + return false; + } + } + + /** + * Filters array of image sizes. + * + * @since 2.0.0 + * + * @param array $sizes Image sizes + */ + return apply_filters( self::$prefix . '_get_all_image_sizes', $sizes ); + } + + /** + * Get the most appropriate image size based on the given thumbnail width and height. + * + * @since 3.3.0 + * + * @param int $thumb_width Thumbnail width. + * @param int $thumb_height Thumbnail height. + * + * @return string|bool Image size name if found, false otherwise. + */ + public static function get_appropriate_image_size( $thumb_width, $thumb_height ) { + $sizes = wp_get_registered_image_subsizes(); + + $closest_size = false; + $closest_distance = PHP_INT_MAX; + + foreach ( $sizes as $size_name => $size_info ) { + $size_width = $size_info['width']; + $size_height = $size_info['height']; + $distance = sqrt( pow( $thumb_width - $size_width, 2 ) + pow( $thumb_height - $size_height, 2 ) ); + + if ( $distance < $closest_distance ) { + $closest_distance = $distance; + $closest_size = $size_name; + } + } + + return $closest_size; + } +} diff --git a/includes/frontend/class-shortcodes.php b/includes/frontend/class-shortcodes.php new file mode 100644 index 0000000..8067a70 --- /dev/null +++ b/includes/frontend/class-shortcodes.php @@ -0,0 +1,89 @@ + false, + 'daily_range' => absint( bsearch_get_option( 'daily_range' ) ), + 'smallest' => absint( bsearch_get_option( 'heatmap_smallest' ) ), + 'largest' => absint( bsearch_get_option( 'heatmap_largest' ) ), + 'unit' => bsearch_get_option( 'heatmap_unit', 'pt' ), + 'hot' => bsearch_get_option( 'heatmap_hot' ), + 'cold' => bsearch_get_option( 'heatmap_cold' ), + 'number' => absint( bsearch_get_option( 'heatmap_limit' ) ), + 'before_term' => bsearch_get_option( 'heatmap_before' ), + 'after_term' => bsearch_get_option( 'heatmap_after' ), + 'link_nofollow' => bsearch_get_option( 'link_nofollow' ), + 'link_new_window' => bsearch_get_option( 'link_new_window' ), + 'format' => 'flat', + 'separator' => "\n", + 'orderby' => 'count', + 'order' => 'RAND', + 'topic_count_text' => null, + 'show_count' => 0, + 'no_results_text' => __( 'No searches made yet', 'better-search' ), + ), + $atts, + 'bsearch_heatmap' + ); + + return get_bsearch_heatmap( $atts ); + } + + /** + * Creates a shortcode [bsearch_form daily="0"]. + * + * @param array $atts Shortcode attributes. + * @return string The Better Search form. + */ + public static function bsearch_form( $atts ) { + $atts = shortcode_atts( + array( + 'before' => '', + 'after' => '', + 'aria_label' => '', + 'post_types' => bsearch_get_option( 'post_types' ), + 'selected_post_types' => '', + 'show_post_types' => false, + ), + $atts, + 'bsearch_form' + ); + + return get_bsearch_form( '', $atts ); + } +} diff --git a/includes/frontend/class-styles-handler.php b/includes/frontend/class-styles-handler.php new file mode 100644 index 0000000..d370bdb --- /dev/null +++ b/includes/frontend/class-styles-handler.php @@ -0,0 +1,62 @@ + '', 'after' => '', - 'echo' => true, 'post' => get_post(), 'excerpt_length' => bsearch_get_option( 'excerpt_length' ), 'use_excerpt' => false, @@ -41,7 +42,7 @@ function the_bsearch_excerpt( $args = array() ) { $args = wp_parse_args( $args, $defaults ); /** - * Filter the arguments used by the_bsearch_excerpt(). + * Filter the arguments used by get_bsearch_excerpt(). * * @since 3.1.0 * @@ -63,11 +64,7 @@ function the_bsearch_excerpt( $args = array() ) { */ $output = apply_filters( 'the_bsearch_excerpt', $output, $args ); - if ( $args['echo'] ) { - echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $output; - } + echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } @@ -76,13 +73,13 @@ function the_bsearch_excerpt( $args = array() ) { * * @since 1.2 * - * @param int|WP_Post $post Post ID or WP_Post instance. - * @param int $excerpt_length Length of the excerpt in words. - * @param bool $use_excerpt Use post excerpt or content. - * @param bool $relevant Only relevant portion of excerpt. + * @param int|\WP_Post $post Post ID or WP_Post instance. + * @param int $excerpt_length Length of the excerpt in words. + * @param bool $use_excerpt Use post excerpt or content. + * @param bool $relevant Only relevant portion of excerpt. * @return string Excerpt */ -function get_bsearch_excerpt( $post = '', $excerpt_length = 0, $use_excerpt = true, $relevant = true ) { +function get_bsearch_excerpt( $post = null, $excerpt_length = 0, $use_excerpt = true, $relevant = true ) { $content = ''; $post = get_post( $post ); @@ -117,7 +114,7 @@ function get_bsearch_excerpt( $post = '', $excerpt_length = 0, $use_excerpt = tr $search_query = str_replace( array( "'", '"', '"', '\+', '\-' ), '', $search_query ); $words = preg_split( '/[\s,\+\.]+/', $search_query ); - $output = bsearch_extract_relevant_excerpt( $words, $output, $excerpt_more ); + $output = Helpers::extract_relevant_excerpt( $words, $output, $excerpt_more ); } if ( $excerpt_length > 0 ) { @@ -158,10 +155,11 @@ function the_bsearch_form( $search_query = '', $args = array() ) { /** - * Function to fetch search form. + * Retrieve the Better Search form. * * @since 1.1 * @since 3.0.0 Add $args + * @since 3.3.0 Remove $echo parameter. This function will always return the form. * * @param string $search_query Search query. * @param string|array $args { @@ -169,13 +167,12 @@ function the_bsearch_form( $search_query = '', $args = array() ) { * * @type string $before Markup to prepend to the search form. * @type string $after Markup to append to the search form. - * @type bool $echo Echo or return? * @type string $aria_label ARIA label for the search form. * Useful to distinguish multiple search forms on the same page and improve accessibility. * @type string[] $post_types Comma separated list or array of post types. * @type bool $show_post_types Whether to show the post types dropdown. * } - * @return void|string Void if 'echo' argument is true, the search form if 'echo' is false. + * @return string The Better Search form. */ function get_bsearch_form( $search_query = '', $args = array() ) { @@ -267,11 +264,7 @@ function get_bsearch_form( $search_query = '', $args = array() ) { */ $result = apply_filters( 'get_bsearch_form', $form, $search_query, $args ); - if ( $args['echo'] ) { - echo $result; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $result; - } + return $result; } @@ -325,6 +318,33 @@ function get_bsearch_title( $text_only = true ) { * Display the header table on the search results page. * * @since 3.0.0 + * @since 3.3.0 Removed $echo parameter. This function will always display the header. + * + * @see get_bsearch_header() + * + * @global WP_Query $wp_query WP_Query + * + * @param string|array $args Array or string of parameters. + */ +function the_bsearch_header( $args = array() ) { + + /** + * Filter the header table. + * + * @since 3.0.0 + * + * @see get_bsearch_header() + * + * @param string $output Header table. + * @param array $args Array of arguments. + */ + echo apply_filters( 'the_bsearch_header', get_bsearch_header( $args ), $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Retrieve the Better Search header. + * + * @since 3.3.0 * * @global WP_Query $wp_query WP_Query * @@ -333,7 +353,6 @@ function get_bsearch_title( $text_only = true ) { * * @type string $before Markup to prepend to the header table. * @type string $after Markup to append to the header table. - * @type bool $echo Echo or return? * @type int $limit Number of posts per page. * @type int $found_posts Total number of posts found. * @type int $max_num_pages Maximum number of pages of results. @@ -341,9 +360,9 @@ function get_bsearch_title( $text_only = true ) { * @type string $search_query Search query. * @type bool $bydate Sory by date. If false, sort by relevance. * } - * @return void|string Void if 'echo' argument is true, the title attribute if 'echo' is false. + * @return string The Better Search header. */ -function the_bsearch_header( $args = array() ) { +function get_bsearch_header( $args = array() ) { /** * WP_Query. * @@ -482,20 +501,16 @@ function the_bsearch_header( $args = array() ) { '; /** - * Filter the header table. + * Filter the Better Search header HTML. * - * @since 3.0.0 + * @since 3.3.0 * - * @param string $output Header table. + * @param string $output Better Search header HTML. * @param array $args Array of arguments. */ - $output = apply_filters( 'the_bsearch_header', $output, $args ); + $output = apply_filters( 'get_bsearch_header', $output, $args ); - if ( $args['echo'] ) { - echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $output; - } + return $output; } @@ -503,6 +518,7 @@ function the_bsearch_header( $args = array() ) { * Display the relevance score for the post. * * @since 3.0.0 + * @since 3.3.0 Removed $echo parameter. This function will always display the score. * * @param string|array $args { * Optional. Array or string of parameters. @@ -511,54 +527,71 @@ function the_bsearch_header( $args = array() ) { * @type int $topscore Top score for which relevance is 100%. * @type string $before Markup to prepend to the relevance score. * @type string $after Markup to append to the relevance score. - * @type bool $echo Echo or return? * } * @return void|string Void if 'echo' argument is true, the title attribute if 'echo' is false. */ function the_bsearch_score( $args = array() ) { + /** + * Filter the relevance score text. + * + * @since 3.0.0 + * + * @param string $output Relevance score text. + * @param array $args Array of arguments. + */ + echo apply_filters( 'the_bsearch_score', get_bsearch_score( $args ), $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Retrieve the relevance score for the post in the search results. + * + * @since 3.3.0 + * + * @param string|array $args { + * Optional. Array or string of parameters. + * + * @type int $score Score of the search result. + * @type int $topscore Top score for which relevance is 100%. + * @type string $before Markup to prepend to the relevance score. + * @type string $after Markup to append to the relevance score. + * @type bool $echo Echo or return? + * } + * @return string Better Search score. + */ +function get_bsearch_score( $args = array() ) { + $defaults = array( 'score' => 0, 'topscore' => 0, 'before' => __( 'Relevance:', 'better-search' ) . ' ', 'after' => '', - 'echo' => true, ); $args = wp_parse_args( $args, $defaults ); - $score = bsearch_score2percent( $args['score'], $args['topscore'] ); + $score = Helpers::score2percent( $args['score'], $args['topscore'] ); $output = $args['before'] . $score . $args['after']; /** * Filter the relevance score text. * - * @since 3.0.0 + * @since 3.3.0 * * @param string $output Relevance score text. * @param array $args Array of arguments. */ - $output = apply_filters( 'the_bsearch_score', $output, $args ); + $output = apply_filters( 'get_bsearch_score', $output, $args ); - if ( $args['echo'] ) { - echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $output; - } + return $output; } /** * Display the Better Search Post Thumbnail. * - * When a theme adds 'post-thumbnail' support, a special 'post-thumbnail' image size - * is registered, which differs from the 'thumbnail' image size managed via the - * Settings > Media screen. - * - * When using the_post_thumbnail() or related functions, the 'post-thumbnail' image - * size is used by default, though a different size can be specified instead as needed. - * * @since 3.0.0 + * @since 3.3.0 Removed $echo parameter. This function will always display the thumbnail. * * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array of * width and height values in pixels (in that order). Default 'post-thumbnail'. @@ -567,23 +600,20 @@ function the_bsearch_score( $args = array() ) { * * @type string $before Display before the thumbnail. * @type string $after Display after the thumbnail. - * @type bool $echo Echo or return? * @type WP_Post $post Post object. * } - * @return void|string Void if 'echo' argument is true, the thumbnail HTML if 'echo' is false. */ function the_bsearch_post_thumbnail( $size = 'thumbnail', $args = array() ) { $defaults = array( 'before' => '', 'after' => '', - 'echo' => true, 'post' => get_post(), 'size' => $size, ); $args = wp_parse_args( $args, $defaults ); - $thumb = bsearch_get_the_post_thumbnail( $args ); + $thumb = Media_Handler::get_the_post_thumbnail( $args ); $output = $args['before'] . $thumb . $args['after']; @@ -599,11 +629,7 @@ function the_bsearch_post_thumbnail( $size = 'thumbnail', $args = array() ) { */ $output = apply_filters( 'the_bsearch_post_thumbnail', $output, $size, $args ); - if ( $args['echo'] ) { - echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $output; - } + echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } @@ -611,24 +637,22 @@ function the_bsearch_post_thumbnail( $size = 'thumbnail', $args = array() ) { * Display the Better Search Date. * * @since 3.0.0 + * @since 3.3.0 Removed $echo parameter. This function will always display the date. * * @param string|array $args { * Optional. Array or string of parameters. * * @type string $before HTML output before the date. * @type string $after HTML output after the date. - * @type bool $echo Echo or return? * @type WP_Post $post Post ID or WP_Post object. Default current post. * @type string $format PHP date format. Defaults to the 'date_format' option. * } - * @return void|string Void if 'echo' argument is true, the post date if 'echo' is false. */ function the_bsearch_date( $args = array() ) { $defaults = array( 'before' => '', 'after' => '', - 'echo' => true, 'post' => get_post(), 'format' => get_option( 'date_format' ), ); @@ -646,11 +670,7 @@ function the_bsearch_date( $args = array() ) { */ $output = apply_filters( 'the_bsearch_date', $output, $args ); - if ( $args['echo'] ) { - echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $output; - } + echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } @@ -720,8 +740,33 @@ function the_bsearch_permalink( $post = 0, $query_args = array() ) { * Retrieves a post’s terms as a list with specified format. Works with custom post types. * * @since 3.0.0 + * @since 3.3.0 Removed $echo parameter. This function will always display the terms. * - * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. + * @see get_bsearch_term_list() + * + * @param int|\WP_Post $post Optional. Post ID or post object. Default is the global `$post`. + * @param string|array $args Optional. Array or string of parameters. + */ +function the_bsearch_term_list( $post = 0, $args = array() ) { + + /** + * Filters the post terms for the current post. + * + * @since 3.0.0 + * + * @param string $output The post term list. + * @param int|\WP_Post $post WP_Post object. + * @param array $args Array of arguments. + */ + echo apply_filters( 'the_bsearch_term_list', get_bsearch_term_list( $post, $args ), $post, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Retrieves a post’s terms as a list with specified format. Works with custom post types. + * + * @since 3.3.0 + * + * @param int|\WP_Post $post Optional. Post ID or post object. Default is the global `$post`. * @param string|array $args { * Optional. Array or string of parameters. * @@ -734,9 +779,9 @@ function the_bsearch_permalink( $post = 0, $query_args = array() ) { * @type bool $echo Echo or return? * @type string|string[] $taxonomy The taxonomy slug or array of slugs for which to retrieve terms. * } - * @return void|string Void if 'echo' argument is true, the post term list if 'echo' is false. + * @return string The post terms list. */ -function the_bsearch_term_list( $post = 0, $args = array() ) { +function get_bsearch_term_list( $post = 0, $args = array() ) { $post = get_post( $post ); if ( empty( $post ) ) { @@ -750,7 +795,6 @@ function the_bsearch_term_list( $post = 0, $args = array() ) { 'before' => '', 'sep' => ' | ', 'after' => '', - 'echo' => true, 'taxonomy' => get_object_taxonomies( $post->post_type ), ); $args = wp_parse_args( $args, $defaults ); @@ -767,37 +811,57 @@ function the_bsearch_term_list( $post = 0, $args = array() ) { /** * Filters the post terms for the current post. * - * @since 3.0.0 + * @since 3.3.0 * * @param string $output The post term list. * @param int|WP_Post $post WP_Post object. * @param array $args Array of arguments. */ - $output = apply_filters( 'the_bsearch_term_list', $output, $post, $args ); + $output = apply_filters( 'get_bsearch_term_list', $output, $post, $args ); - if ( $args['echo'] ) { - echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $output; - } + return $output; } /** - * Display the Better Search Post Type. + * Display the Post Type on the search results page. * * @since 3.0.0 + * @since 3.3.0 Removed $echo parameter. This function will always display the post type. + * + * @see get_bsearch_post_type() * * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. + * @param string|array $args Optional. Array or string of parameters. + */ +function the_bsearch_post_type( $post = 0, $args = array() ) { + + /** + * Filters the post type for the current post. + * + * @since 3.0.0 + * + * @param string $output The post type. + * @param int|WP_Post $post WP_Post object. + * @param array $args Array of arguments. + */ + echo apply_filters( 'the_bsearch_term_list', get_bsearch_post_type( $post, $args ), $post, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Display the Post Type label. + * + * @since 3.3.0 + * + * @param int|\WP_Post $post Optional. Post ID or post object. Default is the global `$post`. * @param string|array $args { * Optional. Array or string of parameters. * * @type string $before HTML output before the post type. * @type string $after HTML output after the post type. - * @type bool $echo Echo or return? * } - * @return void|string Void if 'echo' argument is true, the post date if 'echo' is false. + * @return string The post type label. */ -function the_bsearch_post_type( $post = 0, $args = array() ) { +function get_bsearch_post_type( $post = 0, $args = array() ) { $post = get_post( $post ); if ( empty( $post ) ) { @@ -807,27 +871,23 @@ function the_bsearch_post_type( $post = 0, $args = array() ) { $defaults = array( 'before' => '', 'after' => '', - 'echo' => true, ); $args = wp_parse_args( $args, $defaults ); $obj = get_post_type_object( $post->post_type ); $output = $obj->labels->singular_name; + $output = $args['before'] . $output . $args['after']; /** - * Filters the post type for the current post. + * Filters the post type label for the current post. * - * @since 3.0.0 + * @since 3.3.0 * * @param string $output The post type. - * @param int|WP_Post $post WP_Post object. + * @param int|\WP_Post $post WP_Post object. * @param array $args Array of arguments. */ - $output = apply_filters( 'the_bsearch_term_list', $output, $post, $args ); + $output = apply_filters( 'get_bsearch_term_list', $output, $post, $args ); - if ( $args['echo'] ) { - echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $output; - } + return $output; } diff --git a/includes/modules/heatmap.php b/includes/heatmap.php similarity index 90% rename from includes/modules/heatmap.php rename to includes/heatmap.php index 197ecc0..6f108f0 100644 --- a/includes/modules/heatmap.php +++ b/includes/heatmap.php @@ -5,6 +5,8 @@ * @package Better_Search */ +use WebberZone\Better_Search\Util\Helpers; + // If this file is called directly, then abort execution. if ( ! defined( 'WPINC' ) ) { die; @@ -53,7 +55,7 @@ function the_bsearch_heatmap( $args = array() ) { $output .= $args['before'] . $heatmap . $args['after']; if ( bsearch_get_option( 'show_credit' ) ) { - $output .= bsearch_get_credit_link(); + $output .= Helpers::get_credit_link(); } $output .= ''; @@ -220,8 +222,8 @@ function ( $a, $b ): int { } // Calculate colors. - $hotdec = bsearch_html2rgb( $args['hot'] ); - $colddec = bsearch_html2rgb( $args['cold'] ); + $hotdec = Helpers::html2rgb( $args['hot'] ); + $colddec = Helpers::html2rgb( $args['cold'] ); for ( $i = 0; $i < 3; $i++ ) { $coldval[] = $colddec[ $i ]; $hotval[] = $hotdec[ $i ]; @@ -277,7 +279,7 @@ function ( $a, $b ): int { */ $class = apply_filters( 'bsearch_heatmap_class', $class, $searchvar, $result ); - $formatted_count = sprintf( translate_nooped_plural( $translate_nooped_plural, $count ), bsearch_number_format_i18n( $count ) ); + $formatted_count = sprintf( translate_nooped_plural( $translate_nooped_plural, $count ), Helpers::number_format_i18n( $count ) ); $title = ''; @@ -308,7 +310,7 @@ function ( $a, $b ): int { $target, $aria_label ? sprintf( ' aria-label="%1$s (%2$s)"', esc_attr( $result->name ), esc_attr( $formatted_count ) ) : '', $searchvar, - $args['show_count'] ? ' (' . bsearch_number_format_i18n( $count ) . ')' : '', + $args['show_count'] ? ' (' . Helpers::number_format_i18n( $count ) . ')' : '', $args['after_term'] ); } @@ -359,11 +361,7 @@ function get_bsearch_heatmap_counts( $args = array() ) { // Parse incomming $args into an array and merge it with $defaults. $args = wp_parse_args( $args, $defaults ); - $table_name = $wpdb->prefix . 'bsearch'; - - if ( $args['daily'] ) { - $table_name .= '_daily'; - } + $table_name = Helpers::get_bsearch_table( $args['daily'] ); if ( ! $args['daily'] ) { $sargs = array( @@ -378,7 +376,7 @@ function get_bsearch_heatmap_counts( $args = array() ) { LIMIT %d "; } else { - $current_date = bsearch_get_from_date( null, $args['daily_range'] ); + $current_date = Helpers::get_from_date( null, $args['daily_range'] ); $sargs = array( $current_date, @@ -407,54 +405,3 @@ function get_bsearch_heatmap_counts( $args = array() ) { */ return apply_filters( 'get_bsearch_heatmap_counts', $results, $args ); } - - -/** - * Manual Daily Better Search Heatmap. - * - * @since 1.0 - * - * @return string Daily search heatmap - */ -function get_bsearch_pop_daily() { - _deprecated_function( __FUNCTION__, '3.0.0', 'the_bsearch_heatmap' ); - - return the_bsearch_heatmap( 'daily=1&echo=0' ); -} - - -/** - * Echo daily popular searches. - * - * @since 1.0 - */ -function the_pop_searches_daily() { - _deprecated_function( __FUNCTION__, '3.0.0', 'the_bsearch_heatmap' ); - - return the_bsearch_heatmap( 'daily=1' ); -} - -/** - * Manual Overall Better Search Heatmap. - * - * @since 1.0 - * - * @return $string Popular searches heatmap - */ -function get_bsearch_pop() { - _deprecated_function( __FUNCTION__, '3.0.0', 'the_bsearch_heatmap' ); - - return the_bsearch_heatmap( 'echo=0' ); -} - - -/** - * Echo popular searches list. - * - * @since 1.0 - */ -function the_pop_searches() { - _deprecated_function( __FUNCTION__, '3.0.0', 'the_bsearch_heatmap' ); - - the_bsearch_heatmap(); -} diff --git a/includes/index.php b/includes/index.php deleted file mode 100644 index 8142269..0000000 --- a/includes/index.php +++ /dev/null @@ -1 +0,0 @@ - Media screen. - * - * When using the_post_thumbnail() or related functions, the 'post-thumbnail' image - * size is used by default, though a different size can be specified instead as needed. - * - * @since 2.5.0 - * @since 3.0.0 `postid` argument renamed to `post`. - * New `size` attribute added which can be a registered image size name or array of width and height. - * `thumb_width` and `thumb_height` attributes removed. These are extracted based on size. - * - * @param string|array $args { - * Optional. Array or string of parameters. - * - * @type int|WP_Post $post Post ID or object. - * @type string $size Size of the thumbnail. - * @type string $thumb_meta Meta field that is used to store the location of default thumbnail image. - * @type string $thumb_html HTML / CSS for width and height attributes. - * @type string $thumb_default URL of default thumbnail image. - * @type bool $thumb_default_show Whether to show default thumbnail image. - * @type bool $scan_images Scan post content for the first image. - * @type string $class Thumbnail class. - * } - * @return string Output with the post thumbnail - */ -function bsearch_get_the_post_thumbnail( $args = array() ) { - - $defaults = array( - 'post' => '', - 'size' => 'thumbnail', - 'thumb_meta' => 'post-image', - 'thumb_html' => 'html', - 'thumb_default' => '', - 'thumb_default_show' => true, - 'scan_images' => true, - 'class' => 'bsearch_thumb', - ); - - // Parse incomming $args into an array and merge it with $defaults. - $args = wp_parse_args( $args, $defaults ); - - $result = get_post( $args['post'] ); - - if ( empty( $result ) ) { - return ''; - } - - if ( is_string( $args['size'] ) ) { - list( $args['thumb_width'], $args['thumb_height'] ) = bsearch_get_thumb_size( $args['size'] ); - } else { - $args['thumb_width'] = $args['size'][0]; - $args['thumb_height'] = $args['size'][1]; - } - - $post_title = esc_attr( $result->post_title ); - - $output = ''; - $postimage = ''; - $pick = ''; - - // Let's start fetching the thumbnail. First place to look is in the post meta defined in the Settings page. - if ( ! $postimage ) { - $postimage = get_post_meta( $result->ID, $args['thumb_meta'], true ); - $pick = 'meta'; - if ( $postimage ) { - $attachment_id = bsearch_get_attachment_id_from_url( $postimage ); - - $postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] ); - if ( false !== $postthumb ) { - list( $postimage, $args['thumb_width'], $args['thumb_height'] ) = $postthumb; - $pick .= 'correct'; - } - } - } - - // If there is no thumbnail found, check the post thumbnail. - if ( ! $postimage ) { - if ( false !== get_post_thumbnail_id( $result->ID ) ) { - $attachment_id = ( 'attachment' === $result->post_type ) ? $result->ID : get_post_thumbnail_id( $result->ID ); - - $postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] ); - if ( false !== $postthumb ) { - list( $postimage, $args['thumb_width'], $args['thumb_height'] ) = $postthumb; - $pick = 'featured'; - } - } - } - - // If there is no thumbnail found, fetch the first image in the post, if enabled. - if ( ! $postimage && $args['scan_images'] ) { - - /** - * Filters the post content that is used to scan for images. - * - * A filter function can be tapped into this to execute shortcodes, modify content, etc. - * - * @since 2.5.0 - * - * @param string $result->post_content Post content - * @param object $result Post Object - */ - $post_content = apply_filters( 'bsearch_thumb_post_content', $result->post_content, $result ); - - preg_match_all( '//i', $post_content, $matches ); - if ( isset( $matches[1][0] ) && $matches[1][0] ) { // any image there? - $postimage = $matches[1][0]; // we need the first one only! - } - $pick = 'first'; - if ( $postimage ) { - $attachment_id = bsearch_get_attachment_id_from_url( $postimage ); - - $postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] ); - if ( false !== $postthumb ) { - list( $postimage, $args['thumb_width'], $args['thumb_height'] ) = $postthumb; - $pick .= 'correct'; - } - } - } - - // If there is no thumbnail found, fetch the first child image. - if ( ! $postimage ) { - $postimage = bsearch_get_first_image( $result->ID, $args['thumb_width'], $args['thumb_height'] ); - $pick = 'firstchild'; - } - - // If no other thumbnail set, try to get the custom video thumbnail set by the Video Thumbnails plugin. - if ( ! $postimage ) { - $postimage = get_post_meta( $result->ID, '_video_thumbnail', true ); - $pick = 'video_thumb'; - } - - // If no thumb found and settings permit, use default thumb. - if ( ! $postimage && $args['thumb_default_show'] ) { - $postimage = $args['thumb_default']; - $pick = 'default_thumb'; - } - - // If no thumb found and settings permit, use site icon. - if ( ! $postimage ) { - $postimage = get_site_icon_url( max( $args['thumb_width'], $args['thumb_height'] ) ); - $pick = 'site_icon_max'; - } - - if ( ! $postimage ) { - $postimage = get_site_icon_url( min( $args['thumb_width'], $args['thumb_height'] ) ); - $pick = 'site_icon_min'; - } - - // Hopefully, we've found a thumbnail by now. If so, run it through the custom filter, check for SSL and create the image tag. - if ( $postimage ) { - - /** - * Filters the thumbnail image URL. - * - * @since 2.5.0 - * @since 3.0.0 Second argument changed to $args array and third argument changed to Post object. - * - * @param string $postimage URL of the thumbnail image - * @param array $args Arguments array. - * @param WP_Post $result Post Object. - */ - $postimage = apply_filters( 'bsearch_thumb_url', $postimage, $args, $result ); - - if ( is_ssl() ) { - $postimage = preg_replace( '~http://~', 'https://', $postimage ); - } - - $class = $args['class'] . ' bsearch_' . $pick; - - /** - * Filters the thumbnail classes and allows a filter function to add any more classes if needed. - * - * @since 2.5.0 - * @since 3.1.0 Added $result - * - * @param string $thumb_html Thumbnail HTML - * @param array $args Argument array - * @param WP_Post $result Post Object. - */ - $attr['class'] = apply_filters( 'bsearch_thumb_class', $class, $args, $result ); - - /** - * Filters the thumbnail alt. - * - * @since 2.5.0 - * @since 3.1.0 Added $result - * - * @param string $post_title Thumbnail alt attribute - * @param array $args Argument array - * @param WP_Post $result Post Object. - */ - $attr['alt'] = apply_filters( 'bsearch_thumb_alt', $post_title, $args, $result ); - - /** - * Filters the thumbnail title. - * - * @since 2.5.0 - * @since 3.1.0 Added $args and $result - * - * @param string $post_title Thumbnail title attribute - * @param array $args Argument array - * @param WP_Post $result Post Object. - */ - $attr['title'] = apply_filters( 'bsearch_thumb_title', $post_title, $args, $result ); - - $attr['thumb_html'] = $args['thumb_html']; - $attr['thumb_width'] = $args['thumb_width']; - $attr['thumb_height'] = $args['thumb_height']; - - $output .= bsearch_get_image_html( $postimage, $attr ); - - if ( function_exists( 'wp_img_tag_add_srcset_and_sizes_attr' ) ) { - if ( empty( $attachment_id ) ) { - $attachment_id = bsearch_get_attachment_id_from_url( $postimage ); - } - - if ( ! empty( $attachment_id ) ) { - $output = wp_img_tag_add_srcset_and_sizes_attr( $output, 'bsearch_thumbnail', $attachment_id ); - } - } - - if ( function_exists( 'wp_img_tag_add_loading_attr' ) ) { - $output = wp_img_tag_add_loading_attr( $output, 'bsearch_thumbnail' ); - } - } - - /** - * Filters post thumbnail created for Better Search. - * - * @since 2.5.0 - * - * @param array $output Formatted output - * @param array $args Argument array - */ - return apply_filters( 'bsearch_get_the_post_thumbnail', $output, $args ); -} - - -/** - * Get the first child image in the post. - * - * @since 2.5.0 - * - * @param mixed $post_id Post ID. - * @param int $thumb_width Thumb width. - * @param int $thumb_height Thumb height. - * @return string - */ -function bsearch_get_first_image( $post_id, $thumb_width, $thumb_height ) { - - $args = array( - 'numberposts' => 1, - 'order' => 'ASC', - 'post_mime_type' => 'image', - 'post_parent' => $post_id, - 'post_status' => null, - 'post_type' => 'attachment', - ); - - $attachments = get_children( $args ); - - if ( $attachments ) { - foreach ( $attachments as $attachment ) { - $image_attributes = wp_get_attachment_image_src( $attachment->ID, array( $thumb_width, $thumb_height ) ) ? wp_get_attachment_image_src( $attachment->ID, array( $thumb_width, $thumb_height ) ) : wp_get_attachment_image_src( $attachment->ID, 'full' ); - - /** - * Filters first child image from the post. - * - * @since 2.5.0 - * - * @param array $image_attributes[0] URL of the image - * @param int $post_id Post ID - * @param int $thumb_width Thumb width - * @param int $thumb_height Thumb height - */ - return apply_filters( 'bsearch_get_first_image', $image_attributes[0], $post_id, $thumb_width, $thumb_height ); - } - } else { - return false; - } -} - - -/** - * Get an HTML img element - * - * @since 2.5.0 - * - * @param string $attachment_url Image URL. - * @param array $attr Attributes for the image markup. - * @return string HTML img element or empty string on failure. - */ -function bsearch_get_image_html( $attachment_url, $attr = array() ) { - - // If there is no url, return. - if ( '' == $attachment_url ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - return; - } - - $default_attr = array( - 'src' => $attachment_url, - 'thumb_html' => 'html', - 'thumb_width' => '', - 'thumb_height' => '', - ); - - $attr = wp_parse_args( $attr, $default_attr ); - - $hwstring = bsearch_get_image_hwstring( $attr ); - - // Generate 'srcset' and 'sizes' if not already present. - if ( empty( $attr['srcset'] ) ) { - $attachment_id = bsearch_get_attachment_id_from_url( $attachment_url ); - $image_meta = wp_get_attachment_metadata( $attachment_id ); - - if ( is_array( $image_meta ) ) { - $size_array = array( absint( $attr['thumb_width'] ), absint( $attr['thumb_height'] ) ); - $srcset = wp_calculate_image_srcset( $size_array, $attachment_url, $image_meta, $attachment_id ); - $sizes = wp_calculate_image_sizes( $size_array, $attachment_url, $image_meta, $attachment_id ); - - if ( $srcset && ( $sizes || ! empty( $attr['sizes'] ) ) ) { - $attr['srcset'] = $srcset; - - if ( empty( $attr['sizes'] ) ) { - $attr['sizes'] = $sizes; - } - } - } - } - - // Unset attributes we don't want to display. - unset( $attr['thumb_html'] ); - unset( $attr['thumb_width'] ); - unset( $attr['thumb_height'] ); - - /** - * Filters the list of attachment image attributes. - * - * @since 2.5.0 - * - * @param array $attr Attributes for the image markup. - * @param string $attachment_url Image URL. - */ - $attr = apply_filters( 'bsearch_get_image_attributes', $attr, $attachment_url ); - $attr = array_map( 'esc_attr', $attr ); - - $html = ' $value ) { - $html .= " $name=" . '"' . $value . '"'; - } - $html .= ' />'; - - return apply_filters( 'bsearch_get_image_html', $html ); -} - - -/** - * Retrieve width and height attributes using given width and height values. - * - * @since 2.5.0 - * - * @param array $args Argument array. - * - * @return string Height-width string. - */ -function bsearch_get_image_hwstring( $args = array() ) { - - $default_args = array( - 'thumb_html' => 'html', - 'thumb_width' => '', - 'thumb_height' => '', - ); - $args = wp_parse_args( $args, $default_args ); - - $thumb_html = ''; - - if ( 'css' === $args['thumb_html'] ) { - if ( $args['thumb_width'] ) { - $thumb_html .= 'max-width:' . (int) $args['thumb_width'] . 'px;'; - } - if ( $args['thumb_height'] ) { - $thumb_html .= 'max-height:' . (int) $args['thumb_height'] . 'px;'; - } - if ( ! empty( $thumb_html ) ) { - $thumb_html = ' style="' . $thumb_html . '" '; - } - } elseif ( 'html' === $args['thumb_html'] ) { - if ( $args['thumb_width'] ) { - $thumb_html .= 'width="' . (int) $args['thumb_width'] . '" '; - } - if ( $args['thumb_height'] ) { - $thumb_html .= 'height="' . (int) $args['thumb_height'] . '" '; - } - } - - /** - * Filters the thumbnail HTML and allows a filter function to add any more HTML if needed. - * - * @since 2.5.0 - * - * @param string $thumb_html Thumbnail HTML. - * @param array $args Argument array. - */ - return apply_filters( 'bsearch_thumb_html', $thumb_html, $args ); -} - - -/** - * Function to get the attachment ID from the attachment URL. - * - * @since 2.5.0 - * - * @param string $attachment_url Attachment URL. - * @return int Attachment ID - */ -function bsearch_get_attachment_id_from_url( $attachment_url = '' ) { - - global $wpdb; - $attachment_id = false; - - // If there is no url, return. - if ( '' === $attachment_url ) { - return; - } - - // Get the upload directory paths. - $upload_dir_paths = wp_upload_dir(); - - // Make sure the upload path base directory exists in the attachment URL, to verify that we're working with a media library image. - if ( false !== strpos( $attachment_url, $upload_dir_paths['baseurl'] ) ) { - - // If this is the URL of an auto-generated thumbnail, get the URL of the original image. - $attachment_url = preg_replace( '/-\d+x\d+(?=\.(jpg|jpeg|png|gif|webp)$)/i', '', $attachment_url ); - - // Remove the upload path base directory from the attachment URL. - $attachment_url = str_replace( $upload_dir_paths['baseurl'] . '/', '', $attachment_url ); - - // Finally, run a custom database query to get the attachment ID from the modified attachment URL. - $attachment_id = $wpdb->get_var( $wpdb->prepare( "SELECT wposts.ID FROM $wpdb->posts wposts, $wpdb->postmeta wpostmeta WHERE wposts.ID = wpostmeta.post_id AND wpostmeta.meta_key = '_wp_attached_file' AND wpostmeta.meta_value = %s AND wposts.post_type = 'attachment'", $attachment_url ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - - } - - /** - * Filters attachment ID generated from URL. - * - * @since 2.5.0 - * - * @param int $attachment_id Attachment ID - * @param string $attachment_url Attachment URL - */ - return apply_filters( 'bsearch_get_attachment_id_from_url', $attachment_id, $attachment_url ); -} - - -/** - * Function to get the correct height and width of the thumbnail. - * - * @since 2.5.0 - * @since 3.0.0 First argument is a string. - * - * @param string $size Image size. - * @return array Width and height. If no width and height is found, then 150 is returned for each. - */ -function bsearch_get_thumb_size( $size = 'thumbnail' ) { - - if ( is_string( $size ) ) { - $thumb_size = $size; - } elseif ( isset( $size['thumb_size'] ) ) { - $thumb_size = $size['thumb_size']; - } - - // Get thumbnail size. - $bsearch_thumb_size = bsearch_get_all_image_sizes( $thumb_size ); - - if ( isset( $bsearch_thumb_size['width'] ) ) { - $thumb_width = $bsearch_thumb_size['width']; - $thumb_height = $bsearch_thumb_size['height']; - } - - if ( isset( $thumb_width ) && isset( $thumb_height ) ) { - $thumb_size = array( $thumb_width, $thumb_height ); - } else { - $thumb_size = array( 150, 150 ); - } - - /** - * Filter array of thumbnail size. - * - * @since 2.5.0 - * - * @param array $thumb_size Array with width and height of thumbnail - */ - return apply_filters( 'bsearch_get_thumb_size', $thumb_size ); -} - - -/** - * Get all image sizes. - * - * @since 2.5.0 - * @param string $size Get specific image size. - * @return array Image size names along with width, height and crop setting - */ -function bsearch_get_all_image_sizes( $size = '' ) { - global $_wp_additional_image_sizes; - - /* Get the intermediate image sizes and add the full size to the array. */ - $intermediate_image_sizes = get_intermediate_image_sizes(); - - foreach ( $intermediate_image_sizes as $_size ) { - if ( in_array( $_size, array( 'thumbnail', 'medium', 'large' ), true ) ) { - - $sizes[ $_size ]['name'] = $_size; - $sizes[ $_size ]['width'] = absint( get_option( $_size . '_size_w' ) ); - $sizes[ $_size ]['height'] = absint( get_option( $_size . '_size_h' ) ); - $sizes[ $_size ]['crop'] = (bool) get_option( $_size . '_crop' ); - - if ( ( 0 === $sizes[ $_size ]['width'] ) && ( 0 === $sizes[ $_size ]['height'] ) ) { - unset( $sizes[ $_size ] ); - } - } elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) { - - $sizes[ $_size ] = array( - 'name' => $_size, - 'width' => $_wp_additional_image_sizes[ $_size ]['width'], - 'height' => $_wp_additional_image_sizes[ $_size ]['height'], - 'crop' => (bool) $_wp_additional_image_sizes[ $_size ]['crop'], - ); - } - } - - /* Get only 1 size if found */ - if ( $size ) { - if ( isset( $sizes[ $size ] ) ) { - return $sizes[ $size ]; - } else { - return false; - } - } - - /** - * Filters array of image sizes. - * - * @since 2.5.0 - * - * @param array $sizes Image sizes - */ - return apply_filters( 'bsearch_get_all_image_sizes', $sizes ); -} diff --git a/includes/modules/cache.php b/includes/modules/cache.php deleted file mode 100644 index 2545fd5..0000000 --- a/includes/modules/cache.php +++ /dev/null @@ -1,108 +0,0 @@ - 1, - /* translators: 1: Number of entries. */ - 'message' => sprintf( _n( '%s entry cleared', '%s entries cleared', $count, 'text-domain' ), number_format_i18n( $count ) ), - ) - ) - ); -} -add_action( 'wp_ajax_bsearch_clear_cache', 'bsearch_ajax_clearcache' ); - - -/** - * Get the meta key based on a list of parameters. - * - * @since 3.0.0 - * - * @param mixed $attr Array of attributes typically. - * @param string $context Context of the cache key to be set. - * @return string Cache meta key - */ -function bsearch_cache_get_key( $attr, $context = 'query' ) { - - $key = sprintf( 'bs_cache_%1$s_%2$s', md5( wp_json_encode( $attr ) ), $context ); - - return $key; -} - - -/** - * Get the transient names for Better Search. - * - * @since 3.0.0 - * - * @return array Better Search Cache keys. - */ -function bsearch_cache_get_keys() { - global $wpdb; - - $keys = array(); - - $sql = " - SELECT option_name - FROM {$wpdb->options} - WHERE `option_name` LIKE '_transient_bs_%' - "; - - $results = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared - - if ( is_array( $results ) ) { - foreach ( $results as $result ) { - $keys[] = str_replace( '_transient_', '', $result->option_name ); - } - } - - return apply_filters( 'bsearch_cache_get_keys', $keys ); -} diff --git a/includes/modules/index.php b/includes/modules/index.php deleted file mode 100644 index 8142269..0000000 --- a/includes/modules/index.php +++ /dev/null @@ -1 +0,0 @@ - false, - 'daily_range' => absint( bsearch_get_option( 'daily_range' ) ), - 'smallest' => absint( bsearch_get_option( 'heatmap_smallest' ) ), - 'largest' => absint( bsearch_get_option( 'heatmap_largest' ) ), - 'unit' => bsearch_get_option( 'heatmap_unit', 'pt' ), - 'hot' => bsearch_get_option( 'heatmap_hot' ), - 'cold' => bsearch_get_option( 'heatmap_cold' ), - 'number' => absint( bsearch_get_option( 'heatmap_limit' ) ), - 'before_term' => bsearch_get_option( 'heatmap_before' ), - 'after_term' => bsearch_get_option( 'heatmap_after' ), - 'link_nofollow' => bsearch_get_option( 'link_nofollow' ), - 'link_new_window' => bsearch_get_option( 'link_new_window' ), - 'format' => 'flat', - 'separator' => "\n", - 'orderby' => 'count', - 'order' => 'RAND', - 'topic_count_text' => null, - 'show_count' => 0, - 'no_results_text' => __( 'No searches made yet', 'better-search' ), - ), - $atts, - 'bsearch_heatmap' - ); - - return get_bsearch_heatmap( $atts ); -} -add_shortcode( 'bsearch_heatmap', 'bsearch_heatmap_func' ); - -/** - * Shortcode to get the Better Search form - * - * @since 2.2.0 - * - * @param array $atts Search form attributes. - * @return string HTML output of the search form - */ -function bsearch_search_form_func( $atts ) { - - $atts = shortcode_atts( - array( - 'before' => '', - 'after' => '', - 'aria_label' => '', - 'post_types' => bsearch_get_option( 'post_types' ), - 'selected_post_types' => '', - 'show_post_types' => false, - ), - $atts, - 'bsearch_form' - ); - - return get_bsearch_form( '', $atts ); -} -add_shortcode( 'bsearch_form', 'bsearch_search_form_func' ); diff --git a/includes/modules/tracker.php b/includes/modules/tracker.php deleted file mode 100644 index 3c56dbe..0000000 --- a/includes/modules/tracker.php +++ /dev/null @@ -1,208 +0,0 @@ - $home_url, - 'bsearch_search_query' => $search_query, - 'bsearch_rnd' => wp_rand( 1, time() ), - ); - - /** - * Filter the localize script arguments for the Better Search tracker. - * - * @since 2.2.4 - */ - $ajax_bsearch_tracker = apply_filters( 'bsearch_tracker_script_args', $ajax_bsearch_tracker ); - - wp_enqueue_script( 'bsearch_tracker', plugins_url( 'includes/js/better-search-tracker.min.js', BETTER_SEARCH_PLUGIN_FILE ), array( 'jquery' ), BETTER_SEARCH_VERSION, true ); - - wp_localize_script( 'bsearch_tracker', 'ajax_bsearch_tracker', $ajax_bsearch_tracker ); - - } -} -add_action( 'wp_enqueue_scripts', 'bsearch_enqueue_scripts' ); - - -/** - * Function to add additional queries to query_vars. - * - * @since 2.2.4 - * - * @param array $vars Query variables array. - * @return array Query variables array with Better Search parameters appended - */ -function bsearch_query_vars( $vars ) { - // Add these to the list of queryvars that WP gathers. - $vars[] = 'bsearch_search_query'; - - /** - * Function to add additional queries to query_vars. - * - * @since 2.2.4 - * - * @param array $vars Updated Query variables array with Better Search queries added. - */ - return apply_filters( 'bsearch_query_vars', $vars ); -} -add_filter( 'query_vars', 'bsearch_query_vars' ); - - -/** - * Parses the WordPress object to update/display the count. - * - * @since 2.2.4 - * - * @param object $wp WordPress object. - */ -function bsearch_parse_request( $wp ) { - - if ( empty( $wp ) ) { - global $wp; - } - - if ( ! isset( $wp->query_vars ) || ! is_array( $wp->query_vars ) ) { - return; - } - - if ( array_key_exists( 'bsearch_search_query', $wp->query_vars ) && empty( $wp->query_vars['bsearch_search_query'] ) ) { - exit; - } - - if ( array_key_exists( 'bsearch_search_query', $wp->query_vars ) && ! empty( $wp->query_vars['bsearch_search_query'] ) ) { - - $search_query = isset( $wp->query_vars['bsearch_search_query'] ) ? rawurldecode( wp_kses( wp_unslash( $wp->query_vars['bsearch_search_query'] ), array() ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - $str = bsearch_update_count( $search_query ); - - header( 'content-type: application/x-javascript' ); - wp_send_json( $str ); - - // Stop anything else from loading as it is not needed. - exit; - - } else { - return; - } -} -add_action( 'parse_request', 'bsearch_parse_request' ); - - -/** - * Function to update the count in the database. - * - * @since 2.2.4 - * - * @param string $search_query Search Query. - * - * @return string Response on database update. - */ -function bsearch_update_count( $search_query ) { - - global $wpdb; - - $table_name = $wpdb->prefix . 'bsearch'; - $table_name_daily = $wpdb->prefix . 'bsearch_daily'; - $search_query = str_replace( '"', '"', $search_query ); - $str = ''; - - if ( '' !== $search_query ) { - $results = $wpdb->get_results( $wpdb->prepare( "SELECT searchvar, cntaccess FROM $table_name WHERE searchvar = %s LIMIT 1 ", $search_query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $test = 0; - if ( $results ) { - foreach ( $results as $result ) { - $tt = $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET cntaccess = cntaccess + 1 WHERE searchvar = %s ", $result->searchvar ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $str .= ( false === $tt ) ? 'e_' : 's_' . $tt; - $test = 1; - } - } - if ( 0 === $test ) { - $tt = $wpdb->query( $wpdb->prepare( "INSERT INTO $table_name (searchvar, cntaccess) VALUES( %s, '1') ", $search_query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $str .= ( false === $tt ) ? 'e_' : 's_' . $tt; - } - - // Now update daily count. - $current_date = gmdate( 'Y-m-d', ( time() + ( get_option( 'gmt_offset' ) * 3600 ) ) ); - - $results = $wpdb->get_results( $wpdb->prepare( "SELECT searchvar, cntaccess, dp_date FROM $table_name_daily WHERE searchvar = %s AND dp_date = %s ", $search_query, $current_date ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $test = 0; - if ( $results ) { - foreach ( $results as $result ) { - $ttd = $wpdb->query( $wpdb->prepare( "UPDATE $table_name_daily SET cntaccess = cntaccess + 1 WHERE searchvar = %s AND dp_date = %s ", $result->searchvar, $current_date ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $str .= ( false === $ttd ) ? '_e' : '_s' . $ttd; - $test = 1; - } - } - if ( 0 === $test ) { - $ttd = $wpdb->query( $wpdb->prepare( "INSERT INTO $table_name_daily (searchvar, cntaccess, dp_date) VALUES( %s, '1', %s )", $search_query, $current_date ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $str .= ( false === $ttd ) ? '_e' : '_s' . $ttd; - } - } - - /** - * Filter the response on database update. - * - * @since 2.2.4 - * - * @param string $str Response string. - * @param int $search_query Search query. - */ - return apply_filters( 'bsearch_update_count', $str, $search_query ); -} diff --git a/includes/admin/register-settings.php b/includes/options-api.php similarity index 59% rename from includes/admin/register-settings.php rename to includes/options-api.php index bb7cc7b..6421c35 100644 --- a/includes/admin/register-settings.php +++ b/includes/options-api.php @@ -1,15 +1,10 @@ $settings ) { - - add_settings_section( - 'bsearch_settings_' . $section, // ID used to identify this section and with which to register options, e.g. bsearch_settings_general. - __return_null(), // No title, we will handle this via a separate function. - '__return_false', // No callback function needed. We'll process this separately. - 'bsearch_settings_' . $section // Page on which these options will be added. - ); - - foreach ( $settings as $setting ) { - - $args = wp_parse_args( - $setting, - array( - 'section' => $section, - 'id' => null, - 'name' => '', - 'desc' => '', - 'type' => null, - 'options' => '', - 'max' => null, - 'min' => null, - 'step' => null, - 'size' => null, - 'field_class' => '', - 'field_attributes' => '', - 'placeholder' => '', - ) - ); - - add_settings_field( - 'bsearch_settings[' . $args['id'] . ']', // ID of the settings field. We save it within the bsearch_settings array. - $args['name'], // Label of the setting. - function_exists( 'bsearch_' . $args['type'] . '_callback' ) ? 'bsearch_' . $args['type'] . '_callback' : 'bsearch_missing_callback', // Function to handle the setting. - 'bsearch_settings_' . $section, // Page to display the setting. In our case it is the section as defined above. - 'bsearch_settings_' . $section, // Name of the section. - $args - ); - } - } - - // Register the settings into the options table. - register_setting( 'bsearch_settings', 'bsearch_settings', 'bsearch_settings_sanitize' ); -} -add_action( 'admin_init', 'bsearch_register_settings' ); - - /** * Flattens bsearch_get_registered_settings() into $setting[id] => $setting[type] format. * @@ -227,7 +185,7 @@ function bsearch_get_registered_settings_types() { $options = array(); // Populate some default values. - foreach ( bsearch_get_registered_settings() as $tab => $settings ) { + foreach ( \WebberZone\Better_Search\Admin\Settings\Settings::get_registered_settings() as $tab => $settings ) { foreach ( $settings as $option ) { $options[ $option['id'] ] = $option['type']; } @@ -256,7 +214,7 @@ function bsearch_settings_defaults() { $options = array(); // Populate some default values. - foreach ( bsearch_get_registered_settings() as $tab => $settings ) { + foreach ( \WebberZone\Better_Search\Admin\Settings\Settings::get_registered_settings() as $tab => $settings ) { foreach ( $settings as $option ) { // When checkbox is set to true, set this to 1. if ( 'checkbox' === $option['type'] && ! empty( $option['options'] ) ) { @@ -274,12 +232,6 @@ function bsearch_settings_defaults() { } } - $upgraded_settings = bsearch_upgrade_settings(); - - if ( false !== $upgraded_settings ) { - $options = array_merge( $options, $upgraded_settings ); - } - /** * Filters the default settings array. * @@ -301,10 +253,10 @@ function bsearch_settings_defaults() { */ function bsearch_get_default_option( $key = '' ) { - $default_settings = bsearch_settings_defaults(); + $default_value_settings = bsearch_settings_defaults(); - if ( array_key_exists( $key, $default_settings ) ) { - return $default_settings[ $key ]; + if ( array_key_exists( $key, $default_value_settings ) ) { + return $default_value_settings[ $key ]; } else { return false; } @@ -321,24 +273,3 @@ function bsearch_get_default_option( $key = '' ) { function bsearch_settings_reset() { delete_option( 'bsearch_settings' ); } - -/** - * Upgrade pre v2.5.0 settings. - * - * @since v2.5.0 - * @return array Settings array - */ -function bsearch_upgrade_settings() { - $old_settings = get_option( 'ald_bsearch_settings' ); - - if ( empty( $old_settings ) ) { - return false; - } - - // Start will assigning all the old settings to the new settings and we will unset later on. - $settings = $old_settings; - - $settings['custom_css'] = $old_settings['custom_CSS']; - - return $settings; -} diff --git a/includes/template-redirect.php b/includes/template-redirect.php deleted file mode 100644 index f50d58f..0000000 --- a/includes/template-redirect.php +++ /dev/null @@ -1,129 +0,0 @@ -is_404 ) { - // Set status of 404 to false. - $wp_query->is_404 = false; - $wp_query->is_archive = true; - } - - // Change status code to 200 OK since /search/ returns status code 404. - @header( 'HTTP/1.1 200 OK', 1 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - @header( 'Status: 200 OK', 1 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - - // Added necessary code to the head. - add_action( 'wp_head', 'bsearch_head' ); - - // Set the title. - add_filter( 'wp_title', 'bsearch_title' ); - - // If there is a template file within the parent or child theme then we use it. - $priority_template_lookup = array( - get_stylesheet_directory() . '/better-search-template.php', - get_template_directory() . '/better-search-template.php', - plugin_dir_path( __DIR__ ) . 'templates/template.php', - ); - - foreach ( $priority_template_lookup as $exists ) { - - if ( file_exists( $exists ) ) { - - return $exists; - - } - } - - return $template; -} -add_action( 'template_include', 'bsearch_template_redirect', 1 ); - - -/** - * Insert styles into WordPress Head. Filters `wp_head`. - * - * @since 1.0 - */ -function bsearch_head() { - - $bsearch_custom_css = stripslashes( bsearch_get_option( 'custom_css' ) ); - - // Add custom CSS to header. - if ( ( '' != $bsearch_custom_css ) && is_search() ) { //phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - echo ''; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - - // Add noindex to search results page. - if ( bsearch_get_option( 'meta_noindex' ) ) { - echo ''; - } -} - - -/** - * Change page title. Filters `wp_title`. - * - * @since 1.0 - * - * @param string $title Title of the page. - * @return string Filtered title of the page - */ -function bsearch_title( $title ) { - - if ( ! is_search() ) { - return $title; - } - - $search_query = get_bsearch_query(); - - if ( isset( $search_query ) ) { - /* translators: 1: search query, 2: title of the page */ - $bsearch_title = sprintf( __( 'Search Results for "%1$s" | %2$s', 'better-search' ), $search_query, $title ); - - } - - /** - * Filters the title of the page - * - * @since 2.0.0 - * - * @param string $bsearch_title Title of the page set by Better Search - * @param string $title Original Title of the page - * @param string $search_query Search query - */ - return apply_filters( 'bsearch_title', $bsearch_title, $title, $search_query ); -} diff --git a/includes/util/class-cache.php b/includes/util/class-cache.php new file mode 100644 index 0000000..6393fd0 --- /dev/null +++ b/includes/util/class-cache.php @@ -0,0 +1,126 @@ +delete(); + + exit( + wp_json_encode( + array( + 'success' => 1, + /* translators: 1: Number of entries. */ + 'message' => sprintf( _n( '%s entry cleared', '%s entries cleared', $count, 'better-search' ), number_format_i18n( $count ) ), + ) + ) + ); + } + + /** + * Delete the Top 10 cache. + * + * @since 3.3.0 + * + * @param array $transients Array of transients to delete. + * @return int Number of transients deleted. + */ + public static function delete( $transients = array() ) { + $loop = 0; + + $default_transients = self::get_keys(); + + if ( ! empty( $transients ) ) { + $transients = array_intersect( $default_transients, (array) $transients ); + } else { + $transients = $default_transients; + } + + foreach ( $transients as $transient ) { + $del = delete_transient( $transient ); + if ( $del ) { + ++$loop; + } + } + return $loop; + } + + /** + * Get the default meta keys used for the cache + * + * @return array Transient meta keys + */ + public static function get_keys() { + + global $wpdb; + + $keys = array(); + + $sql = " + SELECT option_name + FROM {$wpdb->options} + WHERE `option_name` LIKE '_transient_bs_%' + "; + + $results = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + + if ( is_array( $results ) ) { + foreach ( $results as $result ) { + $keys[] = str_replace( '_transient_', '', $result->option_name ); + } + } + + return apply_filters( 'bsearch_cache_get_keys', $keys ); + } + + /** + * Get the meta key based on a list of parameters. + * + * @param mixed $attr Array of attributes typically. + * @param string $context Context of the cache key to be set. + * @return string Cache meta key + */ + public static function get_key( $attr, $context = 'query' ) { + + $key = sprintf( 'bs_cache_%1$s_%2$s', md5( wp_json_encode( $attr ) ), $context ); + + return $key; + } +} diff --git a/includes/util/class-helpers.php b/includes/util/class-helpers.php new file mode 100644 index 0000000..be4bfd9 --- /dev/null +++ b/includes/util/class-helpers.php @@ -0,0 +1,598 @@ +prefix . 'bsearch'; + if ( $daily ) { + $table_name .= '_daily'; + } + return $table_name; + } + + /** + * Clean search string from XSS exploits. + * + * @since 3.3.0 + * + * @param string $val Potentially unclean string. + * @return string Cleaned string if successful or empty string on use of banned word + */ + public static function clean_terms( $val ) { + global $bsearch_error; + + // Instantiate WP_Error class. + $bsearch_error = new \WP_Error(); + + $val = rawurldecode( $val ); + + $badwords = array_map( 'trim', explode( ',', bsearch_get_option( 'badwords' ) ) ); + + $censor_char = ''; + + /** + * Allow the censored character to be replaced. + * + * @since 2.1.0 + * + * @param string $censor_char Censored character + * @param string $val Raw search string + */ + $censor_char = apply_filters( 'bsearch_censor_char', $censor_char, $val ); + + $val_censored = self::censor_string( $val, $badwords, $censor_char, bsearch_get_option( 'banned_whole_words' ) ); // No more bad words. + + if ( $val_censored['clean'] !== $val_censored['orig'] ) { + $bsearch_error->add( 'bsearch_banned', __( 'Your search query consists banned words. Please remove these and try again.', 'better-search' ) ); + if ( bsearch_get_option( 'banned_stop_search' ) ) { + return ''; + } + } + + $val = $val_censored['clean']; + $val = preg_replace( '!\s+!', ' ', $val ); // Replace multiple spaces with a single. + $val = preg_replace( '!\++!', '+', $val ); // Replace multiple + with a single. + $val = rtrim( $val, '+' ); // Remove any trailing + signs. + $val = wp_kses_post( $val ); + + /** + * Clean search string from XSS exploits. + * + * @since 2.0.0 + * + * @param string $val Cleaned string + */ + return apply_filters( 'bsearch_clean_terms', $val ); + } + + + /** + * Generates a random string. + * + * @since 3.3.0 + * + * @param string $chars Chars that can be used. + * @param int $len Length of the output string. + * @return string Random string + */ + public static function rand_censor( $chars, $len ) { + + $output = ''; + for ( $i = 0; $i < $len; $i++ ) { + $output .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 ); + } + + return $output; + } + + + /** + * Apply censorship to $message, replacing $badwords with $censor_char. + * + * @since 3.3.0 + * + * @param string $message String to be censored. + * @param array $badwords Array of badwords. + * @param string $censor_char String which replaces bad words. If it's more than 1-char long, a random string will be generated from these chars. Default: '*'. + * @param bool $whole_words Filter whole worlds only. + * @return array Array containing the original string at `orig` and the censored string at `clean`. + */ + public static function censor_string( $message, $badwords, $censor_char = '*', $whole_words = false ) { + + $replacement = array(); + + $leet_replace = array(); + $leet_replace['a'] = '(a|a\.|a\-|4|@|Á|á|À|Â|à|Â|â|Ä|ä|Ã|ã|Å|å|α|Δ|Λ|λ)'; + $leet_replace['b'] = '(b|b\.|b\-|8|\|3|ß|Β|β)'; + $leet_replace['c'] = '(c|c\.|c\-|Ç|ç|¢|€|<|\(|{|©)'; + $leet_replace['d'] = '(d|d\.|d\-|∂|\|\)|Þ|þ|Ð|ð)'; + $leet_replace['e'] = '(e|e\.|e\-|3|€|È|è|É|é|Ê|ê|∑)'; + $leet_replace['f'] = '(f|f\.|f\-|ƒ)'; + $leet_replace['g'] = '(g|g\.|g\-|6|9)'; + $leet_replace['h'] = '(h|h\.|h\-|Η)'; + $leet_replace['i'] = '(i|i\.|i\-|!|\||\]\[|]|1|∫|Ì|Í|Î|Ï|ì|í|î|ï)'; + $leet_replace['j'] = '(j|j\.|j\-)'; + $leet_replace['k'] = '(k|k\.|k\-|Κ|κ)'; + $leet_replace['l'] = '(l|1\.|l\-|!|\||\]\[|]|£|∫|Ì|Í|Î|Ï)'; + $leet_replace['m'] = '(m|m\.|m\-)'; + $leet_replace['n'] = '(n|n\.|n\-|η|Ν|Π)'; + $leet_replace['o'] = '(o|o\.|o\-|0|Ο|ο|Φ|¤|°|ø)'; + $leet_replace['p'] = '(p|p\.|p\-|ρ|Ρ|¶|þ)'; + $leet_replace['q'] = '(q|q\.|q\-)'; + $leet_replace['r'] = '(r|r\.|r\-|®)'; + $leet_replace['s'] = '(s|s\.|s\-|5|\$|§)'; + $leet_replace['t'] = '(t|t\.|t\-|Τ|τ)'; + $leet_replace['u'] = '(u|u\.|u\-|υ|µ)'; + $leet_replace['v'] = '(v|v\.|v\-|υ|ν)'; + $leet_replace['w'] = '(w|w\.|w\-|ω|ψ|Ψ)'; + $leet_replace['x'] = '(x|x\.|x\-|Χ|χ)'; + $leet_replace['y'] = '(y|y\.|y\-|¥|γ|ÿ|ý|Ÿ|Ý)'; + $leet_replace['z'] = '(z|z\.|z\-|Ζ)'; + + // is $censor_char a single char? + $is_one_char = ( strlen( $censor_char ) === 1 ); + + // Add boundary filter for whole words. + if ( $whole_words ) { + $boundary = '\b'; + } else { + $boundary = ''; + } + + // Count the bad words. + $no_of_badwords = count( $badwords ); + + for ( $x = 0; $x < $no_of_badwords; $x++ ) { + + $replacement[ $x ] = $is_one_char + ? str_repeat( $censor_char, strlen( $badwords[ $x ] ) ) + : self::rand_censor( $censor_char, strlen( $badwords[ $x ] ) ); + + $badwords[ $x ] = '/' . $boundary . str_ireplace( array_keys( $leet_replace ), array_values( $leet_replace ), $badwords[ $x ] ) . $boundary . '/i'; + } + + $newstring = array(); + $newstring['orig'] = $message; + $newstring['clean'] = preg_replace( $badwords, $replacement, $newstring['orig'] ); + + return $newstring; + } + + + /** + * Convert Hexadecimal colour code to RGB. + * + * @since 3.3.0 + * + * @param string $color Hexadecimal colour. + * @return array|bool Array containing RGB colour code or false if error. + */ + public static function html2rgb( $color ) { + + if ( '#' === $color[0] ) { + $color = substr( $color, 1 ); + } + + if ( 6 === (int) strlen( $color ) ) { + list( $r, $g, $b ) = array( + $color[0] . $color[1], + $color[2] . $color[3], + $color[4] . $color[5], + ); + } elseif ( 3 === (int) strlen( $color ) ) { + list( $r, $g, $b ) = array( + $color[0] . $color[0], + $color[1] . $color[1], + $color[2] . $color[2], + ); + } else { + return false; + } + + $r = hexdec( $r ); + $g = hexdec( $g ); + $b = hexdec( $b ); + + return array( $r, $g, $b ); + } + + + /** + * Function to convert RGB color code to Hexadecimal. + * + * @since 1.3.4 + * + * @param int|string|array $r Red colour or array of RGB values. + * @param int|string $g (default: -1) Green colour. + * @param int|string $b (default: -1) Blue colour. + * @param bool $padhash Pad # when returning. + * @return string HEX color code + */ + public static function rgb2html( $r, $g = -1, $b = -1, $padhash = false ) { + + if ( is_array( $r ) && 3 === count( $r ) ) { // If $r is an array, extract the RGB values. + list( $r, $g, $b ) = $r; + } + + $r = intval( $r ); + $g = intval( $g ); + $b = intval( $b ); + + $r = dechex( $r < 0 ? 0 : ( $r > 255 ? 255 : $r ) ); + $g = dechex( $g < 0 ? 0 : ( $g > 255 ? 255 : $g ) ); + $b = dechex( $b < 0 ? 0 : ( $b > 255 ? 255 : $b ) ); + + $color = ( strlen( $r ) < 2 ? '0' : '' ) . $r; + $color .= ( strlen( $g ) < 2 ? '0' : '' ) . $g; + $color .= ( strlen( $b ) < 2 ? '0' : '' ) . $b; + + if ( $padhash ) { + $color = '#' . $color; + } + + return $color; + } + + /** + * Retrieve the from date for the query + * + * @since 3.3.0 + * + * @param string $time A date/time string. + * @param int $daily_range Daily range. + * @return string From date + */ + public static function get_from_date( $time = null, $daily_range = null ) { + + $current_time = isset( $time ) ? strtotime( $time ) : strtotime( current_time( 'mysql' ) ); + $daily_range = isset( $daily_range ) ? $daily_range : bsearch_get_option( 'daily_range' ); + + $from_date = $current_time - ( max( 0, ( $daily_range - 1 ) ) * DAY_IN_SECONDS ); + $from_date = gmdate( 'Y-m-d', $from_date ); + + /** + * Retrieve the from date for the query + * + * @since 2.4.0 + * + * @param string $from_date From date. + * @param string $time A date/time string. + * @param int $daily_range Daily range. + */ + return apply_filters( 'bsearch_get_from_date', $from_date, $time, $daily_range ); + } + + + /** + * Convert float number to format based on the locale if number_format_count is true. + * + * @since 3.3.0 + * + * @param float $number The number to convert based on locale. + * @param int $decimals Optional. Precision of the number of decimal places. Default 0. + * @return string Converted number in string format. + */ + public static function number_format_i18n( $number, $decimals = 0 ) { + + $formatted = $number; + + if ( bsearch_get_option( 'number_format_count' ) ) { + $formatted = number_format_i18n( $number, $decimals ); + } + + /** + * Filters the number formatted based on the locale. + * + * @since 2.4.0 + * + * @param string $formatted Converted number in string format. + * @param float $number The number to convert based on locale. + * @param int $decimals Precision of the number of decimal places. + */ + return apply_filters( 'bsearch_number_format_i18n', $formatted, $number, $decimals ); + } + + /** + * Convert a string to CSV. + * + * @since 3.3.0 + * + * @param array $input_array Input string. + * @param string $delimiter Delimiter. + * @param string $enclosure Enclosure. + * @param string $terminator Terminating string. + * @return string CSV string. + */ + public static function str_putcsv( $input_array, $delimiter = ',', $enclosure = '"', $terminator = "\n" ) { + // First convert associative array to numeric indexed array. + $work_array = array(); + foreach ( $input_array as $key => $value ) { + $work_array[] = $value; + } + + $output = ''; + $array_size = count( $work_array ); + + for ( $i = 0; $i < $array_size; $i++ ) { + // Nested array, process nest item. + if ( is_array( $work_array[ $i ] ) ) { + $output .= self::str_putcsv( $work_array[ $i ], $delimiter, $enclosure, $terminator ); + } else { + switch ( gettype( $work_array[ $i ] ) ) { + // Manually set some strings. + case 'NULL': + $sp_format = ''; + break; + case 'boolean': + $sp_format = ( true === $work_array[ $i ] ) ? 'true' : 'false'; + break; + // Make sure sprintf has a good datatype to work with. + case 'integer': + $sp_format = '%i'; + break; + case 'double': + $sp_format = '%0.2f'; + break; + case 'string': + $sp_format = '%s'; + $work_array[ $i ] = str_replace( "$enclosure", "$enclosure$enclosure", $work_array[ $i ] ); + break; + // Unknown or invalid items for a csv - note: the datatype of array is already handled above, assuming the data is nested. + case 'object': + case 'resource': + default: + $sp_format = ''; + break; + } + $output .= sprintf( '%2$s' . $sp_format . '%2$s', $work_array[ $i ], $enclosure ); + $output .= ( $i < ( $array_size - 1 ) ) ? $delimiter : $terminator; + } + } + + return $output; + } + + /** + * Get the link to Better Search homepage. + * + * @since 3.3.0 + * + * @return string HTML markup. + */ + public static function get_credit_link() { + + $output = '
'; + + /* translators: 1: Opening a tag and Better Search, 2: Closing a tag. */ + $output .= sprintf( __( 'Powered by %1$s plugin%2$s', 'better-search' ), 'Better Search', '
' ); + + return $output; + } + + + /** + * Add a wrapper class bsearch_highlight to terms which an be styled using CSS. + * + * @since 3.3.0 + * + * @param string $input Input string. + * @param array $keys Array of terms to highlight. + * + * @return string Highlighted string. + */ + public static function highlight( $input, $keys ) { + // If keys are empty, return the input as is. + if ( empty( $keys ) ) { + return $input; + } + + $highlight_keys = array(); + + foreach ( $keys as $key ) { + if ( ! empty( $key ) && ' ' !== $key ) { + $highlight_keys[] = preg_quote( $key, '/' ); + } + } + + // If highlight_keys are empty, return the input as is. + if ( empty( $highlight_keys ) ) { + return $input; + } + + // Regular expression to match the keys outside of HTML tags. + $regex = '/\b(?!<[^>]*?>)(' . implode( '|', $highlight_keys ) . ')(?![^<]*?>)\b/iu'; + + // Replace matched keys with highlighted version. + $output = preg_replace( $regex, '$1', html_entity_decode( $input ) ); + + return $output; + } + + + /** + * Function to convert the mySQL score to percentage. + * + * @since 3.3.0 + * + * @param float $score Score for the search result. + * @param float $topscore Score for the most relevant search result. + * @return string Score converted to percentage + */ + public static function score2percent( $score, $topscore ) { + + $output = ''; + + if ( $score > 0 && $topscore > 0 ) { + $score = $score * 100 / $topscore; + $output = self::number_format_i18n( $score, 0 ) . '%'; + } + + /** + * Filter search result score + * + * @since 3.0.0 + * + * @param string $output Score converted to percentage. + * @param float $score Score for the search result. + * @param float $topscore Score for the most relevant search result. + */ + return apply_filters( 'bsearch_score2percent', $output, $score, $topscore ); + } + + + /** + * Find the locations of each of the words within the text. + * + * @since 3.3.0 + * + * @param array $words Array of words whose location needs to be extracted. + * @param string $fulltext Text to search the words in. + * @return array + */ + public static function extract_locations( $words, $fulltext ) { + $locations = array(); + if ( ! is_array( $words ) ) { + $words = array( $words ); + } + + if ( + // Check for empty search query to avoid infinite loop. + ! ( 1 === count( $words ) && '' === $words[0] ) + ) { + foreach ( $words as $word ) { + $wordlen = strlen( $word ); + $loc = stripos( $fulltext, $word ); + while ( false !== $loc ) { + $locations[] = $loc; + $loc = stripos( $fulltext, $word, $loc + $wordlen ); + } + } + } + $locations = array_unique( $locations ); + + // If no words were found, show beginning of the fulltext. + if ( empty( $locations ) ) { + $locations[0] = 0; + } + + sort( $locations ); + return $locations; + } + + /** + * Extract the start position of the relevant portion to display. + * + * This is done by looping over each match and finding the smallest distance between two found + * strings. The idea being that the closer the terms are the better match the snippet would be. + * When checking for matches we only change the location if there is a better match. + * The only exception is where we have only two matches in which case we just take the + * first as will be equally distant. + * + * @since 3.3.0 + * + * @param array $locations Array of locations. + * @param int $padding_before Number of characters to include before the first match. + * @return int Starting position of the relevant extract. + */ + public static function extract_start_position( $locations, $padding_before ) { + // If we only have 1 match we dont actually do the for loop so set to the first. + $startpos = $locations[0]; + $loccount = count( $locations ); + $smallestdiff = PHP_INT_MAX; + + // If we only have 2 skip as its probably equally relevant. + if ( count( $locations ) > 2 ) { + // Skip the first as we check 1 behind. + for ( $i = 1; $i < $loccount; $i++ ) { + if ( ( $loccount - 1 ) === $i ) { + $diff = $locations[ $i ] - $locations[ $i - 1 ]; + } else { + $diff = $locations[ $i + 1 ] - $locations[ $i ]; + } + + if ( $smallestdiff > $diff ) { + $smallestdiff = $diff; + $startpos = $locations[ $i ]; + } + } + } + + $startpos = $startpos > $padding_before ? $startpos - $padding_before : 0; + return $startpos; + } + + /** + * Extract the relevant excerpt for a set of words. + * + * @since 3.3.0 + * + * @param array $words Array of words used to determine the relevant excerpt. + * @param string $fulltext Full text to search for the extract. + * @param string $excerpt_more What to append if $text needs to be trimmed. Default '…'. + * @param int $excerpt_length Excerpt length in characters. + * @param int $padding_before Number of characters to include before the first match. + * @return string Excerpt containing the relevant portion of of the text. + */ + public static function extract_relevant_excerpt( $words, $fulltext, $excerpt_more = '…', $excerpt_length = -1, $padding_before = 100 ) { + + $textlength = strlen( $fulltext ); + if ( $textlength <= $excerpt_length ) { + return $fulltext; + } + + $locations = self::extract_locations( $words, $fulltext ); + $startpos = self::extract_start_position( $locations, $padding_before ); + + // if we are going to snip too much... + if ( $textlength - $startpos < $excerpt_length ) { + $startpos = $startpos - ( $textlength - $startpos ) / 2; + } + + $reltext = substr( $fulltext, $startpos, $excerpt_length ); + + // Check to ensure we dont snip the last word if thats the match. + if ( $startpos + $excerpt_length < $textlength ) { + $reltext = substr( $reltext, 0, strrpos( $reltext, ' ' ) ) . $excerpt_more; // Remove last word. + } + + // If we trimmed from the front add ... + if ( 0 !== $startpos ) { + $reltext = $excerpt_more . substr( $reltext, strpos( $reltext, ' ' ) + 1 ); // Remove first word. + } + + return $reltext; + } +} diff --git a/includes/utilities.php b/includes/utilities.php deleted file mode 100644 index 26259bf..0000000 --- a/includes/utilities.php +++ /dev/null @@ -1,552 +0,0 @@ -add( 'bsearch_banned', __( 'Your search query consists banned words. Please remove these and try again.', 'better-search' ) ); - if ( bsearch_get_option( 'banned_stop_search' ) ) { - return ''; - } - } - - $val = $val_censored['clean']; - $val = preg_replace( '!\s+!', ' ', $val ); // Replace multiple spaces with a single. - $val = preg_replace( '!\++!', '+', $val ); // Replace multiple + with a single. - $val = rtrim( $val, '+' ); // Remove any trailing + signs. - $val = wp_kses_post( $val ); - - /** - * Clean search string from XSS exploits. - * - * @since 2.0.0 - * - * @param string $val Cleaned string - */ - return apply_filters( 'bsearch_clean_terms', $val ); -} - - -/** - * Generates a random string. - * - * @since 1.3.3 - * - * @param string $chars Chars that can be used. - * @param int $len Length of the output string. - * @return string Random string - */ -function bsearch_rand_censor( $chars, $len ) { - - $output = ''; - for ( $i = 0; $i < $len; $i++ ) { - $output .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 ); - } - - return $output; -} - - -/** - * Apply censorship to $string, replacing $badwords with $censor_char. - * - * @since 1.3.3 - * - * @param string $string String to be censored. - * @param array $badwords Array of badwords. - * @param string $censor_char String which replaces bad words. If it's more than 1-char long, a random string will be generated from these chars. Default: '*'. - * @param bool $whole_words Filter whole worlds only. - * @return string Cleaned up string - */ -function bsearch_censor_string( $string, $badwords, $censor_char = '*', $whole_words = false ) { - - $leet_replace = array(); - $leet_replace['a'] = '(a|a\.|a\-|4|@|Á|á|À|Â|à|Â|â|Ä|ä|Ã|ã|Å|å|α|Δ|Λ|λ)'; - $leet_replace['b'] = '(b|b\.|b\-|8|\|3|ß|Β|β)'; - $leet_replace['c'] = '(c|c\.|c\-|Ç|ç|¢|€|<|\(|{|©)'; - $leet_replace['d'] = '(d|d\.|d\-|∂|\|\)|Þ|þ|Ð|ð)'; - $leet_replace['e'] = '(e|e\.|e\-|3|€|È|è|É|é|Ê|ê|∑)'; - $leet_replace['f'] = '(f|f\.|f\-|ƒ)'; - $leet_replace['g'] = '(g|g\.|g\-|6|9)'; - $leet_replace['h'] = '(h|h\.|h\-|Η)'; - $leet_replace['i'] = '(i|i\.|i\-|!|\||\]\[|]|1|∫|Ì|Í|Î|Ï|ì|í|î|ï)'; - $leet_replace['j'] = '(j|j\.|j\-)'; - $leet_replace['k'] = '(k|k\.|k\-|Κ|κ)'; - $leet_replace['l'] = '(l|1\.|l\-|!|\||\]\[|]|£|∫|Ì|Í|Î|Ï)'; - $leet_replace['m'] = '(m|m\.|m\-)'; - $leet_replace['n'] = '(n|n\.|n\-|η|Ν|Π)'; - $leet_replace['o'] = '(o|o\.|o\-|0|Ο|ο|Φ|¤|°|ø)'; - $leet_replace['p'] = '(p|p\.|p\-|ρ|Ρ|¶|þ)'; - $leet_replace['q'] = '(q|q\.|q\-)'; - $leet_replace['r'] = '(r|r\.|r\-|®)'; - $leet_replace['s'] = '(s|s\.|s\-|5|\$|§)'; - $leet_replace['t'] = '(t|t\.|t\-|Τ|τ)'; - $leet_replace['u'] = '(u|u\.|u\-|υ|µ)'; - $leet_replace['v'] = '(v|v\.|v\-|υ|ν)'; - $leet_replace['w'] = '(w|w\.|w\-|ω|ψ|Ψ)'; - $leet_replace['x'] = '(x|x\.|x\-|Χ|χ)'; - $leet_replace['y'] = '(y|y\.|y\-|¥|γ|ÿ|ý|Ÿ|Ý)'; - $leet_replace['z'] = '(z|z\.|z\-|Ζ)'; - - // is $censor_char a single char? - $is_one_char = ( strlen( $censor_char ) === 1 ); - - // Add boundary filter for whole words. - if ( $whole_words ) { - $boundary = '\b'; - } else { - $boundary = ''; - } - - // Count the bad words. - $no_of_badwords = count( $badwords ); - - for ( $x = 0; $x < $no_of_badwords; $x++ ) { - - $replacement[ $x ] = $is_one_char - ? str_repeat( $censor_char, strlen( $badwords[ $x ] ) ) - : bsearch_rand_censor( $censor_char, strlen( $badwords[ $x ] ) ); - - $badwords[ $x ] = '/' . $boundary . str_ireplace( array_keys( $leet_replace ), array_values( $leet_replace ), $badwords[ $x ] ) . $boundary . '/i'; - } - - $newstring = array(); - $newstring['orig'] = $string; - $newstring['clean'] = preg_replace( $badwords, $replacement, $newstring['orig'] ); - - return $newstring; -} - - -/** - * Convert Hexadecimal colour code to RGB. - * - * @since 1.3.4 - * - * @param string $color Hexadecimal colour. - * @return array Array containing RGB colour code - */ -function bsearch_html2rgb( $color ) { - - if ( '#' === $color[0] ) { - $color = substr( $color, 1 ); - } - - if ( 6 === (int) strlen( $color ) ) { - list( $r, $g, $b ) = array( - $color[0] . $color[1], - $color[2] . $color[3], - $color[4] . $color[5], - ); - } elseif ( 3 === (int) strlen( $color ) ) { - list( $r, $g, $b ) = array( - $color[0] . $color[0], - $color[1] . $color[1], - $color[2] . $color[2], - ); - } else { - return false; - } - - $r = hexdec( $r ); - $g = hexdec( $g ); - $b = hexdec( $b ); - - return array( $r, $g, $b ); -} - - -/** - * Function to convert RGB color code to Hexadecimal. - * - * @since 1.3.4 - * - * @param int|string|array $r Red colour or array of RGB values. - * @param int|string $g (default: -1) Green colour. - * @param int|string $b (default: -1) Blue colour. - * @param bool $padhash Pad # when returning. - * @return string HEX color code - */ -function bsearch_rgb2html( $r, $g = -1, $b = -1, $padhash = false ) { - - if ( is_array( $r ) && 3 === count( $r ) ) { // If $r is an array, extract the RGB values. - list( $r, $g, $b ) = $r; - } - - $r = intval( $r ); - $g = intval( $g ); - $b = intval( $b ); - - $r = dechex( $r < 0 ? 0 : ( $r > 255 ? 255 : $r ) ); - $g = dechex( $g < 0 ? 0 : ( $g > 255 ? 255 : $g ) ); - $b = dechex( $b < 0 ? 0 : ( $b > 255 ? 255 : $b ) ); - - $color = ( strlen( $r ) < 2 ? '0' : '' ) . $r; - $color .= ( strlen( $g ) < 2 ? '0' : '' ) . $g; - $color .= ( strlen( $b ) < 2 ? '0' : '' ) . $b; - - if ( $padhash ) { - $color = '#' . $color; - } - - return $color; -} - -/** - * Retrieve the from date for the query - * - * @since 2.4.0 - * - * @param string $time A date/time string. - * @param int $daily_range Daily range. - * @return string From date - */ -function bsearch_get_from_date( $time = null, $daily_range = null ) { - - $current_time = isset( $time ) ? strtotime( $time ) : strtotime( current_time( 'mysql' ) ); - $daily_range = isset( $daily_range ) ? $daily_range : bsearch_get_option( 'daily_range' ); - - $from_date = $current_time - ( max( 0, ( $daily_range - 1 ) ) * DAY_IN_SECONDS ); - $from_date = gmdate( 'Y-m-d', $from_date ); - - /** - * Retrieve the from date for the query - * - * @since 2.4.0 - * - * @param string $from_date From date. - * @param string $time A date/time string. - * @param int $daily_range Daily range. - */ - return apply_filters( 'bsearch_get_from_date', $from_date, $time, $daily_range ); -} - - -/** - * Convert float number to format based on the locale if number_format_count is true. - * - * @since 2.4.0 - * - * @param float $number The number to convert based on locale. - * @param int $decimals Optional. Precision of the number of decimal places. Default 0. - * @return string Converted number in string format. - */ -function bsearch_number_format_i18n( $number, $decimals = 0 ) { - - $formatted = $number; - - if ( bsearch_get_option( 'number_format_count' ) ) { - $formatted = number_format_i18n( $number, $decimals ); - } - - /** - * Filters the number formatted based on the locale. - * - * @since 2.4.0 - * - * @param string $formatted Converted number in string format. - * @param float $number The number to convert based on locale. - * @param int $decimals Precision of the number of decimal places. - */ - return apply_filters( 'bsearch_number_format_i18n', $formatted, $number, $decimals ); -} - -/** - * Convert a string to CSV. - * - * @since 2.5.0 - * - * @param array $array Input string. - * @param string $delimiter Delimiter. - * @param string $enclosure Enclosure. - * @param string $terminator Terminating string. - * @return string CSV string. - */ -function bsearch_str_putcsv( $array, $delimiter = ',', $enclosure = '"', $terminator = "\n" ) { - // First convert associative array to numeric indexed array. - $work_array = array(); - foreach ( $array as $key => $value ) { - $work_array[] = $value; - } - - $string = ''; - $array_size = count( $work_array ); - - for ( $i = 0; $i < $array_size; $i++ ) { - // Nested array, process nest item. - if ( is_array( $work_array[ $i ] ) ) { - $string .= bsearch_str_putcsv( $work_array[ $i ], $delimiter, $enclosure, $terminator ); - } else { - switch ( gettype( $work_array[ $i ] ) ) { - // Manually set some strings. - case 'NULL': - $sp_format = ''; - break; - case 'boolean': - $sp_format = ( true === $work_array[ $i ] ) ? 'true' : 'false'; - break; - // Make sure sprintf has a good datatype to work with. - case 'integer': - $sp_format = '%i'; - break; - case 'double': - $sp_format = '%0.2f'; - break; - case 'string': - $sp_format = '%s'; - $work_array[ $i ] = str_replace( "$enclosure", "$enclosure$enclosure", $work_array[ $i ] ); - break; - // Unknown or invalid items for a csv - note: the datatype of array is already handled above, assuming the data is nested. - case 'object': - case 'resource': - default: - $sp_format = ''; - break; - } - $string .= sprintf( '%2$s' . $sp_format . '%2$s', $work_array[ $i ], $enclosure ); - $string .= ( $i < ( $array_size - 1 ) ) ? $delimiter : $terminator; - } - } - - return $string; -} - -/** - * Get the link to Better Search homepage. - * - * @since 2.5.0 - * - * @return string HTML markup. - */ -function bsearch_get_credit_link() { - - $output = '
'; - - /* translators: 1: Opening a tag and Better Search, 2: Closing a tag. */ - $output .= sprintf( __( 'Powered by %1$s plugin%2$s', 'better-search' ), 'Better Search', '
' ); - - return $output; -} - - -/** - * Add a wrapper class bsearch_highlight to terms which an be styled using CSS. - * - * @since 2.5.0 - * - * @param string $input Input string. - * @param array $keys Array of terms to highlight. - * - * @return string Highlighted string. - */ -function bsearch_highlight( $input, $keys ) { - $highlight_keys = array(); - - foreach ( $keys as $key ) { - if ( ! empty( $key ) && ' ' !== $key ) { - $highlight_keys[] = preg_quote( $key, '/' ); - } - } - - if ( ! empty( $highlight_keys ) ) { - $reg_ex = '/\b(?!<[^>]*?>)(' . implode( '|', $highlight_keys ) . ')(?![^<]*?>)\b/iu'; - $output = preg_replace( $reg_ex, '$1', html_entity_decode( $input ) ); - return $output; - } - - return $input; -} - - -/** - * Function to convert the mySQL score to percentage. - * - * @since 3.0.0 - * - * @param float $score Score for the search result. - * @param float $topscore Score for the most relevant search result. - * @return string Score converted to percentage - */ -function bsearch_score2percent( $score, $topscore ) { - - $output = ''; - - if ( $score > 0 && $topscore > 0 ) { - $score = $score * 100 / $topscore; - $output = bsearch_number_format_i18n( $score, 0 ) . '%'; - } - - /** - * Filter search result score - * - * @since 3.0.0 - * - * @return string $output Score converted to percentage. - * @param float $score Score for the search result. - * @param float $topscore Score for the most relevant search result. - */ - return apply_filters( 'bsearch_score2percent', $output, $score, $topscore ); -} - - -/** - * Find the locations of each of the words within the text. - * - * @since 3.1.0 - * - * @param array $words Array of words whose location needs to be extracted. - * @param string $fulltext Text to search the words in. - * @return array - */ -function bsearch_extract_locations( $words, $fulltext ) { - $locations = array(); - if ( ! is_array( $words ) ) { - $words = array( $words ); - } - - if ( - // Check for empty search query to avoid infinite loop. - ! ( 1 === count( $words ) && '' === $words[0] ) - ) { - foreach ( $words as $word ) { - $wordlen = strlen( $word ); - $loc = stripos( $fulltext, $word ); - while ( false !== $loc ) { - $locations[] = $loc; - $loc = stripos( $fulltext, $word, $loc + $wordlen ); - } - } - } - $locations = array_unique( $locations ); - - // If no words were found, show beginning of the fulltext. - if ( empty( $locations ) ) { - $locations[0] = 0; - } - - sort( $locations ); - return $locations; -} - -/** - * Extract the start position of the relevant portion to display. - * - * This is done by looping over each match and finding the smallest distance between two found - * strings. The idea being that the closer the terms are the better match the snippet would be. - * When checking for matches we only change the location if there is a better match. - * The only exception is where we have only two matches in which case we just take the - * first as will be equally distant. - * - * @since 3.1.0 - * - * @param array $locations Array of locations. - * @param int $padding_before Number of characters to include before the first match. - * @return int Starting position of the relevant extract. - */ -function bsearch_extract_start_position( $locations, $padding_before ) { - // If we only have 1 match we dont actually do the for loop so set to the first. - $startpos = $locations[0]; - $loccount = count( $locations ); - $smallestdiff = PHP_INT_MAX; - - // If we only have 2 skip as its probably equally relevant. - if ( count( $locations ) > 2 ) { - // Skip the first as we check 1 behind. - for ( $i = 1; $i < $loccount; $i++ ) { - if ( ( $loccount - 1 ) === $i ) { - $diff = $locations[ $i ] - $locations[ $i - 1 ]; - } else { - $diff = $locations[ $i + 1 ] - $locations[ $i ]; - } - - if ( $smallestdiff > $diff ) { - $smallestdiff = $diff; - $startpos = $locations[ $i ]; - } - } - } - - $startpos = $startpos > $padding_before ? $startpos - $padding_before : 0; - return $startpos; -} - -/** - * Extract the relevant excerpt for a set of words. - * - * @since 3.1.0 - * - * @param array $words Array of words used to determine the relevant excerpt. - * @param string $fulltext Full text to search for the extract. - * @param string $excerpt_more What to append if $text needs to be trimmed. Default '…'. - * @param int $excerpt_length Excerpt length in characters. - * @param int $padding_before Number of characters to include before the first match. - * @return string Excerpt containing the relevant portion of of the text. - */ -function bsearch_extract_relevant_excerpt( $words, $fulltext, $excerpt_more = '…', $excerpt_length = -1, $padding_before = 100 ) { - - $textlength = strlen( $fulltext ); - if ( $textlength <= $excerpt_length ) { - return $fulltext; - } - - $locations = bsearch_extract_locations( $words, $fulltext ); - $startpos = bsearch_extract_start_position( $locations, $padding_before ); - - // if we are going to snip too much... - if ( $textlength - $startpos < $excerpt_length ) { - $startpos = $startpos - ( $textlength - $startpos ) / 2; - } - - $reltext = substr( $fulltext, $startpos, $excerpt_length ); - - // Check to ensure we dont snip the last word if thats the match. - if ( $startpos + $excerpt_length < $textlength ) { - $reltext = substr( $reltext, 0, strrpos( $reltext, ' ' ) ) . $excerpt_more; // Remove last word. - } - - // If we trimmed from the front add ... - if ( 0 !== $startpos ) { - $reltext = $excerpt_more . substr( $reltext, strpos( $reltext, ' ' ) + 1 ); // Remove first word. - } - - return $reltext; -} diff --git a/includes/wp-filters.php b/includes/wp-filters.php deleted file mode 100644 index b71046a..0000000 --- a/includes/wp-filters.php +++ /dev/null @@ -1,118 +0,0 @@ -'; - } - - // Add custom CSS to header. - if ( '' != $bsearch_custom_css ) { //phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $output .= ''; - } - } - - /** - * Filters the output HTML added to wp_head - * - * @since 2.0.0 - * - * @return string $output Output HTML added to wp_head - */ - $output = apply_filters( 'bsearch_clause_head', $output ); - - echo $output; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} -add_action( 'wp_head', 'bsearch_clause_head' ); - - - -/** - * Highlight the search term - * - * @since 2.0.0 - * - * @param string $content Post content. - * @return string Post Content - */ -function bsearch_content( $content ) { - - if ( is_admin() || ! in_the_loop() ) { - return $content; - } - - $referer = wp_get_referer() ? urldecode( wp_get_referer() ) : ''; - if ( is_search() ) { - $is_referer_search_engine = true; - } else { - $siteurl = get_option( 'home' ); - if ( preg_match( "#^$siteurl#i", $referer ) ) { - parse_str( (string) wp_parse_url( $referer, PHP_URL_QUERY ), $queries ); - if ( ! empty( $queries['s'] ) ) { - $is_referer_search_engine = true; - } - } - } - - if ( empty( $is_referer_search_engine ) ) { - return $content; - } - - if ( bsearch_get_option( 'highlight' ) && is_search() ) { - $search_query = get_bsearch_query(); - } elseif ( bsearch_get_option( 'highlight_followed_links' ) ) { - $search_query = preg_replace( '/^.*s=([^&]+)&?.*$/i', '$1', $referer ); - $search_query = preg_replace( '/\'|"/', '', $search_query ); - } - - if ( ! empty( $search_query ) ) { - $search_query = str_replace( array( "'", '"', '"', '\+', '\-' ), '', $search_query ); - $keys = preg_split( '/[\s,\+\.]+/', $search_query ); - $content = bsearch_highlight( $content, $keys ); - } - - return apply_filters( 'bsearch_content', $content ); -} -add_filter( 'the_content', 'bsearch_content', 999 ); -add_filter( 'get_the_excerpt', 'bsearch_content', 999 ); -add_filter( 'the_title', 'bsearch_content', 999 ); -add_filter( 'the_bsearch_excerpt', 'bsearch_content', 999 ); - -/** - * Enqueue styles and scripts. - * - * @since 3.0.0 - */ -function bsearch_enqueue_scripts_styles() { - - if ( bsearch_get_option( 'include_styles' ) ) { - wp_register_style( 'bsearch-style', plugins_url( 'includes/css/bsearch-styles.min.css', BETTER_SEARCH_PLUGIN_FILE ), array(), BETTER_SEARCH_VERSION ); - } - - if ( ! is_admin() && ( is_search() || is_singular() ) ) { - wp_enqueue_style( 'bsearch-style' ); - wp_add_inline_style( 'bsearch-style', esc_html( bsearch_get_option( 'custom_css' ) ) ); - } -} -add_action( 'wp_enqueue_scripts', 'bsearch_enqueue_scripts_styles' ); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..7602fe2 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,101 @@ +parameters: + ignoreErrors: + - + message: "#^Constant BETTER_SEARCH_PLUGIN_DIR not found\\.$#" + count: 1 + path: includes/admin/class-admin.php + + - + message: "#^Constant BETTER_SEARCH_PLUGIN_URL not found\\.$#" + count: 7 + path: includes/admin/class-admin.php + + - + message: "#^Variable \\$menu_page might not be defined\\.$#" + count: 1 + path: includes/admin/settings/class-settings-api.php + + - + message: "#^Constant BETTER_SEARCH_PLUGIN_URL not found\\.$#" + count: 3 + path: includes/admin/settings/sidebar.php + + - + message: "#^Constant BETTER_SEARCH_PLUGIN_DIR not found\\.$#" + count: 1 + path: includes/autoloader.php + + - + message: "#^Access to an undefined property WP_Query\\:\\:\\$topscore\\.$#" + count: 3 + path: includes/class-better-search.php + + - + message: "#^Cannot access property \\$score on int\\|WP_Post\\.$#" + count: 1 + path: includes/class-better-search.php + + - + message: "#^Variable \\$cache_name might not be defined\\.$#" + count: 1 + path: includes/class-better-search.php + + - + message: "#^Variable \\$cache_time might not be defined\\.$#" + count: 1 + path: includes/class-better-search.php + + - + message: "#^Negated boolean expression is always false\\.$#" + count: 1 + path: includes/class-tracker.php + + - + message: "#^Property WP\\:\\:\\$query_vars \\(array\\) in isset\\(\\) is not nullable\\.$#" + count: 1 + path: includes/class-tracker.php + + - + message: "#^Result of \\|\\| is always false\\.$#" + count: 1 + path: includes/class-tracker.php + + - + message: "#^Variable \\$wp in empty\\(\\) always exists and is not falsy\\.$#" + count: 1 + path: includes/class-tracker.php + + - + message: "#^Method WebberZone\\\\Better_Search\\\\Frontend\\\\Widgets\\\\Search_Box\\:\\:form\\(\\) should return string but return statement is missing\\.$#" + count: 1 + path: includes/frontend/widgets/class-search-box.php + + - + message: "#^Method WebberZone\\\\Better_Search\\\\Frontend\\\\Widgets\\\\Search_Heatmap\\:\\:form\\(\\) should return string but return statement is missing\\.$#" + count: 1 + path: includes/frontend/widgets/class-search-heatmap.php + + - + message: "#^Parameter \\#2 \\$array of function implode expects array\\, array\\, string\\|WP_Error\\> given\\.$#" + count: 1 + path: includes/general-template.php + + - + message: "#^Function the_bsearch_heatmap\\(\\) never returns void so it can be removed from the return type\\.$#" + count: 1 + path: includes/heatmap.php + + - + message: "#^Variable \\$a might not be defined\\.$#" + count: 2 + path: includes/heatmap.php + + - + message: "#^Access to an undefined property Better_Search_Query\\:\\:\\$topscore\\.$#" + count: 1 + path: templates/template.php + + - + message: "#^Variable \\$post might not be defined\\.$#" + count: 2 + path: templates/template.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..7984cf8 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,10 @@ +includes: + - phpstan-baseline.neon +parameters: + level: 5 + paths: + - better-search.php + - uninstall.php + - includes/ + - templates/ + ignoreErrors: diff --git a/phpunit/bootstrap.php b/phpunit/bootstrap.php index f199958..da16ee8 100644 --- a/phpunit/bootstrap.php +++ b/phpunit/bootstrap.php @@ -40,6 +40,6 @@ function _manually_load_plugin() { global $bsearch_settings, $current_user; -bsearch_install( true ); +\WebberZone\Better_Search\Admin\Activator::activation_hook( false ); $bsearch_settings = bsearch_get_settings(); diff --git a/readme.txt b/readme.txt index 8775140..a46210d 100644 --- a/readme.txt +++ b/readme.txt @@ -106,7 +106,13 @@ Know of a better profanity filter? Suggest one in the [forums](https://wordpress = 3.3.0 = +Complete rewrite of the plugin code using OOP. The plugin now uses autoloading and namespaces. This is a major release. + +* Features: + * New Admin Dashboard will show the number of searches and the top searches for the day, week, month and all time + * Enhancements: + * Better Search Tracker doesn't use jQuery anymore * Uninstall now uses `get_sites()` behind the scenes to delete options from all sites in a multisite install = 3.2.2 = diff --git a/templates/template.php b/templates/template.php index a3a01b1..44d992a 100644 --- a/templates/template.php +++ b/templates/template.php @@ -20,6 +20,7 @@ $post_types = ( 'any' === $selected_post_types ) ? bsearch_get_option( 'post_types' ) : $selected_post_types; // Reset wp_query temporary. +global $wp_query; $tmp_wpquery = $wp_query; // Set up Better_Search_Query to replace $wp_query.