diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index 22227cc4e..0bc320444 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -38,6 +38,7 @@ 'plugin-check__include-experimental' ); const useAi = document.getElementById( 'plugin-check__use-ai' ); + const useAiName = document.getElementById( 'plugin-check__use-ai-name' ); // Handle disabling the Check it button when a plugin is not selected. function canRunChecks() { @@ -92,6 +93,9 @@ if ( useAi ) { useAi.disabled = true; } + if ( useAiName ) { + useAiName.disabled = true; + } if ( includeExperimental ) { includeExperimental.disabled = true; } @@ -144,6 +148,9 @@ if ( useAi ) { useAi.disabled = false; } + if ( useAiName ) { + useAiName.disabled = false; + } if ( includeExperimental ) { includeExperimental.disabled = false; } @@ -596,6 +603,10 @@ includeExperimental && includeExperimental.checked ? 1 : 0 ); pluginCheckData.append( 'use-ai', useAi && useAi.checked ? 1 : 0 ); + pluginCheckData.append( + 'use-ai-name', + useAiName && useAiName.checked ? 1 : 0 + ); for ( let i = 0; i < data.checks.length; i++ ) { pluginCheckData.append( 'checks[]', data.checks[ i ] ); @@ -669,6 +680,10 @@ includeExperimental && includeExperimental.checked ? 1 : 0 ); pluginCheckData.append( 'use-ai', useAi && useAi.checked ? 1 : 0 ); + pluginCheckData.append( + 'use-ai-name', + useAiName && useAiName.checked ? 1 : 0 + ); for ( let i = 0; i < categoriesList.length; i++ ) { if ( categoriesList[ i ].checked ) { @@ -957,6 +972,10 @@ includeExperimental && includeExperimental.checked ? 1 : 0 ); pluginCheckData.append( 'use-ai', useAi && useAi.checked ? 1 : 0 ); + pluginCheckData.append( + 'use-ai-name', + useAiName && useAiName.checked ? 1 : 0 + ); for ( let i = 0; i < typesList.length; i++ ) { if ( typesList[ i ].checked ) { diff --git a/assets/js/plugin-check-namer.js b/assets/js/plugin-check-namer.js deleted file mode 100644 index f8e3d90cb..000000000 --- a/assets/js/plugin-check-namer.js +++ /dev/null @@ -1,432 +0,0 @@ -/** - * Plugin Check Namer tool. - */ - -/* global pluginCheckNamer */ - -( function () { - 'use strict'; - - function setText( el, text ) { - if ( ! el ) { - return; - } - el.textContent = text; - } - - function setHtml( el, html ) { - if ( ! el ) { - return; - } - el.innerHTML = html; - } - - function escapeHtml( text ) { - const div = document.createElement( 'div' ); - div.textContent = text; - return div.innerHTML; - } - - document.addEventListener( 'DOMContentLoaded', function () { - const form = document.getElementById( 'plugin-check-namer-form' ); - const input = document.getElementById( 'plugin_check_namer_input' ); - - if ( ! form || ! input || ! window.pluginCheckNamer ) { - return; - } - - const authorInput = document.getElementById( - 'plugin_check_namer_author' - ); - const modelSelect = document.getElementById( - 'plugin_check_namer_model' - ); - - const submitBtn = document.getElementById( - 'plugin-check-namer-submit' - ); - const spinner = document.getElementById( 'plugin-check-namer-spinner' ); - - const resultWrap = document.getElementById( - 'plugin-check-namer-result' - ); - const verdictEl = document.getElementById( - 'plugin-check-namer-verdict' - ); - const explainEl = document.getElementById( - 'plugin-check-namer-explanation' - ); - const verdictContainer = document.getElementById( - 'plugin-check-namer-verdict-container' - ); - const confusionPluginsDiv = document.getElementById( - 'plugin-check-namer-confusion-plugins' - ); - const confusionPluginsList = document.getElementById( - 'plugin-check-namer-confusion-plugins-list' - ); - const confusionOthersDiv = document.getElementById( - 'plugin-check-namer-confusion-others' - ); - const confusionOthersList = document.getElementById( - 'plugin-check-namer-confusion-others-list' - ); - const timingDiv = document.getElementById( - 'plugin-check-namer-timing' - ); - const timingValue = document.getElementById( - 'plugin-check-namer-timing-value' - ); - const tokensDiv = document.getElementById( - 'plugin-check-namer-tokens' - ); - const tokensValue = document.getElementById( - 'plugin-check-namer-tokens-value' - ); - const errorDiv = document.getElementById( 'plugin-check-namer-error' ); - const errorEl = errorDiv ? errorDiv.querySelector( 'p' ) : null; - - function setLoading( isLoading ) { - if ( spinner ) { - spinner.classList.toggle( 'is-active', isLoading ); - } - submitBtn.disabled = isLoading; - } - - form.addEventListener( 'submit', function ( event ) { - event.preventDefault(); - - const name = ( input.value || '' ).trim(); - if ( ! name ) { - setText( errorEl, pluginCheckNamer.messages.missingName ); - if ( errorEl ) { - errorEl.classList.remove( 'plugin-check-namer-hidden' ); - } - return; - } - - // Clear previous results. - if ( resultWrap ) { - resultWrap.classList.add( 'plugin-check-namer-hidden' ); - } - if ( verdictEl ) { - setText( verdictEl, '' ); - } - if ( explainEl ) { - setHtml( explainEl, '' ); - } - if ( errorDiv ) { - errorDiv.classList.add( 'plugin-check-namer-hidden' ); - } - if ( errorEl ) { - setText( errorEl, '' ); - } - if ( confusionPluginsDiv ) { - confusionPluginsDiv.classList.add( - 'plugin-check-namer-hidden' - ); - } - if ( confusionPluginsList ) { - confusionPluginsList.innerHTML = ''; - } - if ( confusionOthersDiv ) { - confusionOthersDiv.classList.add( 'plugin-check-namer-hidden' ); - } - if ( confusionOthersList ) { - confusionOthersList.innerHTML = ''; - } - if ( timingDiv ) { - timingDiv.classList.add( 'plugin-check-namer-hidden' ); - } - if ( timingValue ) { - setText( timingValue, '' ); - } - if ( tokensDiv ) { - tokensDiv.classList.add( 'plugin-check-namer-hidden' ); - } - if ( tokensValue ) { - setText( tokensValue, '' ); - } - if ( verdictContainer ) { - verdictContainer.classList.add( 'plugin-check-namer-hidden' ); - } - - // Record start time. - const startTime = Date.now(); - - setLoading( true ); - - const formData = new FormData(); - formData.append( 'action', 'plugin_check_namer_analyze' ); - formData.append( 'nonce', pluginCheckNamer.nonce ); - formData.append( 'plugin_name', name ); - if ( authorInput ) { - formData.append( - 'author_name', - ( authorInput.value || '' ).trim() - ); - } - if ( modelSelect ) { - formData.append( - 'model_preference', - ( modelSelect.value || '' ).trim() - ); - } - - fetch( pluginCheckNamer.ajaxUrl, { - method: 'POST', - credentials: 'same-origin', - body: formData, - } ) - .then( function ( response ) { - return response.json(); - } ) - .then( function ( payload ) { - if ( ! payload || ! payload.success ) { - throw new Error( - payload && payload.data && payload.data.message - ? payload.data.message - : pluginCheckNamer.messages.genericError - ); - } - - setText( verdictEl, payload.data.verdict || '' ); - setHtml( explainEl, payload.data.explanation || '' ); - - // Set border color based on verdict. - if ( verdictContainer ) { - const verdict = ( - payload.data.verdict || '' - ).toLowerCase(); - let borderColor = '#2271b1'; // Default blue. - - if ( verdict.indexOf( 'disallowed' ) !== -1 ) { - borderColor = '#d63638'; // Red for disallowed. - } else if ( - verdict.indexOf( 'good' ) !== -1 || - verdict.indexOf( 'low' ) !== -1 || - verdict.indexOf( 'no issues' ) !== -1 - ) { - borderColor = '#00a32a'; // Green for good. - } else if ( - verdict.indexOf( 'generally allowable' ) !== -1 || - verdict.indexOf( 'allowable' ) !== -1 - ) { - borderColor = '#2271b1'; // Blue for generally allowable. - } else if ( - verdict.indexOf( 'review' ) !== -1 || - verdict.indexOf( 'medium' ) !== -1 || - verdict.indexOf( 'issues found' ) !== -1 - ) { - borderColor = '#dba617'; // Yellow/orange for needs review. - } else if ( - verdict.indexOf( 'problematic' ) !== -1 || - verdict.indexOf( 'high' ) !== -1 - ) { - borderColor = '#d63638'; // Red for problematic. - } - - verdictContainer.style.borderLeftColor = borderColor; - verdictContainer.classList.remove( - 'plugin-check-namer-hidden' - ); - } - - // Calculate and display elapsed time. - if ( timingDiv && timingValue ) { - const endTime = Date.now(); - const elapsedSeconds = Math.round( - ( endTime - startTime ) / 1000 - ); - timingValue.textContent = - elapsedSeconds + ' ' + 'seconds'; - timingDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } - - // Display token usage if available. - if ( - tokensDiv && - tokensValue && - payload.data.token_usage - ) { - const tokenUsage = payload.data.token_usage; - let tokensText = ''; - - // Add AI provider and model info if available. - if ( payload.data.ai_info ) { - const parts = []; - if ( payload.data.ai_info.provider ) { - parts.push( payload.data.ai_info.provider ); - } - if ( payload.data.ai_info.model ) { - parts.push( payload.data.ai_info.model ); - } - if ( parts.length ) { - tokensText = parts.join( ' / ' ) + ' - '; - } - } - - const totalTokens = - tokenUsage.total_tokens || - ( tokenUsage.prompt_tokens && - tokenUsage.completion_tokens - ? tokenUsage.prompt_tokens + - tokenUsage.completion_tokens - : null ); - - if ( totalTokens ) { - tokensText += totalTokens + ' total'; - } - - // Add breakdown with prompt and completion tokens. - if ( - tokenUsage.prompt_tokens && - tokenUsage.completion_tokens - ) { - tokensText += - ' (' + - tokenUsage.prompt_tokens + - ' prompt + ' + - tokenUsage.completion_tokens + - ' completion)'; - } - - // Add similar name query tokens if available. - if ( - tokenUsage.similar_name && - tokenUsage.similar_name.total_tokens - ) { - tokensText += - ' [Similar name query: ' + - tokenUsage.similar_name.total_tokens + - ' tokens]'; - } - - tokensValue.textContent = tokensText; - tokensDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } - - // Display confusion_existing_plugins if available. - if ( - confusionPluginsDiv && - confusionPluginsList && - payload.data.confusion_existing_plugins && - payload.data.confusion_existing_plugins.length > 0 - ) { - confusionPluginsList.innerHTML = ''; - payload.data.confusion_existing_plugins.forEach( - function ( plugin ) { - const div = document.createElement( 'div' ); - div.className = - 'plugin-check-namer-confusion-item'; - div.innerHTML = - '' + - escapeHtml( plugin.name || '' ) + - '' + - ( plugin.active_installations - ? ' (' + - escapeHtml( - plugin.active_installations - ) + - ' ' + - 'active installs' + - ')' - : '' ) + - ( plugin.owner_username - ? ' - ' + - escapeHtml( plugin.owner_username ) + - '' - : '' ) + - '
' + - '' + - escapeHtml( plugin.explanation || '' ) + - '' + - ( plugin.link - ? '
' + - escapeHtml( plugin.link ) + - '' - : '' ); - confusionPluginsList.appendChild( div ); - } - ); - confusionPluginsDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } else if ( confusionPluginsDiv ) { - confusionPluginsDiv.classList.add( - 'plugin-check-namer-hidden' - ); - } - - // Display confusion_existing_others if available. - if ( - confusionOthersDiv && - confusionOthersList && - payload.data.confusion_existing_others && - payload.data.confusion_existing_others.length > 0 - ) { - confusionOthersList.innerHTML = ''; - payload.data.confusion_existing_others.forEach( - function ( item ) { - const div = document.createElement( 'div' ); - div.className = - 'plugin-check-namer-confusion-item plugin-check-namer-confusion-item-others'; - div.innerHTML = - '' + - escapeHtml( item.name || '' ) + - '' + - '
' + - '' + - escapeHtml( item.explanation || '' ) + - '' + - ( item.link - ? '
' + - escapeHtml( item.link ) + - '' - : '' ); - confusionOthersList.appendChild( div ); - } - ); - confusionOthersDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } else if ( confusionOthersDiv ) { - confusionOthersDiv.classList.add( - 'plugin-check-namer-hidden' - ); - } - - if ( resultWrap ) { - resultWrap.classList.remove( - 'plugin-check-namer-hidden' - ); - } - } ) - .catch( function ( err ) { - setText( - errorEl, - err && err.message - ? err.message - : pluginCheckNamer.messages.genericError - ); - - if ( errorDiv ) { - errorDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } - } ) - .finally( function () { - setLoading( false ); - } ); - } ); - } ); -} )(); diff --git a/includes/Admin/Admin_AJAX.php b/includes/Admin/Admin_AJAX.php index eef695078..cb5afad13 100644 --- a/includes/Admin/Admin_AJAX.php +++ b/includes/Admin/Admin_AJAX.php @@ -226,12 +226,13 @@ public function clean_up_environment() { public function get_checks_to_run() { $this->check_request_validity(); - $categories = filter_input( INPUT_POST, 'categories', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); - $categories = is_null( $categories ) ? array() : $categories; - $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); - $checks = is_null( $checks ) ? array() : $checks; - $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); - $runner = $this->get_ajax_runner(); + $categories = filter_input( INPUT_POST, 'categories', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $categories = is_null( $categories ) ? array() : $categories; + $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $checks = is_null( $checks ) ? array() : $checks; + $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); + $use_ai_name = 1 === filter_input( INPUT_POST, 'use-ai-name', FILTER_VALIDATE_INT ); + $runner = $this->get_ajax_runner(); if ( is_wp_error( $runner ) ) { wp_send_json_error( $runner, 500 ); @@ -241,6 +242,7 @@ public function get_checks_to_run() { $this->configure_runner( $runner ); $runner->set_categories( $categories ); $runner->set_use_ai( $use_ai ); + $runner->set_use_ai_name( $use_ai_name ); $plugin_basename = $runner->get_plugin_basename(); $checks_to_run = $runner->get_checks_to_run(); @@ -295,6 +297,7 @@ public function run_checks() { $include_experimental = 1 === filter_input( INPUT_POST, 'include-experimental', FILTER_VALIDATE_INT ); $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); + $use_ai_name = 1 === filter_input( INPUT_POST, 'use-ai-name', FILTER_VALIDATE_INT ); $types = filter_input( INPUT_POST, 'types', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); $types = is_null( $types ) ? array( 'error', 'warning' ) : $types; @@ -303,6 +306,7 @@ public function run_checks() { $runner->set_check_slugs( $checks ); $runner->set_plugin( $plugin ); $runner->set_use_ai( $use_ai ); + $runner->set_use_ai_name( $use_ai_name ); $results = $runner->run(); } catch ( Exception $error ) { wp_send_json_error( diff --git a/includes/Admin/Namer_Page.php b/includes/Admin/Namer_Page.php deleted file mode 100644 index cb4b0d415..000000000 --- a/includes/Admin/Namer_Page.php +++ /dev/null @@ -1,460 +0,0 @@ -hook_suffix = add_management_page( - __( 'Plugin Check Namer', 'plugin-check' ), - __( 'Plugin Check Namer', 'plugin-check' ), - 'manage_options', - self::MENU_SLUG, - array( $this, 'render_page' ) - ); - } - - /** - * Enqueues scripts for the tools page. - * - * @since 1.8.0 - * - * @param string $hook_suffix Current admin page hook suffix. - */ - public function enqueue_scripts( $hook_suffix ) { - if ( $hook_suffix !== $this->hook_suffix ) { - return; - } - - wp_enqueue_style( - 'plugin-check-admin', - plugins_url( 'assets/css/plugin-check-admin.css', WP_PLUGIN_CHECK_MAIN_FILE ), - array(), - WP_PLUGIN_CHECK_VERSION - ); - - wp_enqueue_script( - 'plugin-check-namer', - plugins_url( 'assets/js/plugin-check-namer.js', WP_PLUGIN_CHECK_MAIN_FILE ), - array(), - WP_PLUGIN_CHECK_VERSION, - true - ); - - wp_localize_script( - 'plugin-check-namer', - 'pluginCheckNamer', - array( - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'plugin_check_namer_ajax' ), - 'messages' => array( - 'missingName' => __( 'Please enter a plugin name.', 'plugin-check' ), - 'genericError' => __( 'An unexpected error occurred.', 'plugin-check' ), - ), - ) - ); - } - - /** - * AJAX handler to analyze a plugin name. - * - * @since 1.8.0 - */ - public function ajax_analyze() { - check_ajax_referer( 'plugin_check_namer_ajax', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'plugin-check' ) ) ); - } - - $name = $this->get_plugin_name_from_request(); - if ( empty( $name ) ) { - wp_send_json_error( array( 'message' => __( 'Please enter a plugin name.', 'plugin-check' ) ) ); - } - - $author = $this->get_author_name_from_request(); - - $model_preference = $this->get_model_preference_from_request(); - $ai_config = $this->get_ai_config( $model_preference ); - if ( is_wp_error( $ai_config ) ) { - wp_send_json_error( array( 'message' => $ai_config->get_error_message() ) ); - } - - $analysis = $this->run_name_analysis( $ai_config['model_preference'], $name, $author ); - if ( is_wp_error( $analysis ) ) { - wp_send_json_error( array( 'message' => $analysis->get_error_message() ) ); - } - - $parsed = $this->parse_analysis( $analysis ); - $response = $this->build_ajax_response( $parsed, $analysis, $ai_config ); - - wp_send_json_success( $response ); - } - - /** - * Builds AJAX response from parsed analysis. - * - * @since 1.8.0 - * - * @param array $parsed Parsed analysis. - * @param string|array $analysis Raw analysis. - * @param array $ai_config AI configuration with model preference info. - * @return array Response array. - */ - protected function build_ajax_response( $parsed, $analysis, $ai_config = array() ) { - $raw_output = $this->get_raw_output( $parsed, $analysis ); - $raw_output = $this->format_json_output( $raw_output ); - - $response = array( - 'verdict' => $parsed['verdict'], - 'explanation' => $parsed['explanation'], - 'raw' => $raw_output, - ); - - if ( ! empty( $parsed['confusion_existing_plugins'] ) ) { - $response['confusion_existing_plugins'] = $parsed['confusion_existing_plugins']; - } - if ( ! empty( $parsed['confusion_existing_others'] ) ) { - $response['confusion_existing_others'] = $parsed['confusion_existing_others']; - } - if ( ! empty( $parsed['token_usage'] ) ) { - $response['token_usage'] = $parsed['token_usage']; - } - - // Add AI model preference information. - if ( ! empty( $ai_config['model_preference'] ) ) { - $response['ai_info'] = array( - 'model' => $ai_config['model_preference'], - ); - } - - return $response; - } - - /** - * Gets plugin name from request. - * - * @since 1.8.0 - * - * @return string Plugin name or empty string. - */ - protected function get_plugin_name_from_request() { - $name = isset( $_POST['plugin_name'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_name'] ) ) : ''; - return trim( $name ); - } - - /** - * Gets author name from request. - * - * @since 1.8.0 - * - * @return string Author name or empty string. - */ - protected function get_author_name_from_request() { - $author = isset( $_POST['author_name'] ) ? sanitize_text_field( wp_unslash( $_POST['author_name'] ) ) : ''; - return trim( $author ); - } - - /** - * Renders the page. - * - * @since 1.8.0 - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function render_page() { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - $ai_config = $this->get_ai_config(); - ?> -
-

- - -
-

- get_error_message() ); - ?> -

-
- - check_ai_prerequisites() && is_wp_error( $this->check_ai_connectors() ) ) { - ?> -

- - - -

- - -
- - - - - - - - - - - - - - - - - -

- -
- -
- -

-

- - -

-
- -

- -
-

-
-

- - -

-

- - -

-

- - -

-

- - -

-
-
-

-
-
- -
-

-
-
-
-
- get_model_preference_from_request(); - $user_id = get_current_user_id(); - - if ( empty( $input ) ) { - $this->handle_analyze_error( $user_id, '', new WP_Error( 'missing_input', __( 'Please enter a plugin name.', 'plugin-check' ) ) ); - return; - } - - $ai_config = $this->get_ai_config( $model_preference ); - if ( is_wp_error( $ai_config ) ) { - $this->handle_analyze_error( $user_id, $input, $ai_config ); - return; - } - - $analysis = $this->run_name_analysis( $ai_config['model_preference'], $input, $author ); - - if ( is_wp_error( $analysis ) ) { - $this->handle_analyze_error( $user_id, $input, $analysis ); - return; - } - - $this->store_result( - $user_id, - array( - 'input' => $input, - 'analysis' => $analysis, - ) - ); - wp_safe_redirect( $this->get_page_url() ); - exit; - } - - /** - * Handles analyze error and redirects. - * - * @since 1.8.0 - * - * @param int $user_id User ID. - * @param string $input Input value. - * @param WP_Error $error Error object. - */ - protected function handle_analyze_error( $user_id, $input, $error ) { - $this->store_result( - $user_id, - array( - 'input' => $input, - 'error' => $error, - ) - ); - wp_safe_redirect( $this->get_page_url() ); - exit; - } - - /** - * Gets the page URL. - * - * @since 1.8.0 - * - * @return string - */ - protected function get_page_url() { - return add_query_arg( array( 'page' => self::MENU_SLUG ), admin_url( 'tools.php' ) ); - } -} diff --git a/includes/CLI/Plugin_Check_Command.php b/includes/CLI/Plugin_Check_Command.php index 834478e78..119e5b8d6 100644 --- a/includes/CLI/Plugin_Check_Command.php +++ b/includes/CLI/Plugin_Check_Command.php @@ -149,6 +149,9 @@ public function __construct( Plugin_Context $plugin_context ) { * [--ai-model=] * : AI model preference for analysis (e.g., 'openai::gpt-4o'). Requires --ai. * + * [--ai-name] + * : Enable AI-based plugin name checking. + * * ## EXAMPLES * * wp plugin check akismet @@ -191,6 +194,7 @@ public function check( $args, $assoc_args ) { 'mode' => 'new', 'ai' => false, 'ai-model' => '', + 'ai-name' => false, ) ); @@ -252,6 +256,7 @@ static function ( $dirs ) use ( $excluded_files ) { $runner->set_slug( $options['slug'] ); $runner->set_mode( $options['mode'] ); $runner->set_use_ai( $options['ai'] ); + $runner->set_use_ai_name( $options['ai-name'] ); if ( ! empty( $options['ai-model'] ) ) { $runner->set_ai_model_preference( $options['ai-model'] ); } diff --git a/includes/Checker/Abstract_Check_Runner.php b/includes/Checker/Abstract_Check_Runner.php index 8f66010ac..69c1ecf19 100644 --- a/includes/Checker/Abstract_Check_Runner.php +++ b/includes/Checker/Abstract_Check_Runner.php @@ -21,6 +21,7 @@ * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.TooManyFields) */ abstract class Abstract_Check_Runner implements Check_Runner { @@ -42,6 +43,14 @@ abstract class Abstract_Check_Runner implements Check_Runner { */ protected $use_ai = false; + /** + * Whether AI plugin name checking is enabled. + * + * @since x.x.x + * @var bool + */ + protected $use_ai_name = false; + /** * AI model preference for analysis. * @@ -324,6 +333,17 @@ final public function set_use_ai( $use_ai ) { $this->use_ai = (bool) $use_ai; } + /** + * Sets whether to use AI plugin name check. + * + * @since x.x.x + * + * @param bool $use_ai_name True to enable AI name check, false to disable. + */ + final public function set_use_ai_name( $use_ai_name ) { + $this->use_ai_name = (bool) $use_ai_name; + } + /** * Sets the AI model preference for analysis. * @@ -342,11 +362,33 @@ final public function set_ai_model_preference( $model_preference ) { * * @return bool True if AI analysis should be used, false otherwise. */ - protected function should_use_ai() { + public function should_use_ai() { // Check if explicitly set via setter (e.g., CLI flag or checkbox). return $this->use_ai; } + /** + * Determines if AI plugin name check should be used. + * + * @since x.x.x + * + * @return bool True if AI name check should be used, false otherwise. + */ + public function should_use_ai_name() { + return $this->use_ai_name; + } + + /** + * Gets the AI model preference for analysis. + * + * @since x.x.x + * + * @return string Model preference. + */ + final public function get_ai_model_preference() { + return $this->ai_model_preference; + } + /** * Sets categories for filtering the checks. * diff --git a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php new file mode 100644 index 000000000..10aa17eab --- /dev/null +++ b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php @@ -0,0 +1,288 @@ +should_run_ai_check( $runner ) ) { + return; + } + + $model_preference = $this->get_model_preference( $runner ); + $plugin_data = $this->get_plugin_name_and_author( $result ); + if ( ! $plugin_data ) { + return; + } + + $analysis = $this->run_name_analysis( $model_preference, $plugin_data['name'], $plugin_data['author'] ); + $this->handle_analysis_response( $result, $analysis, $plugin_data['file'] ); + } + + /** + * Determines if the AI check should run. + * + * @since x.x.x + * + * @param mixed $runner The active runner. + * @return bool True if the check should run, false otherwise. + */ + private function should_run_ai_check( $runner ) { + if ( ! $runner ) { + return false; + } + if ( ! method_exists( $runner, 'should_use_ai_name' ) ) { + return false; + } + if ( ! $runner->should_use_ai_name() ) { + return false; + } + if ( is_wp_error( $this->check_ai_prerequisites() ) ) { + return false; + } + if ( is_wp_error( $this->check_ai_connectors() ) ) { + return false; + } + return true; + } + + /** + * Gets the selected AI model preference. + * + * @since x.x.x + * + * @param mixed $runner The active runner. + * @return string The model preference. + */ + private function get_model_preference( $runner ) { + $model = ''; + if ( $runner && method_exists( $runner, 'get_ai_model_preference' ) ) { + $model = $runner->get_ai_model_preference(); + } + if ( empty( $model ) && class_exists( Settings_Page::class ) ) { + $model = Settings_Page::get_model_preference(); + } + return $model; + } + + /** + * Gets the plugin name and author from headers. + * + * @since x.x.x + * + * @param Check_Result $result The check result. + * @return array|null Name and author if found, null otherwise. + */ + private function get_plugin_name_and_author( Check_Result $result ) { + if ( ! function_exists( 'get_plugin_data' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $file = $result->plugin()->main_file(); + $header = get_plugin_data( $file ); + $name = isset( $header['Name'] ) ? $header['Name'] : ''; + $author = isset( $header['AuthorName'] ) ? $header['AuthorName'] : ''; + + if ( empty( $name ) ) { + return null; + } + + return array( + 'name' => $name, + 'author' => $author, + 'file' => $file, + ); + } + + /** + * Handles the AI analysis response and parses results. + * + * @since x.x.x + * + * @param Check_Result $result The check result. + * @param mixed $analysis The analysis response or WP_Error. + * @param string $plugin_main_file The plugin main file path. + */ + private function handle_analysis_response( Check_Result $result, $analysis, string $plugin_main_file ) { + if ( is_wp_error( $analysis ) ) { + $this->add_result_warning_for_file( + $result, + sprintf( + /* translators: %s: Error message. */ + __( 'AI plugin name check failed: %s', 'plugin-check' ), + $analysis->get_error_message() + ), + 'ai_name_check_failed', + $plugin_main_file + ); + return; + } + + $parsed = $this->parse_analysis( $analysis ); + $this->process_analysis_results( $result, $parsed, $plugin_main_file ); + } + + /** + * Process analysis results and add appropriate warnings/errors. + * + * @since x.x.x + * + * @param Check_Result $result The check result. + * @param array $parsed The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function process_analysis_results( Check_Result $result, array $parsed, string $plugin_main_file ) { + if ( ! isset( $parsed['processed_data'] ) ) { + return; + } + + $data = $parsed['processed_data']; + + $this->check_disallowed_name( $result, $data, $plugin_main_file ); + $this->check_naming_issues( $result, $data, $plugin_main_file ); + $this->check_owner_issues( $result, $data, $plugin_main_file ); + $this->check_similar_plugins( $result, $parsed, $plugin_main_file ); + } + + /** + * Check if the plugin name is disallowed. + * + * @since x.x.x + * + * @param Check_Result $result The check result. + * @param array $data The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function check_disallowed_name( Check_Result $result, array $data, string $plugin_main_file ) { + if ( ! empty( $data['disallowed'] ) ) { + $msg = isset( $data['disallowed_explanation'] ) ? $data['disallowed_explanation'] : __( 'The plugin name is disallowed.', 'plugin-check' ); + $this->add_result_error_for_file( $result, $msg, 'plugin_name_disallowed', $plugin_main_file ); + } + } + + /** + * Check for possible naming issues. + * + * @since x.x.x + * + * @param Check_Result $result The check result. + * @param array $data The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function check_naming_issues( Check_Result $result, array $data, string $plugin_main_file ) { + if ( ! empty( $data['possible_naming_issues'] ) ) { + $msg = isset( $data['naming_explanation'] ) ? $data['naming_explanation'] : __( 'The plugin name has possible naming issues.', 'plugin-check' ); + $this->add_result_warning_for_file( $result, $msg, 'plugin_name_issue', $plugin_main_file ); + } + } + + /** + * Check for possible owner issues. + * + * @since x.x.x + * + * @param Check_Result $result The check result. + * @param array $data The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function check_owner_issues( Check_Result $result, array $data, string $plugin_main_file ) { + if ( ! empty( $data['possible_owner_issues'] ) ) { + $msg = isset( $data['owner_explanation'] ) ? $data['owner_explanation'] : __( 'The plugin name has possible trademark or ownership issues.', 'plugin-check' ); + $this->add_result_warning_for_file( $result, $msg, 'plugin_name_trademark_issue', $plugin_main_file ); + } + } + + /** + * Check for similar plugins. + * + * @since x.x.x + * + * @param Check_Result $result The check result. + * @param array $parsed The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function check_similar_plugins( Check_Result $result, array $parsed, string $plugin_main_file ) { + if ( ! empty( $parsed['confusion_existing_plugins'] ) && is_array( $parsed['confusion_existing_plugins'] ) ) { + $plugins_list = array(); + foreach ( $parsed['confusion_existing_plugins'] as $plugin ) { + $plugins_list[] = sprintf( '%s (%s, %s active installs)', $plugin['name'], $plugin['similarity_level'], $plugin['active_installations'] ); + } + $this->add_result_warning_for_file( + $result, + sprintf( + /* translators: %s: List of similar plugins. */ + __( 'Plugin name is similar to existing plugins: %s', 'plugin-check' ), + implode( '; ', $plugins_list ) + ), + 'plugin_name_similarity', + $plugin_main_file + ); + } + } + + /** + * Gets the description for the check. + * + * @since x.x.x + * + * @return string Description. + */ + public function get_description(): string { + return __( 'Checks the plugin name for guideline compliance, trademark conflicts, and similarity with existing plugins using AI.', 'plugin-check' ); + } + + /** + * Gets the documentation URL for the check. + * + * @since x.x.x + * + * @return string The documentation URL. + */ + public function get_documentation_url(): string { + return __( 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/', 'plugin-check' ); + } +} diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php index d4d5c548d..00778a080 100644 --- a/includes/Checker/Default_Check_Repository.php +++ b/includes/Checker/Default_Check_Repository.php @@ -105,6 +105,7 @@ private function register_default_checks() { 'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(), 'wp_functions_compatibility' => new Checks\Plugin_Repo\WP_Functions_Compatibility_Check(), 'ai_provider' => new Checks\General\AI_Provider_Check(), + 'ai_name' => new Checks\Plugin_Repo\AI_Name_Check(), ) ); diff --git a/includes/Plugin_Main.php b/includes/Plugin_Main.php index 3aa0a4343..d211a1afd 100644 --- a/includes/Plugin_Main.php +++ b/includes/Plugin_Main.php @@ -77,10 +77,5 @@ public function add_hooks() { // Create the Settings page. $settings_page = new Settings_Page(); $settings_page->add_hooks(); - - // Create the Plugin Check Namer tool page. - $namer_page_class = '\\WordPress\\Plugin_Check\\Admin\\Namer_Page'; - $namer_page = new $namer_page_class(); - $namer_page->add_hooks(); } } diff --git a/templates/admin-page.php b/templates/admin-page.php index d8bfe935e..26183e659 100644 --- a/templates/admin-page.php +++ b/templates/admin-page.php @@ -85,6 +85,9 @@

+

+ +

diff --git a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php new file mode 100644 index 000000000..29be2abc0 --- /dev/null +++ b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php @@ -0,0 +1,32 @@ +createMock( AJAX_Runner::class ); + $runner->method( 'should_use_ai_name' )->willReturn( false ); + + $reflection = new \ReflectionClass( Plugin_Request_Utility::class ); + $property = $reflection->getProperty( 'runner' ); + $property->setAccessible( true ); + $property->setValue( null, $runner ); + + $check = new AI_Name_Check(); + $context = new Check_Context( WP_PLUGIN_CHECK_MAIN_FILE ); + $result = new Check_Result( $context ); + + $check->run( $result ); + + $this->assertEmpty( $result->get_errors() ); + $this->assertEmpty( $result->get_warnings() ); + + $property->setValue( null, null ); + } +}