diff --git a/composer.json b/composer.json index 5d18fa6..3bba3cd 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ }, "config": { "allow-plugins": { - "composer/installers": true + "composer/installers": true, + "dealerdirect/phpcodesniffer-composer-installer": true } }, "scripts": { @@ -41,5 +42,8 @@ "_bump-minor-helper": "php -r \"$c=json_decode(file_get_contents('composer.json'),true);$v=explode('.',$c['version']);$v[1]++;$v[2]=0;$c['version']=implode('.',$v);file_put_contents('composer.json',json_encode($c,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));\"", "_bump-patch-helper": "php -r \"$c=json_decode(file_get_contents('composer.json'),true);$v=explode('.',$c['version']);$v[2]++;$c['version']=implode('.',$v);file_put_contents('composer.json',json_encode($c,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));\"", "git-tag": "php -r \"$c=json_decode(file_get_contents('composer.json'),true);echo 'v'.$c['version'];\" | xargs -I {} git tag {}" + }, + "require-dev": { + "wp-coding-standards/wpcs": "^3.3" } } diff --git a/composer.lock b/composer.lock index 0fe1c8b..e12c975 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7e85e8c69165825a03c852f8721f5240", + "content-hash": "6957c88f3adf57b462003206a6fdca93", "packages": [ { "name": "composer/installers", @@ -153,7 +153,424 @@ "time": "2024-06-24T20:46:46+00:00" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2025-11-25T12:08:04+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, diff --git a/faq-jsonld.php b/faq-jsonld.php index 5ff8532..4d03dca 100644 --- a/faq-jsonld.php +++ b/faq-jsonld.php @@ -1,100 +1,102 @@ prefix.'fqj_mappings'); -define('FQJ_OPTION_KEY', 'fqj_settings'); +// phpcs:disable PSR1.Files.SideEffects + +define( 'FQJ_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); +define( 'FQJ_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); +define( 'FQJ_DB_TABLE', $GLOBALS['wpdb']->prefix . 'fqj_mappings' ); +define( 'FQJ_OPTION_KEY', 'fqj_settings' ); /** * Autoload includes */ -require_once FQJ_PLUGIN_DIR.'includes/settings.php'; -require_once FQJ_PLUGIN_DIR.'includes/indexer.php'; -require_once FQJ_PLUGIN_DIR.'includes/queue.php'; -require_once FQJ_PLUGIN_DIR.'includes/admin.php'; -require_once FQJ_PLUGIN_DIR.'includes/frontend.php'; -require_once FQJ_PLUGIN_DIR.'includes/wpcli.php'; -require_once FQJ_PLUGIN_DIR.'includes/health.php'; +require_once FQJ_PLUGIN_DIR . 'includes/settings.php'; +require_once FQJ_PLUGIN_DIR . 'includes/indexer.php'; +require_once FQJ_PLUGIN_DIR . 'includes/queue.php'; +require_once FQJ_PLUGIN_DIR . 'includes/admin.php'; +require_once FQJ_PLUGIN_DIR . 'includes/frontend.php'; +require_once FQJ_PLUGIN_DIR . 'includes/class-fqjcli.php'; +require_once FQJ_PLUGIN_DIR . 'includes/health.php'; /** * Register CPT */ -function fqj_register_cpt_faq_item() -{ - $labels = [ - 'name' => 'FAQ Items', - 'singular_name' => 'FAQ Item', - 'add_new_item' => 'Add FAQ Item', - 'edit_item' => 'Edit FAQ Item', - 'new_item' => 'New FAQ Item', - 'view_item' => 'View FAQ Item', - 'search_items' => 'Search FAQ Items', - 'not_found' => 'No FAQ items found', - 'all_items' => 'All FAQ Items', - ]; - $args = [ - 'labels' => $labels, - 'public' => false, - 'show_ui' => true, - 'show_in_menu' => true, - 'capability_type' => 'post', - 'hierarchical' => false, - 'supports' => ['title', 'editor'], - 'menu_position' => 25, - 'menu_icon' => 'dashicons-editor-help', - 'has_archive' => false, - ]; - register_post_type('faq_item', $args); +function fqj_register_cpt_faq_item() { + $labels = array( + 'name' => 'FAQ Items', + 'singular_name' => 'FAQ Item', + 'add_new_item' => 'Add FAQ Item', + 'edit_item' => 'Edit FAQ Item', + 'new_item' => 'New FAQ Item', + 'view_item' => 'View FAQ Item', + 'search_items' => 'Search FAQ Items', + 'not_found' => 'No FAQ items found', + 'all_items' => 'All FAQ Items', + ); + $args = array( + 'labels' => $labels, + 'public' => false, + 'show_ui' => true, + 'show_in_menu' => true, + 'capability_type' => 'post', + 'hierarchical' => false, + 'supports' => array( 'title', 'editor' ), + 'menu_position' => 25, + 'menu_icon' => 'dashicons-editor-help', + 'has_archive' => false, + ); + register_post_type( 'faq_item', $args ); } -add_action('init', 'fqj_register_cpt_faq_item'); +add_action( 'init', 'fqj_register_cpt_faq_item' ); /** * Activation: create DB table, defaults, schedule cron */ -function fqj_activate() -{ - fqj_create_table(); +function fqj_activate() { + fqj_create_table(); - $defaults = [ - 'cache_ttl' => 12 * HOUR_IN_SECONDS, - 'batch_size' => 500, - 'output_type' => 'faqsection', - 'queue_cron_interval' => 'fqj_five_minutes', // interval name - ]; - $opts = get_option(FQJ_OPTION_KEY, []); - $opts = wp_parse_args($opts, $defaults); - update_option(FQJ_OPTION_KEY, $opts); + $defaults = array( + 'cache_ttl' => 12 * HOUR_IN_SECONDS, + 'batch_size' => 500, + 'output_type' => 'faqsection', + 'queue_cron_interval' => 'fqj_five_minutes', // Interval name. + ); + $opts = get_option( FQJ_OPTION_KEY, array() ); + $opts = wp_parse_args( $opts, $defaults ); + update_option( FQJ_OPTION_KEY, $opts ); - // schedule cron if not scheduled - if (! wp_next_scheduled('fqj_process_invalidation_queue')) { - // Use a custom interval 'fqj_five_minutes' registered in queue.php - wp_schedule_event(time() + 60, 'fqj_five_minutes', 'fqj_process_invalidation_queue'); - } + // schedule cron if not scheduled. + if ( ! wp_next_scheduled( 'fqj_process_invalidation_queue' ) ) { + // Use a custom interval 'fqj_five_minutes' registered in queue.php. + wp_schedule_event( time() + 60, 'fqj_five_minutes', 'fqj_process_invalidation_queue' ); + } } -register_activation_hook(__FILE__, 'fqj_activate'); +register_activation_hook( __FILE__, 'fqj_activate' ); /** * Deactivation: clear cron */ -function fqj_deactivate() -{ - $timestamp = wp_next_scheduled('fqj_process_invalidation_queue'); - if ($timestamp) { - wp_unschedule_event($timestamp, 'fqj_process_invalidation_queue'); - } +function fqj_deactivate() { + $timestamp = wp_next_scheduled( 'fqj_process_invalidation_queue' ); + if ( $timestamp ) { + wp_unschedule_event( $timestamp, 'fqj_process_invalidation_queue' ); + } } -register_deactivation_hook(__FILE__, 'fqj_deactivate'); +register_deactivation_hook( __FILE__, 'fqj_deactivate' ); diff --git a/includes/admin.php b/includes/admin.php index 1401654..33e53ee 100644 --- a/includes/admin.php +++ b/includes/admin.php @@ -1,216 +1,310 @@ post_type !== 'faq_item') { - return; - } - - wp_enqueue_style('fqj-select2-css', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css', [], '4.0.13'); - wp_enqueue_script('fqj-select2-js', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js', ['jquery'], '4.0.13', true); - - wp_enqueue_script('fqj-admin-js', FQJ_PLUGIN_URL.'assets/js/fqj-admin.js', ['jquery', 'fqj-select2-js'], '1.0', true); - wp_localize_script('fqj-admin-js', 'fqjAdmin', [ - 'ajax_url' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('fqj_admin_nonce'), - 'post_id' => get_the_ID(), - ]); +function fqj_admin_assets( $hook ) { + global $post; + if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) { + return; + } + if ( ! $post || 'faq_item' !== $post->post_type ) { + return; + } + + wp_enqueue_style( + 'fqj-select2-css', + 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css', + array(), + '4.0.13' + ); + wp_enqueue_script( + 'fqj-select2-js', + 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js', + array( 'jquery' ), + '4.0.13', + true + ); + + wp_enqueue_script( + 'fqj-admin-js', + FQJ_PLUGIN_URL . 'assets/js/fqj-admin.js', + array( 'jquery', 'fqj-select2-js' ), + '1.0', + true + ); + wp_localize_script( + 'fqj-admin-js', + 'fqjAdmin', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'fqj_admin_nonce' ), + 'post_id' => get_the_ID(), + ) + ); } -add_action('admin_enqueue_scripts', 'fqj_admin_assets'); +add_action( 'admin_enqueue_scripts', 'fqj_admin_assets' ); /** * Add association meta box */ -function fqj_add_meta_boxes() -{ - add_meta_box( - 'fqj_assoc_rules', - 'Associations & Output', - 'fqj_assoc_rules_meta_box_cb', - 'faq_item', - 'normal', - 'default' - ); +function fqj_add_meta_boxes() { + add_meta_box( + 'fqj_assoc_rules', + 'Associations & Output', + 'fqj_assoc_rules_meta_box_cb', + 'faq_item', + 'normal', + 'default' + ); } -add_action('add_meta_boxes', 'fqj_add_meta_boxes'); +add_action( 'add_meta_boxes', 'fqj_add_meta_boxes' ); + +/** + * Render association meta box. + * + * @param WP_Post $post The post object. + */ +function fqj_assoc_rules_meta_box_cb( $post ) { + wp_nonce_field( 'fqj_save_meta', 'fqj_meta_nonce' ); + + // Load stored JSON payload for this FAQ. + $data_json = get_post_meta( $post->ID, 'fqj_assoc_data_json', true ); + $data_json = $data_json ? $data_json : '{}'; + $data = json_decode( $data_json, true ); -function fqj_assoc_rules_meta_box_cb($post) -{ - wp_nonce_field('fqj_save_meta', 'fqj_meta_nonce'); + // Hydrate posts and terms with titles/names for Select2. + if ( isset( $data['posts'] ) && is_array( $data['posts'] ) && ! empty( $data['posts'] ) ) { + $hydrated_posts = array(); + $posts_to_fetch = array_map( 'intval', $data['posts'] ); + $fetched = get_posts( + array( + 'include' => $posts_to_fetch, + 'post_type' => 'any', + 'posts_per_page' => -1, + ) + ); + foreach ( $fetched as $p ) { + $hydrated_posts[] = array( + 'id' => $p->ID, + 'text' => get_the_title( $p ) . ' – ' . get_permalink( $p ), + ); + } + $data['posts'] = $hydrated_posts; + } - // Load stored JSON payload for this FAQ - $data_json = get_post_meta($post->ID, 'fqj_assoc_data_json', true) ?: '{}'; - $assoc_type = get_post_meta($post->ID, 'fqj_assoc_type', true) ?: 'urls'; + if ( isset( $data['terms'] ) && is_array( $data['terms'] ) && ! empty( $data['terms'] ) ) { + $hydrated_terms = array(); + $terms_to_fetch = array_map( 'intval', $data['terms'] ); + foreach ( $terms_to_fetch as $tid ) { + $term = get_term( $tid ); + if ( $term && ! is_wp_error( $term ) ) { + $hydrated_terms[] = array( + 'id' => $term->term_id, + 'text' => $term->name . ' (' . $term->taxonomy . ')', + ); + } + } + $data['terms'] = $hydrated_terms; + } - $assoc_types = [ - 'urls' => 'By URLs (one per line)', - 'posts' => 'By Posts (search & multi-select)', - 'post_types' => 'By Post Types (apply to all posts of selected types)', - 'tax_terms' => 'By Taxonomy Terms (search & multi-select)', - 'global' => 'Global (site-wide)', - ]; + $data_json_hydrated = wp_json_encode( $data ); - echo '
'; - echo ''; + $assoc_type = get_post_meta( $post->ID, 'fqj_assoc_type', true ); + $assoc_type = $assoc_type ? $assoc_type : 'urls'; - echo ''; + $assoc_types = array( + 'urls' => 'By URLs (one per line)', + 'posts' => 'By Posts (search & multi-select)', + 'post_types' => 'By Post Types (apply to all posts of selected types)', + 'tax_terms' => 'By Taxonomy Terms (search & multi-select)', + 'global' => 'Global (site-wide)', + ); - echo ''; + echo ''; + echo ''; - echo 'Enter association details. Use the "By Posts" option to search and multi-select posts/pages. For large sites, prefer Post Types or Taxonomy Terms.
'; + echo ''; + + echo ''; + + echo 'Enter association details. Use the "By Posts" option to search and multi-select ' . + 'posts/pages. For large sites, prefer Post Types or Taxonomy Terms.
'; } /** * Save meta handler (similar to previous implementation but store payload JSON for indexing) + * + * @param int $post_id The post ID. + * @param WP_Post $post The post object. */ -function fqj_save_meta_handler($post_id, $post) -{ - if (! isset($_POST['fqj_meta_nonce'])) { - return; - } - if (! wp_verify_nonce($_POST['fqj_meta_nonce'], 'fqj_save_meta')) { - return; - } - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - return; - } - if ($post->post_type !== 'faq_item') { - return; - } - if (! current_user_can('edit_post', $post_id)) { - return; - } - - $assoc_type = isset($_POST['fqj_assoc_type']) ? sanitize_text_field($_POST['fqj_assoc_type']) : 'urls'; - $payload = []; - - if ($assoc_type === 'urls') { - $raw = isset($_POST['fqj_assoc_urls']) ? trim(wp_unslash($_POST['fqj_assoc_urls'])) : ''; - $lines = preg_split("/\r\n|\n|\r/", $raw); - $urls = []; - foreach ($lines as $l) { - $l = trim($l); - if (empty($l)) { - continue; - } - $urls[] = esc_url_raw($l); - } - $payload['urls'] = array_values(array_unique($urls)); - } elseif ($assoc_type === 'posts') { - $post_ids = []; - if (isset($_POST['fqj_assoc_posts_select']) && is_array($_POST['fqj_assoc_posts_select'])) { - foreach ($_POST['fqj_assoc_posts_select'] as $pid) { - $post_ids[] = intval($pid); - } - } - $payload['posts'] = array_values(array_unique($post_ids)); - } elseif ($assoc_type === 'post_types') { - $ptypes = []; - if (isset($_POST['fqj_assoc_post_types']) && is_array($_POST['fqj_assoc_post_types'])) { - foreach ($_POST['fqj_assoc_post_types'] as $pt) { - $ptypes[] = sanitize_text_field($pt); - } - } - $payload['post_types'] = array_values(array_unique($ptypes)); - } elseif ($assoc_type === 'tax_terms') { - $terms = []; - if (isset($_POST['fqj_assoc_terms_select']) && is_array($_POST['fqj_assoc_terms_select'])) { - foreach ($_POST['fqj_assoc_terms_select'] as $t) { - $terms[] = intval($t); - } - } - $payload['terms'] = array_values(array_unique($terms)); - } elseif ($assoc_type === 'global') { - $payload['global'] = true; - } - - update_post_meta($post_id, 'fqj_assoc_type', $assoc_type); - update_post_meta($post_id, 'fqj_assoc_data_json', wp_json_encode($payload)); - - // Rebuild index (indexer will delete and reinsert mappings, then invalidate transients) - fqj_rebuild_index_for_faq($post_id); +function fqj_save_meta_handler( $post_id, $post ) { + if ( ! isset( $_POST['fqj_meta_nonce'] ) ) { + return; + } + if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['fqj_meta_nonce'] ) ), 'fqj_save_meta' ) ) { + return; + } + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + if ( 'faq_item' !== $post->post_type ) { + return; + } + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return; + } + + $assoc_type = isset( $_POST['fqj_assoc_type'] ) ? sanitize_text_field( wp_unslash( $_POST['fqj_assoc_type'] ) ) : 'urls'; + $payload = array(); + + if ( 'urls' === $assoc_type ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $raw = isset( $_POST['fqj_assoc_urls'] ) ? trim( wp_unslash( $_POST['fqj_assoc_urls'] ) ) : ''; + $lines = preg_split( "/\r\n|\n|\r/", $raw ); + $urls = array(); + foreach ( $lines as $l ) { + $l = trim( $l ); + if ( empty( $l ) ) { + continue; + } + $urls[] = esc_url_raw( $l ); + } + $payload['urls'] = array_values( array_unique( $urls ) ); + } elseif ( 'posts' === $assoc_type ) { + $post_ids = array(); + if ( isset( $_POST['fqj_assoc_posts_select'] ) && is_array( $_POST['fqj_assoc_posts_select'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + foreach ( wp_unslash( $_POST['fqj_assoc_posts_select'] ) as $pid ) { + $post_ids[] = intval( $pid ); + } + } + $payload['posts'] = array_values( array_unique( $post_ids ) ); + } elseif ( 'post_types' === $assoc_type ) { + $ptypes = array(); + if ( isset( $_POST['fqj_assoc_post_types'] ) && is_array( $_POST['fqj_assoc_post_types'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + foreach ( wp_unslash( $_POST['fqj_assoc_post_types'] ) as $pt ) { + $ptypes[] = sanitize_text_field( $pt ); + } + } + $payload['post_types'] = array_values( array_unique( $ptypes ) ); + } elseif ( 'tax_terms' === $assoc_type ) { + $terms = array(); + if ( isset( $_POST['fqj_assoc_terms_select'] ) && is_array( $_POST['fqj_assoc_terms_select'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + foreach ( wp_unslash( $_POST['fqj_assoc_terms_select'] ) as $t ) { + $terms[] = intval( $t ); + } + } + $payload['terms'] = array_values( array_unique( $terms ) ); + } elseif ( 'global' === $assoc_type ) { + $payload['global'] = true; + } + + update_post_meta( $post_id, 'fqj_assoc_type', $assoc_type ); + update_post_meta( $post_id, 'fqj_assoc_data_json', wp_json_encode( $payload ) ); + + // Rebuild index (indexer will delete and reinsert mappings, then invalidate transients). + fqj_rebuild_index_for_faq( $post_id ); } -add_action('save_post', 'fqj_save_meta_handler', 10, 2); +add_action( 'save_post', 'fqj_save_meta_handler', 10, 2 ); /** * AJAX: search posts for Select2 */ -function fqj_ajax_search_posts() -{ - check_ajax_referer('fqj_admin_nonce', 'nonce'); - $q = isset($_REQUEST['q']) ? sanitize_text_field(wp_unslash($_REQUEST['q'])) : ''; - $results = []; - if (strlen($q) < 1) { - wp_send_json($results); - } - - $args = [ - 's' => $q, - 'post_type' => ['post', 'page'], - 'posts_per_page' => 20, - 'post_status' => 'publish', - ]; - $posts = get_posts($args); - foreach ($posts as $p) { - $results[] = ['id' => $p->ID, 'text' => get_the_title($p).' – '.get_permalink($p)]; - } - wp_send_json($results); +function fqj_ajax_search_posts() { + check_ajax_referer( 'fqj_admin_nonce', 'nonce' ); + $q = isset( $_REQUEST['q'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['q'] ) ) : ''; + $results = array(); + if ( strlen( $q ) < 1 ) { + wp_send_json( $results ); + } + + $args = array( + 's' => $q, + 'post_type' => array( 'post', 'page' ), + 'posts_per_page' => 20, + 'post_status' => 'publish', + ); + $posts = get_posts( $args ); + foreach ( $posts as $p ) { + $results[] = array( + 'id' => $p->ID, + 'text' => get_the_title( $p ) . ' – ' . get_permalink( $p ), + ); + } + wp_send_json( $results ); } -add_action('wp_ajax_fqj_search_posts', 'fqj_ajax_search_posts'); +add_action( 'wp_ajax_fqj_search_posts', 'fqj_ajax_search_posts' ); /** * AJAX: search terms */ -function fqj_ajax_search_terms() -{ - check_ajax_referer('fqj_admin_nonce', 'nonce'); - $q = isset($_REQUEST['q']) ? sanitize_text_field(wp_unslash($_REQUEST['q'])) : ''; - $results = []; - if (strlen($q) < 1) { - wp_send_json($results); - } - - $taxonomies = get_taxonomies(['public' => true], 'names'); - foreach ($taxonomies as $tax) { - $terms = get_terms(['taxonomy' => $tax, 'name__like' => $q, 'number' => 10, 'hide_empty' => false]); - if (is_wp_error($terms)) { - continue; - } - foreach ($terms as $t) { - $results[] = ['id' => $t->term_id, 'text' => $t->name.' ('.$tax.')']; - } - } - wp_send_json($results); +function fqj_ajax_search_terms() { + check_ajax_referer( 'fqj_admin_nonce', 'nonce' ); + $q = isset( $_REQUEST['q'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['q'] ) ) : ''; + $results = array(); + if ( strlen( $q ) < 1 ) { + wp_send_json( $results ); + } + + $taxonomies = get_taxonomies( array( 'public' => true ), 'names' ); + foreach ( $taxonomies as $tax ) { + $terms = get_terms( + array( + 'taxonomy' => $tax, + 'name__like' => $q, + 'number' => 10, + 'hide_empty' => false, + ) + ); + if ( is_wp_error( $terms ) ) { + continue; + } + foreach ( $terms as $t ) { + $results[] = array( + 'id' => $t->term_id, + 'text' => $t->name . ' (' . $tax . ')', + ); + } + } + wp_send_json( $results ); } -add_action('wp_ajax_fqj_search_terms', 'fqj_ajax_search_terms'); +add_action( 'wp_ajax_fqj_search_terms', 'fqj_ajax_search_terms' ); /** * AJAX: get public post types */ -function fqj_ajax_get_post_types() -{ - check_ajax_referer('fqj_admin_nonce', 'nonce'); - $pts = get_post_types(['public' => true], 'objects'); - $out = []; - foreach ($pts as $k => $obj) { - $out[] = ['name' => $k, 'label' => $obj->labels->singular_name]; - } - wp_send_json($out); +function fqj_ajax_get_post_types() { + check_ajax_referer( 'fqj_admin_nonce', 'nonce' ); + $pts = get_post_types( array( 'public' => true ), 'objects' ); + $out = array(); + foreach ( $pts as $k => $obj ) { + $out[] = array( + 'name' => $k, + 'label' => $obj->labels->singular_name, + ); + } + wp_send_json( $out ); } -add_action('wp_ajax_fqj_get_post_types', 'fqj_ajax_get_post_types'); +add_action( 'wp_ajax_fqj_get_post_types', 'fqj_ajax_get_post_types' ); diff --git a/includes/class-fqjcli.php b/includes/class-fqjcli.php new file mode 100644 index 0000000..2888a90 --- /dev/null +++ b/includes/class-fqjcli.php @@ -0,0 +1,74 @@ +get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", '%_transient_fqj_faq_json_%' ) ); + $count = 0; + if ( $rows ) { + foreach ( $rows as $opt ) { + $key = preg_replace( '/^_transient_|^_transient_timeout_/', '', $opt ); + if ( delete_transient( $key ) ) { + ++$count; + } + } + } + WP_CLI::success( "Purged {$count} faq transients." ); + } + + /** + * Process invalidation queue via CLI. + * Usage: wp fqj process-queue --limit=1000 + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. + */ + public function process_queue( $args, $assoc_args ) { + $limit = isset( $assoc_args['limit'] ) ? intval( $assoc_args['limit'] ) : null; + WP_CLI::log( 'Processing invalidate queue...' ); + $processed = fqj_process_invalidation_queue_now( $limit ); + WP_CLI::success( "Processed {$processed} invalidation items." ); + } + + /** + * Show queue length + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. + */ + public function queue_info( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + $len = fqj_queue_length(); + WP_CLI::success( "Queue length: {$len}" ); + } + } + + WP_CLI::add_command( 'fqj', 'FQJ\FqjCli' ); +} diff --git a/includes/frontend.php b/includes/frontend.php index bc0117b..dafc592 100644 --- a/includes/frontend.php +++ b/includes/frontend.php @@ -1,156 +1,183 @@ ID); - - $opts = get_option(FQJ_OPTION_KEY); - $cache_ttl = isset($opts['cache_ttl']) ? intval($opts['cache_ttl']) : 12 * HOUR_IN_SECONDS; - $output_type = isset($opts['output_type']) ? $opts['output_type'] : 'faqsection'; - - $transient_key = 'fqj_faq_json_'.$current_id; - $cached = get_transient($transient_key); - if ($cached !== false) { - if ($cached === '__FQJ_EMPTY__') { - echo "\n\n"; - } elseif (trim($cached) !== '') { - echo "\n\n".$cached."\n"; - } else { - // Should not happen, but for safety - echo "\n\n"; - } - - return; - } - - // Determine candidate mapping values for lookup - $candidate_values = []; - - // direct post id mapping - $candidate_values[] = ['type' => 'post', 'value' => (string) $current_id]; - - // current post type mapping - $pt = get_post_type($current_id); - if ($pt) { - $candidate_values[] = ['type' => 'post_type', 'value' => $pt]; - } - - // terms - $terms = wp_get_post_terms($current_id); - if (! is_wp_error($terms) && ! empty($terms)) { - foreach ($terms as $t) { - $candidate_values[] = ['type' => 'term', 'value' => (string) $t->term_id]; - } - } - - // URL mapping - try canonical/permalink - $permalink = get_permalink($current_id); - if ($permalink) { - $candidate_values[] = ['type' => 'url', 'value' => rtrim(strtok($permalink, '?'), '/')]; - } - - // Always include global mapping - $candidate_values[] = ['type' => 'global', 'value' => '1']; - - // Query mapping table for matching faq_ids - global $wpdb; - $table = FQJ_DB_TABLE; - - // Build WHERE clauses for each candidate - $where_clauses = []; - $params = []; - foreach ($candidate_values as $c) { - // mapping_type = ? AND mapping_value LIKE ? - $where_clauses[] = '(mapping_type = %s AND mapping_value LIKE %s)'; - $params[] = $c['type']; - // we match serialized values, so use LIKE %value% (safe because our simple values are stored raw or serialized) - $params[] = '%'.$wpdb->esc_like((string) $c['value']).'%'; - } - - if (empty($where_clauses)) { - return; - } - - $where_sql = implode(' OR ', $where_clauses); - - $sql = $wpdb->prepare("SELECT DISTINCT faq_id FROM {$table} WHERE {$where_sql}", $params); - - $faq_ids = $wpdb->get_col($sql); - if (empty($faq_ids)) { - // cache negative result to avoid repeated queries - set_transient($transient_key, '__FQJ_EMPTY__', $cache_ttl); - echo "\n\n"; - - return; - } - - // Now fetch the FAQ posts - $args = [ - 'post_type' => 'faq_item', - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'post__in' => $faq_ids, - 'orderby' => 'post__in', - ]; - $faqs = get_posts($args); - if (! $faqs) { - set_transient($transient_key, '__FQJ_EMPTY__', $cache_ttl); - echo "\n\n"; - - return; - } - - $main_entities = []; - foreach ($faqs as $f) { - $q = get_the_title($f); - $a = wp_strip_all_tags(apply_filters('the_content', $f->post_content)); - if (empty($q) || empty($a)) { - continue; - } - - $main_entities[] = [ - '@type' => 'Question', - 'name' => $q, - 'acceptedAnswer' => [ - '@type' => 'Answer', - 'text' => $a, - ], - ]; - } - - if (empty($main_entities)) { - set_transient($transient_key, '__FQJ_EMPTY__', $cache_ttl); - echo "\n\n"; - - return; - } - - $json_ld = [ - '@context' => 'https://schema.org', - '@type' => ($output_type === 'faqpage' ? 'FAQPage' : 'FAQSection'), - 'mainEntity' => $main_entities, - ]; - - $script = ''; - - echo "\n\n"; - echo $script."\n"; - - set_transient($transient_key, $script, $cache_ttl); +function fqj_maybe_print_faq_jsonld() { + if ( is_admin() ) { + return; + } + + global $post; + if ( ! $post ) { + return; + } + $current_id = intval( $post->ID ); + + $opts = get_option( FQJ_OPTION_KEY ); + $cache_ttl = isset( $opts['cache_ttl'] ) ? intval( $opts['cache_ttl'] ) : 12 * HOUR_IN_SECONDS; + $output_type = isset( $opts['output_type'] ) ? $opts['output_type'] : 'faqsection'; + + $transient_key = 'fqj_faq_json_' . $current_id; + $cached = get_transient( $transient_key ); + if ( false !== $cached ) { + if ( '__FQJ_EMPTY__' === $cached ) { + echo "\n\n"; + } elseif ( '' !== trim( $cached ) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo "\n\n" . $cached . "\n"; + } else { + // Should not happen, but for safety. + echo "\n\n"; + } + + return; + } + + // Determine candidate mapping values for lookup. + $candidate_values = array(); + + // Direct post id mapping. + $candidate_values[] = array( + 'type' => 'post', + 'value' => (string) $current_id, + ); + + // Current post type mapping. + $pt = get_post_type( $current_id ); + if ( $pt ) { + $candidate_values[] = array( + 'type' => 'post_type', + 'value' => $pt, + ); + } + + // Terms. + $terms = wp_get_post_terms( $current_id ); + if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { + foreach ( $terms as $t ) { + $candidate_values[] = array( + 'type' => 'term', + 'value' => (string) $t->term_id, + ); + } + } + + // URL mapping - try canonical/permalink. + $permalink = get_permalink( $current_id ); + if ( $permalink ) { + $candidate_values[] = array( + 'type' => 'url', + 'value' => rtrim( strtok( $permalink, '?' ), '/' ), + ); + } + + // Always include global mapping. + $candidate_values[] = array( + 'type' => 'global', + 'value' => '1', + ); + + // Query mapping table for matching faq_ids. + global $wpdb; + $table = FQJ_DB_TABLE; + + // Build WHERE clauses for each candidate. + $where_clauses = array(); + $params = array(); + foreach ( $candidate_values as $c ) { + // Use exact match for all types to prevent partial matches (e.g. post ID 1 matching 10, 11, etc.) + // Since we store scalar values directly (or serialized which resolves to string), = is safer. + // For 'url', if we wanted partial matches we'd need a different logic, but here we expect exact path match. + $where_clauses[] = '(mapping_type = %s AND mapping_value = %s)'; + $params[] = $c['type']; + $params[] = (string) $c['value']; + } + + if ( empty( $where_clauses ) ) { + return; + } + + $where_sql = implode( ' OR ', $where_clauses ); + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $sql = $wpdb->prepare( "SELECT DISTINCT faq_id FROM {$table} WHERE {$where_sql}", $params ); + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $faq_ids = $wpdb->get_col( $sql ); + if ( empty( $faq_ids ) ) { + // cache negative result to avoid repeated queries. + set_transient( $transient_key, '__FQJ_EMPTY__', $cache_ttl ); + echo "\n\n"; + + return; + } + + // Now fetch the FAQ posts. + $args = array( + 'post_type' => 'faq_item', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'post__in' => $faq_ids, + 'orderby' => 'post__in', + ); + $faqs = get_posts( $args ); + if ( ! $faqs ) { + set_transient( $transient_key, '__FQJ_EMPTY__', $cache_ttl ); + echo "\n\n"; + + return; + } + + $main_entities = array(); + foreach ( $faqs as $f ) { + $q = get_the_title( $f ); + $a = wp_strip_all_tags( apply_filters( 'the_content', $f->post_content ) ); + if ( empty( $q ) || empty( $a ) ) { + continue; + } + + $main_entities[] = array( + '@type' => 'Question', + 'name' => $q, + 'acceptedAnswer' => array( + '@type' => 'Answer', + 'text' => $a, + ), + ); + } + + if ( empty( $main_entities ) ) { + set_transient( $transient_key, '__FQJ_EMPTY__', $cache_ttl ); + echo "\n\n"; + + return; + } + + $json_ld = array( + '@context' => 'https://schema.org', + '@type' => ( 'faqpage' === $output_type ? 'FAQPage' : 'FAQSection' ), + 'mainEntity' => $main_entities, + ); + + $script = ''; + + echo "\n\n"; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $script . "\n"; + + set_transient( $transient_key, $script, $cache_ttl ); } -add_action('wp_head', 'fqj_maybe_print_faq_jsonld', 1); +add_action( 'wp_head', 'fqj_maybe_print_faq_jsonld', 1 ); diff --git a/includes/health.php b/includes/health.php index 836e639..75d3904 100644 --- a/includes/health.php +++ b/includes/health.php @@ -1,8 +1,16 @@ admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('fqj_health_nonce'), - ]); +function fqj_health_admin_assets( $hook ) { + // check page hook: load only on fqj-health page. + if ( 'faq_item_page_fqj-health' !== $hook ) { + return; + } + + wp_enqueue_script( + 'fqj-health-js', + FQJ_PLUGIN_URL . 'assets/js/fqj-health.js', + array( 'jquery' ), + '1.0', + true + ); + wp_localize_script( + 'fqj-health-js', + 'fqjHealth', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'fqj_health_nonce' ), + ) + ); } -add_action('admin_enqueue_scripts', 'fqj_health_admin_assets'); +add_action( 'admin_enqueue_scripts', 'fqj_health_admin_assets' ); /** * Render health page */ -function fqj_render_health_page() -{ - if (! current_user_can('manage_options')) { - return; - } - $queue_len = fqj_queue_length(); - $last_run_ts = get_option('fqj_last_queue_run', 0); - $log = get_option('fqj_invalidation_log', []); - if (! is_array($log)) { - $log = []; - } - - ?> -Pending invalidation items in queue:
-Last queue run:
- -- - - - -
- -Shows up to the last runs (newest first).
- -| Timestamp | -Processed | -Sample post IDs | -No invalidation runs logged yet. | '; - } else { - foreach ($log as $entry) { - $ts = isset($entry['ts']) ? intval($entry['ts']) : 0; - $processed = isset($entry['processed']) ? intval($entry['processed']) : 0; - $sample = isset($entry['sample']) && is_array($entry['sample']) ? $entry['sample'] : []; - echo '
|---|---|---|
| '.esc_html($ts ? date_i18n('Y-m-d H:i:s', $ts).' ('.human_time_diff($ts, time()).' ago)' : 'n/a').' | '; - echo ''.esc_html($processed).' | '; - echo ''.esc_html(implode(', ', $sample)).' | '; - echo '
wp-cron.php or use WP-CLI to process the queue manually.Pending invalidation items in queue: + +
+Last queue run: + +
+ ++ + + + +
+ +Shows up to the last runs (newest first).
+ +| Timestamp | +Processed | +Sample post IDs | +No invalidation runs logged yet. | '; + } else { + foreach ( $log as $entry ) { + $ts = isset( $entry['ts'] ) ? intval( $entry['ts'] ) : 0; + $processed = isset( $entry['processed'] ) ? intval( $entry['processed'] ) : 0; + $sample = isset( $entry['sample'] ) && is_array( $entry['sample'] ) ? $entry['sample'] : array(); + echo '
|---|---|---|
| ' . esc_html( + $ts + ? date_i18n( 'Y-m-d H:i:s', $ts ) . ' (' . human_time_diff( $ts, time() ) . ' ago)' + : 'n/a' + ) . ' | '; + echo '' . esc_html( $processed ) . ' | '; + echo '' . esc_html( implode( ', ', $sample ) ) . ' | '; + echo '
wp-cron.php or use WP-CLI to process the queue manually.
+ FAQ JSON-LD queue: %d posts pending invalidation. The background worker (WP-Cron) will process them in batches.
FAQ JSON-LD queue: %d ' . + 'posts pending invalidation. The background worker (WP-Cron) will process them in batches.
Settings saved.
'; - } - - $cache_ttl = isset($options['cache_ttl']) ? intval($options['cache_ttl']) : 12 * HOUR_IN_SECONDS; - $batch_size = isset($options['batch_size']) ? intval($options['batch_size']) : 500; - $output_type = isset($options['output_type']) ? $options['output_type'] : 'faqsection'; - ?> -- Purge all FAQ transients: -
- - -All FAQ transients purged.
Settings saved.
'; + } + + $cache_ttl = isset( $options['cache_ttl'] ) ? intval( $options['cache_ttl'] ) : 12 * HOUR_IN_SECONDS; + $batch_size = isset( $options['batch_size'] ) ? intval( $options['batch_size'] ) : 500; + $output_type = isset( $options['output_type'] ) ? $options['output_type'] : 'faqsection'; + ?> ++ Purge all FAQ transients: +
+ + + +All FAQ transients purged.