| 1 | <?php |
|---|
| 2 | /** |
|---|
| 3 | * Trait WordPress\Plugin_Check\Traits\AI_Check_Names |
|---|
| 4 | * |
|---|
| 5 | * @package plugin-check |
|---|
| 6 | */ |
|---|
| 7 | |
|---|
| 8 | namespace WordPress\Plugin_Check\Traits; |
|---|
| 9 | |
|---|
| 10 | use WP_Error; |
|---|
| 11 | |
|---|
| 12 | /** |
|---|
| 13 | * Trait for the Plugin Check Namer tool logic. |
|---|
| 14 | * |
|---|
| 15 | * @since 1.8.0 |
|---|
| 16 | */ |
|---|
| 17 | trait AI_Check_Names { |
|---|
| 18 | |
|---|
| 19 | /** |
|---|
| 20 | * Runs the name analysis via AI (makes two queries like internal scanner). |
|---|
| 21 | * |
|---|
| 22 | * @since 1.8.0 |
|---|
| 23 | * |
|---|
| 24 | * @param string $model_preference Model preference (optional). |
|---|
| 25 | * @param string $name Plugin name to evaluate. |
|---|
| 26 | * @param string $author Optional author/brand name. |
|---|
| 27 | * @return array|WP_Error Array with 'text' and 'token_usage' keys, or WP_Error. |
|---|
| 28 | */ |
|---|
| 29 | protected function run_name_analysis( $model_preference, $name, $author = '' ) { |
|---|
| 30 | // First query: Similar name search. |
|---|
| 31 | $similar_name_result = $this->run_similar_name_query( $model_preference, $name ); |
|---|
| 32 | if ( is_wp_error( $similar_name_result ) ) { |
|---|
| 33 | return $similar_name_result; |
|---|
| 34 | } |
|---|
| 35 | |
|---|
| 36 | // Build additional context from similar name results. |
|---|
| 37 | $additional_context = $this->build_similar_name_context( $similar_name_result['text'] ); |
|---|
| 38 | |
|---|
| 39 | // Second query: Pre-review with similar name results as context. |
|---|
| 40 | $prereview_result = $this->run_prereview_query( $model_preference, $name, $additional_context, $author ); |
|---|
| 41 | if ( is_wp_error( $prereview_result ) ) { |
|---|
| 42 | return $prereview_result; |
|---|
| 43 | } |
|---|
| 44 | |
|---|
| 45 | // Combine token usage from both queries. |
|---|
| 46 | $prereview_result['token_usage']['similar_name'] = $similar_name_result['token_usage']; |
|---|
| 47 | |
|---|
| 48 | return $prereview_result; |
|---|
| 49 | } |
|---|
| 50 | |
|---|
| 51 | /** |
|---|
| 52 | * Runs the similar name query (first query). |
|---|
| 53 | * |
|---|
| 54 | * @since 1.8.0 |
|---|
| 55 | * |
|---|
| 56 | * @param string $model_preference Model preference (optional). |
|---|
| 57 | * @param string $name Plugin name to evaluate. |
|---|
| 58 | * @return array|WP_Error Array with 'text' and 'token_usage' keys, or WP_Error. |
|---|
| 59 | */ |
|---|
| 60 | protected function run_similar_name_query( $model_preference, $name ) { |
|---|
| 61 | $prompt_template = $this->get_prompt_template( 'ai-check-similar-name.md' ); |
|---|
| 62 | if ( is_wp_error( $prompt_template ) ) { |
|---|
| 63 | return $prompt_template; |
|---|
| 64 | } |
|---|
| 65 | |
|---|
| 66 | $prompt = $prompt_template . "\n\nPlugin name: {$name}\nPlugin description: (not provided)\n"; |
|---|
| 67 | |
|---|
| 68 | // Execute AI request with structured output configuration. |
|---|
| 69 | return $this->execute_ai_request( |
|---|
| 70 | $prompt, |
|---|
| 71 | $model_preference, |
|---|
| 72 | function ( $builder ) { |
|---|
| 73 | $this->maybe_set_structured_output( $builder, 'similar_name' ); |
|---|
| 74 | } |
|---|
| 75 | ); |
|---|
| 76 | } |
|---|
| 77 | |
|---|
| 78 | /** |
|---|
| 79 | * Runs the pre-review query (second query). |
|---|
| 80 | * |
|---|
| 81 | * @since 1.8.0 |
|---|
| 82 | * |
|---|
| 83 | * @param string $model_preference Model preference (optional). |
|---|
| 84 | * @param string $name Plugin name to evaluate. |
|---|
| 85 | * @param string $additional_context Additional context from similar name query. |
|---|
| 86 | * @param string $author Optional author/brand name. |
|---|
| 87 | * @return array|WP_Error Array with 'text' and 'token_usage' keys, or WP_Error. |
|---|
| 88 | */ |
|---|
| 89 | protected function run_prereview_query( $model_preference, $name, $additional_context = '', $author = '' ) { |
|---|
| 90 | $prompt_template = $this->get_prompt_template( 'ai-check-prereview.md' ); |
|---|
| 91 | if ( is_wp_error( $prompt_template ) ) { |
|---|
| 92 | return $prompt_template; |
|---|
| 93 | } |
|---|
| 94 | |
|---|
| 95 | $output_template = $this->get_prompt_template( 'ai-check-prereview-output.md' ); |
|---|
| 96 | if ( is_wp_error( $output_template ) ) { |
|---|
| 97 | return $output_template; |
|---|
| 98 | } |
|---|
| 99 | |
|---|
| 100 | // Combine developer prompt (system instructions). |
|---|
| 101 | $developer_prompt = $prompt_template . "\n\n" . $output_template; |
|---|
| 102 | |
|---|
| 103 | // Build user prompt with plugin information. |
|---|
| 104 | $user_prompt = "# Plugin basic information\n\n"; |
|---|
| 105 | $user_prompt .= "- Display name for the plugin: {$name}\n"; |
|---|
| 106 | |
|---|
| 107 | // Add author/brand name if provided. |
|---|
| 108 | if ( ! empty( $author ) ) { |
|---|
| 109 | $user_prompt .= "- Author/Brand name: {$author}\n"; |
|---|
| 110 | $user_prompt .= "\nNote: The author/brand name provided indicates that the submitter owns or represents this brand. If the plugin name matches or is related to this brand, do not suggest changing the plugin name unless there are other significant conflicts.\n"; |
|---|
| 111 | } |
|---|
| 112 | |
|---|
| 113 | // Add additional context from similar name query if available. |
|---|
| 114 | if ( ! empty( $additional_context ) ) { |
|---|
| 115 | $user_prompt .= "\n\n" . $additional_context; |
|---|
| 116 | } |
|---|
| 117 | |
|---|
| 118 | // Combine both prompts for the AI call. |
|---|
| 119 | $full_prompt = $developer_prompt . "\n\n---\n\n" . $user_prompt; |
|---|
| 120 | |
|---|
| 121 | // Execute AI request with structured output configuration. |
|---|
| 122 | return $this->execute_ai_request( |
|---|
| 123 | $full_prompt, |
|---|
| 124 | $model_preference, |
|---|
| 125 | function ( $builder ) { |
|---|
| 126 | $this->maybe_set_structured_output( $builder, 'prereview' ); |
|---|
| 127 | } |
|---|
| 128 | ); |
|---|
| 129 | } |
|---|
| 130 | |
|---|
| 131 | /** |
|---|
| 132 | * Executes an AI request with the provided parameters. |
|---|
| 133 | * |
|---|
| 134 | * @since 1.9.0 |
|---|
| 135 | * |
|---|
| 136 | * @SuppressWarnings(PHPMD.NPathComplexity) |
|---|
| 137 | * |
|---|
| 138 | * @param string $prompt The prompt to send to the AI. |
|---|
| 139 | * @param string $model_preference Model preference (optional). |
|---|
| 140 | * @param callable|null $builder_config Optional callback to configure the prompt builder before execution. |
|---|
| 141 | * @return array|WP_Error Array with 'text' and optional 'token_usage', or WP_Error on failure. |
|---|
| 142 | */ |
|---|
| 143 | protected function execute_ai_request( $prompt, $model_preference = '', $builder_config = null ) { |
|---|
| 144 | if ( ! function_exists( 'wp_ai_client_prompt' ) ) { |
|---|
| 145 | return new WP_Error( |
|---|
| 146 | 'ai_client_not_available', |
|---|
| 147 | __( 'AI client is not available. This feature requires WordPress 7.0 or newer.', 'plugin-check' ) |
|---|
| 148 | ); |
|---|
| 149 | } |
|---|
| 150 | |
|---|
| 151 | $builder = wp_ai_client_prompt( $prompt ); |
|---|
| 152 | if ( is_wp_error( $builder ) ) { |
|---|
| 153 | return $builder; |
|---|
| 154 | } |
|---|
| 155 | |
|---|
| 156 | $builder = $this->apply_model_preference( $builder, $model_preference ); |
|---|
| 157 | if ( is_wp_error( $builder ) ) { |
|---|
| 158 | return $builder; |
|---|
| 159 | } |
|---|
| 160 | |
|---|
| 161 | if ( is_callable( $builder_config ) ) { |
|---|
| 162 | call_user_func( $builder_config, $builder ); |
|---|
| 163 | } |
|---|
| 164 | |
|---|
| 165 | // Try to generate a rich result first. |
|---|
| 166 | $result = null; |
|---|
| 167 | if ( method_exists( $builder, 'generate_text_result' ) ) { |
|---|
| 168 | $result = $builder->generate_text_result(); |
|---|
| 169 | } elseif ( method_exists( $builder, 'generateTextResult' ) ) { |
|---|
| 170 | $result = $builder->generateTextResult(); |
|---|
| 171 | } |
|---|
| 172 | |
|---|
| 173 | if ( $result ) { |
|---|
| 174 | if ( is_wp_error( $result ) ) { |
|---|
| 175 | return $result; |
|---|
| 176 | } |
|---|
| 177 | |
|---|
| 178 | $text = method_exists( $result, 'to_text' ) ? $result->to_text() : ( method_exists( $result, 'toText' ) ? $result->toText() : '' ); |
|---|
| 179 | $usage = $this->extract_token_usage( $result ); |
|---|
| 180 | |
|---|
| 181 | return array_filter( |
|---|
| 182 | array( |
|---|
| 183 | 'text' => $text, |
|---|
| 184 | 'token_usage' => $usage, |
|---|
| 185 | ) |
|---|
| 186 | ); |
|---|
| 187 | } |
|---|
| 188 | |
|---|
| 189 | $text = $builder->generate_text(); |
|---|
| 190 | if ( is_wp_error( $text ) ) { |
|---|
| 191 | return $text; |
|---|
| 192 | } |
|---|
| 193 | |
|---|
| 194 | return array( |
|---|
| 195 | 'text' => (string) $text, |
|---|
| 196 | ); |
|---|
| 197 | } |
|---|
| 198 | |
|---|
| 199 | /** |
|---|
| 200 | * Applies a model preference to the prompt builder if supported. |
|---|
| 201 | * |
|---|
| 202 | * @since 1.9.0 |
|---|
| 203 | * |
|---|
| 204 | * @param object $builder Prompt builder instance. |
|---|
| 205 | * @param string $model_preference Model preference. |
|---|
| 206 | * @return object|WP_Error Updated builder or WP_Error. |
|---|
| 207 | */ |
|---|
| 208 | protected function apply_model_preference( $builder, $model_preference ) { |
|---|
| 209 | if ( empty( $model_preference ) ) { |
|---|
| 210 | return $builder; |
|---|
| 211 | } |
|---|
| 212 | |
|---|
| 213 | $preference = $this->normalize_model_preference( $model_preference ); |
|---|
| 214 | |
|---|
| 215 | try { |
|---|
| 216 | $result = $builder->using_model_preference( $preference ); |
|---|
| 217 | return $result ? $result : $builder; |
|---|
| 218 | } catch ( \Exception $e ) { |
|---|
| 219 | // If method doesn't exist or fails, return WP_Error. |
|---|
| 220 | return new WP_Error( |
|---|
| 221 | 'model_preference_error', |
|---|
| 222 | sprintf( |
|---|
| 223 | /* translators: %s: Exception message */ |
|---|
| 224 | __( 'Failed to apply model preference: %s', 'plugin-check' ), |
|---|
| 225 | $e->getMessage() |
|---|
| 226 | ) |
|---|
| 227 | ); |
|---|
| 228 | } |
|---|
| 229 | } |
|---|
| 230 | |
|---|
| 231 | /** |
|---|
| 232 | * Normalizes a model preference string into a supported preference format. |
|---|
| 233 | * |
|---|
| 234 | * @since 1.9.0 |
|---|
| 235 | * |
|---|
| 236 | * @param string $model_preference Model preference string. |
|---|
| 237 | * @return string|array Normalized preference. |
|---|
| 238 | */ |
|---|
| 239 | protected function normalize_model_preference( $model_preference ) { |
|---|
| 240 | $trimmed = trim( (string) $model_preference ); |
|---|
| 241 | if ( '' === $trimmed ) { |
|---|
| 242 | return ''; |
|---|
| 243 | } |
|---|
| 244 | |
|---|
| 245 | foreach ( array( '::', '|', ':' ) as $separator ) { |
|---|
| 246 | if ( false !== strpos( $trimmed, $separator ) ) { |
|---|
| 247 | list( $provider, $model ) = array_map( 'trim', explode( $separator, $trimmed, 2 ) ); |
|---|
| 248 | if ( '' !== $provider && '' !== $model ) { |
|---|
| 249 | return array( $provider, $model ); |
|---|
| 250 | } |
|---|
| 251 | } |
|---|
| 252 | } |
|---|
| 253 | |
|---|
| 254 | return $trimmed; |
|---|
| 255 | } |
|---|
| 256 | |
|---|
| 257 | /** |
|---|
| 258 | * Extracts token usage from a result object, if available. |
|---|
| 259 | * |
|---|
| 260 | * @since 1.9.0 |
|---|
| 261 | * |
|---|
| 262 | * @SuppressWarnings(PHPMD.NPathComplexity) |
|---|
| 263 | * |
|---|
| 264 | * @param object $result Result object. |
|---|
| 265 | * @return array|null Token usage array or null. |
|---|
| 266 | */ |
|---|
| 267 | protected function extract_token_usage( $result ) { |
|---|
| 268 | $usage = null; |
|---|
| 269 | |
|---|
| 270 | if ( method_exists( $result, 'get_token_usage' ) ) { |
|---|
| 271 | $usage = $result->get_token_usage(); |
|---|
| 272 | } elseif ( method_exists( $result, 'getTokenUsage' ) ) { |
|---|
| 273 | $usage = $result->getTokenUsage(); |
|---|
| 274 | } |
|---|
| 275 | |
|---|
| 276 | if ( ! $usage || ! is_object( $usage ) ) { |
|---|
| 277 | return null; |
|---|
| 278 | } |
|---|
| 279 | |
|---|
| 280 | $prompt_tokens = method_exists( $usage, 'get_prompt_tokens' ) ? $usage->get_prompt_tokens() : ( method_exists( $usage, 'getPromptTokens' ) ? $usage->getPromptTokens() : null ); |
|---|
| 281 | $completion_tokens = method_exists( $usage, 'get_completion_tokens' ) ? $usage->get_completion_tokens() : ( method_exists( $usage, 'getCompletionTokens' ) ? $usage->getCompletionTokens() : null ); |
|---|
| 282 | $total_tokens = method_exists( $usage, 'get_total_tokens' ) ? $usage->get_total_tokens() : ( method_exists( $usage, 'getTotalTokens' ) ? $usage->getTotalTokens() : null ); |
|---|
| 283 | |
|---|
| 284 | if ( null === $prompt_tokens && null === $completion_tokens && null === $total_tokens ) { |
|---|
| 285 | return null; |
|---|
| 286 | } |
|---|
| 287 | |
|---|
| 288 | return array_filter( |
|---|
| 289 | array( |
|---|
| 290 | 'prompt_tokens' => $prompt_tokens, |
|---|
| 291 | 'completion_tokens' => $completion_tokens, |
|---|
| 292 | 'total_tokens' => $total_tokens, |
|---|
| 293 | ), |
|---|
| 294 | static function ( $value ) { |
|---|
| 295 | return null !== $value; |
|---|
| 296 | } |
|---|
| 297 | ); |
|---|
| 298 | } |
|---|
| 299 | |
|---|
| 300 | /** |
|---|
| 301 | * Builds additional context from similar name results. |
|---|
| 302 | * |
|---|
| 303 | * @since 1.8.0 |
|---|
| 304 | * |
|---|
| 305 | * @param string $similar_name_result Similar name query result. |
|---|
| 306 | * @return string Additional context text. |
|---|
| 307 | */ |
|---|
| 308 | protected function build_similar_name_context( $similar_name_result ) { |
|---|
| 309 | if ( empty( $similar_name_result ) ) { |
|---|
| 310 | return ''; |
|---|
| 311 | } |
|---|
| 312 | |
|---|
| 313 | $context = "# Possible similarity to other plugins, trademarks and project names.\n\n"; |
|---|
| 314 | $context .= "We've detected the following possible similarities. Check them and determine if there is a high similarity. This is not an exhaustive list. It is only the result of an internet search, so you need to check its validity for this case. Do not mention them in your reply.\n\n"; |
|---|
| 315 | $context .= $similar_name_result; |
|---|
| 316 | |
|---|
| 317 | return $context; |
|---|
| 318 | } |
|---|
| 319 | |
|---|
| 320 | /** |
|---|
| 321 | * Loads the AI prompt template. |
|---|
| 322 | * |
|---|
| 323 | * @since 1.8.0 |
|---|
| 324 | * |
|---|
| 325 | * @param string $filename Optional filename to load. Default 'ai-check-similar-name.md'. |
|---|
| 326 | * @return string|WP_Error Prompt template or error. |
|---|
| 327 | */ |
|---|
| 328 | protected function get_prompt_template( $filename = 'ai-check-similar-name.md' ) { |
|---|
| 329 | if ( ! defined( 'WP_PLUGIN_CHECK_PLUGIN_DIR_PATH' ) ) { |
|---|
| 330 | return new WP_Error( 'plugin_constant_not_defined', __( 'Plugin constant not defined.', 'plugin-check' ) ); |
|---|
| 331 | } |
|---|
| 332 | |
|---|
| 333 | $path = WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'prompts/' . $filename; |
|---|
| 334 | if ( ! file_exists( $path ) ) { |
|---|
| 335 | return new WP_Error( 'prompt_not_found', __( 'Prompt template not found.', 'plugin-check' ) ); |
|---|
| 336 | } |
|---|
| 337 | |
|---|
| 338 | $contents = (string) file_get_contents( $path ); |
|---|
| 339 | $contents = trim( $contents ); |
|---|
| 340 | |
|---|
| 341 | if ( empty( $contents ) ) { |
|---|
| 342 | return new WP_Error( 'prompt_empty', __( 'Prompt template is empty.', 'plugin-check' ) ); |
|---|
| 343 | } |
|---|
| 344 | |
|---|
| 345 | return $contents; |
|---|
| 346 | } |
|---|
| 347 | |
|---|
| 348 | /** |
|---|
| 349 | * Parses the analysis into a verdict and explanation. |
|---|
| 350 | * |
|---|
| 351 | * @since 1.8.0 |
|---|
| 352 | * |
|---|
| 353 | * @param array|string $analysis AI output (array with 'text' and 'token_usage', or string for backward compat). |
|---|
| 354 | * @return array |
|---|
| 355 | */ |
|---|
| 356 | protected function parse_analysis( $analysis ) { |
|---|
| 357 | // Extract text from array format (new format with token usage). |
|---|
| 358 | $analysis_text = is_array( $analysis ) && isset( $analysis['text'] ) ? $analysis['text'] : $analysis; |
|---|
| 359 | |
|---|
| 360 | if ( empty( $analysis_text ) ) { |
|---|
| 361 | return array( |
|---|
| 362 | 'verdict' => '❓ ' . __( 'Empty Response', 'plugin-check' ), |
|---|
| 363 | 'explanation' => __( 'The AI did not return any analysis. Please try again.', 'plugin-check' ), |
|---|
| 364 | ); |
|---|
| 365 | } |
|---|
| 366 | |
|---|
| 367 | $analysis_trim = trim( (string) $analysis_text ); |
|---|
| 368 | |
|---|
| 369 | // Try parsing as JSON first (structured output format). |
|---|
| 370 | $parsed_data = $this->parse_json_response( $analysis_trim ); |
|---|
| 371 | |
|---|
| 372 | // If JSON parsing failed, try markdown format. |
|---|
| 373 | if ( empty( $parsed_data ) || ! isset( $parsed_data['possible_naming_issues'] ) ) { |
|---|
| 374 | $parsed_data = $this->parse_markdown_format( $analysis_trim ); |
|---|
| 375 | } |
|---|
| 376 | |
|---|
| 377 | if ( ! empty( $parsed_data ) && isset( $parsed_data['possible_naming_issues'] ) ) { |
|---|
| 378 | $result = $this->parse_prereview_response( $parsed_data ); |
|---|
| 379 | |
|---|
| 380 | // Add token usage info if available. |
|---|
| 381 | if ( is_array( $analysis ) && isset( $analysis['token_usage'] ) ) { |
|---|
| 382 | $result['token_usage'] = $analysis['token_usage']; |
|---|
| 383 | } |
|---|
| 384 | |
|---|
| 385 | return $result; |
|---|
| 386 | } |
|---|
| 387 | |
|---|
| 388 | // Unable to parse format. |
|---|
| 389 | return array( |
|---|
| 390 | 'verdict' => '❓ ' . __( 'Unable to Parse', 'plugin-check' ), |
|---|
| 391 | 'explanation' => wp_kses_post( __( 'The AI response could not be parsed. Raw response:', 'plugin-check' ) . '<br><br>' . esc_html( substr( $analysis_trim, 0, 500 ) ) ), |
|---|
| 392 | 'raw' => $analysis_trim, |
|---|
| 393 | ); |
|---|
| 394 | } |
|---|
| 395 | |
|---|
| 396 | /** |
|---|
| 397 | * Parses JSON response from AI. |
|---|
| 398 | * |
|---|
| 399 | * @since 1.8.0 |
|---|
| 400 | * |
|---|
| 401 | * @param string $text AI response text. |
|---|
| 402 | * @return array Parsed data array or empty array if not valid JSON. |
|---|
| 403 | */ |
|---|
| 404 | protected function parse_json_response( $text ) { |
|---|
| 405 | if ( empty( $text ) ) { |
|---|
| 406 | return array(); |
|---|
| 407 | } |
|---|
| 408 | |
|---|
| 409 | $trimmed = trim( $text ); |
|---|
| 410 | |
|---|
| 411 | // Remove markdown code fences if present. |
|---|
| 412 | $trimmed = preg_replace( '/^```(?:json)?\s*\n?/m', '', $trimmed ); |
|---|
| 413 | $trimmed = preg_replace( '/\n?```\s*$/m', '', $trimmed ); |
|---|
| 414 | $trimmed = trim( $trimmed ); |
|---|
| 415 | |
|---|
| 416 | // Try to find JSON object boundaries. |
|---|
| 417 | $first_brace = strpos( $trimmed, '{' ); |
|---|
| 418 | if ( false !== $first_brace ) { |
|---|
| 419 | $last_brace = strrpos( $trimmed, '}' ); |
|---|
| 420 | if ( false !== $last_brace && $last_brace > $first_brace ) { |
|---|
| 421 | $json_text = substr( $trimmed, $first_brace, $last_brace - $first_brace + 1 ); |
|---|
| 422 | } else { |
|---|
| 423 | $json_text = $trimmed; |
|---|
| 424 | } |
|---|
| 425 | } else { |
|---|
| 426 | $json_text = $trimmed; |
|---|
| 427 | } |
|---|
| 428 | |
|---|
| 429 | // Try to decode as JSON. |
|---|
| 430 | $decoded = json_decode( $json_text, true ); |
|---|
| 431 | |
|---|
| 432 | if ( JSON_ERROR_NONE === json_last_error() && is_array( $decoded ) && isset( $decoded['possible_naming_issues'] ) ) { |
|---|
| 433 | return $decoded; |
|---|
| 434 | } |
|---|
| 435 | |
|---|
| 436 | return array(); |
|---|
| 437 | } |
|---|
| 438 | |
|---|
| 439 | /** |
|---|
| 440 | * Parses markdown/YAML-like format from AI response. |
|---|
| 441 | * |
|---|
| 442 | * Format: - key: value |
|---|
| 443 | * |
|---|
| 444 | * @since 1.8.0 |
|---|
| 445 | * |
|---|
| 446 | * @param string $text AI response text. |
|---|
| 447 | * @return array Parsed data array. |
|---|
| 448 | */ |
|---|
| 449 | protected function parse_markdown_format( $text ) { |
|---|
| 450 | $result = array(); |
|---|
| 451 | $lines = explode( "\n", $text ); |
|---|
| 452 | |
|---|
| 453 | foreach ( $lines as $line ) { |
|---|
| 454 | $parsed = $this->parse_markdown_line( trim( $line ) ); |
|---|
| 455 | if ( null !== $parsed ) { |
|---|
| 456 | $result[ $parsed['key'] ] = $parsed['value']; |
|---|
| 457 | } |
|---|
| 458 | } |
|---|
| 459 | |
|---|
| 460 | return $result; |
|---|
| 461 | } |
|---|
| 462 | |
|---|
| 463 | /** |
|---|
| 464 | * Parses a single markdown line into key-value pair. |
|---|
| 465 | * |
|---|
| 466 | * @since 1.8.0 |
|---|
| 467 | * |
|---|
| 468 | * @param string $line Line to parse. |
|---|
| 469 | * @return array|null Array with 'key' and 'value', or null if line should be skipped. |
|---|
| 470 | */ |
|---|
| 471 | protected function parse_markdown_line( $line ) { |
|---|
| 472 | if ( empty( $line ) ) { |
|---|
| 473 | return null; |
|---|
| 474 | } |
|---|
| 475 | |
|---|
| 476 | $line = ltrim( $line, '- ' ); |
|---|
| 477 | $colon_pos = strpos( $line, ':' ); |
|---|
| 478 | |
|---|
| 479 | if ( false === $colon_pos ) { |
|---|
| 480 | return null; |
|---|
| 481 | } |
|---|
| 482 | |
|---|
| 483 | $key = trim( substr( $line, 0, $colon_pos ) ); |
|---|
| 484 | $value = trim( substr( $line, $colon_pos + 1 ) ); |
|---|
| 485 | |
|---|
| 486 | if ( empty( $key ) ) { |
|---|
| 487 | return null; |
|---|
| 488 | } |
|---|
| 489 | |
|---|
| 490 | return array( |
|---|
| 491 | 'key' => $key, |
|---|
| 492 | 'value' => $this->parse_markdown_value( $key, $value ), |
|---|
| 493 | ); |
|---|
| 494 | } |
|---|
| 495 | |
|---|
| 496 | /** |
|---|
| 497 | * Parses markdown value based on format. |
|---|
| 498 | * |
|---|
| 499 | * @since 1.8.0 |
|---|
| 500 | * |
|---|
| 501 | * @param string $key Field key. |
|---|
| 502 | * @param string $value Field value. |
|---|
| 503 | * @return mixed Parsed value (string, bool, or array). |
|---|
| 504 | */ |
|---|
| 505 | protected function parse_markdown_value( $key, $value ) { |
|---|
| 506 | // Try JSON array. |
|---|
| 507 | if ( 0 === strpos( $value, '[' ) ) { |
|---|
| 508 | $decoded = json_decode( $value, true ); |
|---|
| 509 | if ( is_array( $decoded ) ) { |
|---|
| 510 | return $decoded; |
|---|
| 511 | } |
|---|
| 512 | } |
|---|
| 513 | |
|---|
| 514 | // Parse booleans. |
|---|
| 515 | $lower = strtolower( $value ); |
|---|
| 516 | if ( 'true' === $lower ) { |
|---|
| 517 | return true; |
|---|
| 518 | } |
|---|
| 519 | if ( 'false' === $lower ) { |
|---|
| 520 | return false; |
|---|
| 521 | } |
|---|
| 522 | |
|---|
| 523 | // Parse comma-separated for disallowed_type. |
|---|
| 524 | if ( 'disallowed_type' === $key && false !== strpos( $value, ',' ) ) { |
|---|
| 525 | return array_map( 'trim', explode( ',', $value ) ); |
|---|
| 526 | } |
|---|
| 527 | |
|---|
| 528 | return $value; |
|---|
| 529 | } |
|---|
| 530 | |
|---|
| 531 | /** |
|---|
| 532 | * Parses pre-review response format into user-friendly output. |
|---|
| 533 | * |
|---|
| 534 | * @since 1.8.0 |
|---|
| 535 | * |
|---|
| 536 | * @param array $decoded Decoded JSON response. |
|---|
| 537 | * @return array{verdict:string,explanation:string,processed_data:array} Parsed result. |
|---|
| 538 | */ |
|---|
| 539 | protected function parse_prereview_response( $decoded ) { |
|---|
| 540 | $verdict = $this->build_verdict( $decoded ); |
|---|
| 541 | $explanation_parts = $this->build_explanation_parts( $decoded ); |
|---|
| 542 | $explanation = ! empty( $explanation_parts ) ? implode( '<br><br>', $explanation_parts ) : __( 'No detailed analysis available.', 'plugin-check' ); |
|---|
| 543 | |
|---|
| 544 | return array( |
|---|
| 545 | 'verdict' => $verdict, |
|---|
| 546 | 'explanation' => wp_kses_post( $explanation ), |
|---|
| 547 | 'processed_data' => $decoded, |
|---|
| 548 | ); |
|---|
| 549 | } |
|---|
| 550 | |
|---|
| 551 | /** |
|---|
| 552 | * Builds verdict from decoded data. |
|---|
| 553 | * |
|---|
| 554 | * @since 1.8.0 |
|---|
| 555 | * |
|---|
| 556 | * @param array $decoded Decoded data. |
|---|
| 557 | * @return string Verdict string. |
|---|
| 558 | */ |
|---|
| 559 | protected function build_verdict( $decoded ) { |
|---|
| 560 | $issues = $this->collect_issues( $decoded ); |
|---|
| 561 | $is_disallowed = ! empty( $decoded['disallowed'] ); |
|---|
| 562 | |
|---|
| 563 | if ( $is_disallowed ) { |
|---|
| 564 | return '❌ ' . __( 'Disallowed', 'plugin-check' ); |
|---|
| 565 | } |
|---|
| 566 | |
|---|
| 567 | if ( ! empty( $issues ) ) { |
|---|
| 568 | return '⚠️ ' . __( 'Issues Found', 'plugin-check' ) . ': ' . implode( ', ', $issues ); |
|---|
| 569 | } |
|---|
| 570 | |
|---|
| 571 | // Check for suggestions, trademarks, or other indicators that suggest it's not clearly OK. |
|---|
| 572 | $has_suggestions = ! empty( $decoded['suggested_display_name'] ) || ! empty( $decoded['suggested_slug'] ); |
|---|
| 573 | $has_trademarks = ! empty( $decoded['trademarks_or_project_names_array'] ) && is_array( $decoded['trademarks_or_project_names_array'] ) && count( $decoded['trademarks_or_project_names_array'] ) > 0; |
|---|
| 574 | |
|---|
| 575 | if ( $has_suggestions || $has_trademarks ) { |
|---|
| 576 | return 'ℹ️ ' . __( 'Generally Allowable', 'plugin-check' ); |
|---|
| 577 | } |
|---|
| 578 | |
|---|
| 579 | return '✅ ' . __( 'No Issues Detected', 'plugin-check' ); |
|---|
| 580 | } |
|---|
| 581 | |
|---|
| 582 | /** |
|---|
| 583 | * Collects issues from decoded data. |
|---|
| 584 | * |
|---|
| 585 | * @since 1.8.0 |
|---|
| 586 | * |
|---|
| 587 | * @param array $decoded Decoded data. |
|---|
| 588 | * @return array List of issues. |
|---|
| 589 | */ |
|---|
| 590 | protected function collect_issues( $decoded ) { |
|---|
| 591 | $issues = array(); |
|---|
| 592 | |
|---|
| 593 | if ( ! empty( $decoded['possible_naming_issues'] ) ) { |
|---|
| 594 | $issues[] = __( 'Naming', 'plugin-check' ); |
|---|
| 595 | } |
|---|
| 596 | if ( ! empty( $decoded['possible_owner_issues'] ) ) { |
|---|
| 597 | $issues[] = __( 'Owner/Trademark', 'plugin-check' ); |
|---|
| 598 | } |
|---|
| 599 | if ( ! empty( $decoded['possible_description_issues'] ) ) { |
|---|
| 600 | $issues[] = __( 'Description', 'plugin-check' ); |
|---|
| 601 | } |
|---|
| 602 | |
|---|
| 603 | return $issues; |
|---|
| 604 | } |
|---|
| 605 | |
|---|
| 606 | /** |
|---|
| 607 | * Builds explanation parts from decoded data. |
|---|
| 608 | * |
|---|
| 609 | * @since 1.8.0 |
|---|
| 610 | * |
|---|
| 611 | * @param array $decoded Decoded data. |
|---|
| 612 | * @return array Explanation parts. |
|---|
| 613 | */ |
|---|
| 614 | protected function build_explanation_parts( $decoded ) { |
|---|
| 615 | $parts = array(); |
|---|
| 616 | |
|---|
| 617 | $this->add_disallowed_section( $parts, $decoded ); |
|---|
| 618 | $this->add_naming_section( $parts, $decoded ); |
|---|
| 619 | $this->add_owner_section( $parts, $decoded ); |
|---|
| 620 | $this->add_description_section( $parts, $decoded ); |
|---|
| 621 | $this->add_trademarks_section( $parts, $decoded ); |
|---|
| 622 | $this->add_suggestions_section( $parts, $decoded ); |
|---|
| 623 | $this->add_language_section( $parts, $decoded ); |
|---|
| 624 | |
|---|
| 625 | return $parts; |
|---|
| 626 | } |
|---|
| 627 | |
|---|
| 628 | /** |
|---|
| 629 | * Adds disallowed section to explanation parts. |
|---|
| 630 | * |
|---|
| 631 | * @since 1.8.0 |
|---|
| 632 | * |
|---|
| 633 | * @param array $parts Explanation parts array (passed by reference). |
|---|
| 634 | * @param array $decoded Decoded data. |
|---|
| 635 | * @return void |
|---|
| 636 | */ |
|---|
| 637 | protected function add_disallowed_section( &$parts, $decoded ) { |
|---|
| 638 | if ( empty( $decoded['disallowed'] ) ) { |
|---|
| 639 | return; |
|---|
| 640 | } |
|---|
| 641 | |
|---|
| 642 | $text = ''; |
|---|
| 643 | if ( ! empty( $decoded['disallowed_explanation'] ) ) { |
|---|
| 644 | $text .= $decoded['disallowed_explanation']; |
|---|
| 645 | } |
|---|
| 646 | if ( ! empty( $decoded['disallowed_type'] ) && is_array( $decoded['disallowed_type'] ) ) { |
|---|
| 647 | $text .= ' (' . implode( ', ', $decoded['disallowed_type'] ) . ')'; |
|---|
| 648 | } |
|---|
| 649 | if ( ! empty( $text ) ) { |
|---|
| 650 | $parts[] = '<strong>' . __( '🚫 Disallowed:', 'plugin-check' ) . '</strong> ' . $text; |
|---|
| 651 | } |
|---|
| 652 | } |
|---|
| 653 | |
|---|
| 654 | /** |
|---|
| 655 | * Adds naming section to explanation parts. |
|---|
| 656 | * |
|---|
| 657 | * @since 1.8.0 |
|---|
| 658 | * |
|---|
| 659 | * @param array $parts Explanation parts array (passed by reference). |
|---|
| 660 | * @param array $decoded Decoded data. |
|---|
| 661 | * @return void |
|---|
| 662 | */ |
|---|
| 663 | protected function add_naming_section( &$parts, $decoded ) { |
|---|
| 664 | if ( ! empty( $decoded['possible_naming_issues'] ) && ! empty( $decoded['naming_explanation'] ) ) { |
|---|
| 665 | $parts[] = '<strong>' . __( '📝 Naming:', 'plugin-check' ) . '</strong> ' . $decoded['naming_explanation']; |
|---|
| 666 | } |
|---|
| 667 | } |
|---|
| 668 | |
|---|
| 669 | /** |
|---|
| 670 | * Adds owner/trademark section to explanation parts. |
|---|
| 671 | * |
|---|
| 672 | * @since 1.8.0 |
|---|
| 673 | * |
|---|
| 674 | * @param array $parts Explanation parts array (passed by reference). |
|---|
| 675 | * @param array $decoded Decoded data. |
|---|
| 676 | * @return void |
|---|
| 677 | */ |
|---|
| 678 | protected function add_owner_section( &$parts, $decoded ) { |
|---|
| 679 | if ( ! empty( $decoded['possible_owner_issues'] ) && ! empty( $decoded['owner_explanation'] ) ) { |
|---|
| 680 | $parts[] = '<strong>' . __( '©️ Owner/Trademark:', 'plugin-check' ) . '</strong> ' . $decoded['owner_explanation']; |
|---|
| 681 | } |
|---|
| 682 | } |
|---|
| 683 | |
|---|
| 684 | /** |
|---|
| 685 | * Adds description section to explanation parts. |
|---|
| 686 | * |
|---|
| 687 | * @since 1.8.0 |
|---|
| 688 | * |
|---|
| 689 | * @param array $parts Explanation parts array (passed by reference). |
|---|
| 690 | * @param array $decoded Decoded data. |
|---|
| 691 | * @return void |
|---|
| 692 | */ |
|---|
| 693 | protected function add_description_section( &$parts, $decoded ) { |
|---|
| 694 | if ( ! empty( $decoded['possible_description_issues'] ) && ! empty( $decoded['description_explanation'] ) ) { |
|---|
| 695 | $parts[] = '<strong>' . __( '📄 Description:', 'plugin-check' ) . '</strong> ' . $decoded['description_explanation']; |
|---|
| 696 | } |
|---|
| 697 | } |
|---|
| 698 | |
|---|
| 699 | /** |
|---|
| 700 | * Adds trademarks section to explanation parts. |
|---|
| 701 | * |
|---|
| 702 | * @since 1.8.0 |
|---|
| 703 | * |
|---|
| 704 | * @param array $parts Explanation parts array (passed by reference). |
|---|
| 705 | * @param array $decoded Decoded data. |
|---|
| 706 | * @return void |
|---|
| 707 | */ |
|---|
| 708 | protected function add_trademarks_section( &$parts, $decoded ) { |
|---|
| 709 | if ( ! empty( $decoded['trademarks_or_project_names_array'] ) && is_array( $decoded['trademarks_or_project_names_array'] ) ) { |
|---|
| 710 | $trademarks = implode( ', ', array_map( 'esc_html', $decoded['trademarks_or_project_names_array'] ) ); |
|---|
| 711 | $parts[] = '<strong>' . __( '™️ Trademarks Detected:', 'plugin-check' ) . '</strong> ' . $trademarks; |
|---|
| 712 | } |
|---|
| 713 | } |
|---|
| 714 | |
|---|
| 715 | /** |
|---|
| 716 | * Adds suggestions section to explanation parts. |
|---|
| 717 | * |
|---|
| 718 | * @since 1.8.0 |
|---|
| 719 | * |
|---|
| 720 | * @param array $parts Explanation parts array (passed by reference). |
|---|
| 721 | * @param array $decoded Decoded data. |
|---|
| 722 | * @return void |
|---|
| 723 | */ |
|---|
| 724 | protected function add_suggestions_section( &$parts, $decoded ) { |
|---|
| 725 | $suggestions = array(); |
|---|
| 726 | |
|---|
| 727 | if ( ! empty( $decoded['suggested_display_name'] ) ) { |
|---|
| 728 | $suggestions[] = '<strong>' . __( 'Display Name:', 'plugin-check' ) . '</strong> ' . esc_html( $decoded['suggested_display_name'] ); |
|---|
| 729 | } |
|---|
| 730 | if ( ! empty( $decoded['suggested_slug'] ) ) { |
|---|
| 731 | $suggestions[] = '<strong>' . __( 'Slug:', 'plugin-check' ) . '</strong> ' . esc_html( $decoded['suggested_slug'] ); |
|---|
| 732 | } |
|---|
| 733 | if ( ! empty( $decoded['short_description'] ) ) { |
|---|
| 734 | $suggestions[] = '<strong>' . __( 'Description:', 'plugin-check' ) . '</strong> ' . esc_html( $decoded['short_description'] ); |
|---|
| 735 | } |
|---|
| 736 | if ( ! empty( $decoded['plugin_category'] ) ) { |
|---|
| 737 | $suggestions[] = '<strong>' . __( 'Category:', 'plugin-check' ) . '</strong> ' . esc_html( $decoded['plugin_category'] ); |
|---|
| 738 | } |
|---|
| 739 | |
|---|
| 740 | if ( ! empty( $suggestions ) ) { |
|---|
| 741 | $parts[] = '<br><strong>' . __( '💡 Suggestions:', 'plugin-check' ) . '</strong><br>' . implode( '<br>', $suggestions ); |
|---|
| 742 | } |
|---|
| 743 | } |
|---|
| 744 | |
|---|
| 745 | /** |
|---|
| 746 | * Adds language section to explanation parts. |
|---|
| 747 | * |
|---|
| 748 | * @since 1.8.0 |
|---|
| 749 | * |
|---|
| 750 | * @param array $parts Explanation parts array (passed by reference). |
|---|
| 751 | * @param array $decoded Decoded data. |
|---|
| 752 | * @return void |
|---|
| 753 | */ |
|---|
| 754 | protected function add_language_section( &$parts, $decoded ) { |
|---|
| 755 | if ( isset( $decoded['description_language_is_in_english'] ) && false === $decoded['description_language_is_in_english'] ) { |
|---|
| 756 | if ( ! empty( $decoded['description_what_is_not_in_english'] ) ) { |
|---|
| 757 | $parts[] = '<strong>' . __( '🌐 Language:', 'plugin-check' ) . '</strong> ' . $decoded['description_what_is_not_in_english']; |
|---|
| 758 | } |
|---|
| 759 | } |
|---|
| 760 | } |
|---|
| 761 | |
|---|
| 762 | /** |
|---|
| 763 | * Attempts to set structured output on the builder if supported. |
|---|
| 764 | * |
|---|
| 765 | * @since 1.8.0 |
|---|
| 766 | * |
|---|
| 767 | * @param object $builder The prompt builder instance. |
|---|
| 768 | * @param string $query_type Type of query: 'similar_name' or 'prereview'. |
|---|
| 769 | * @return void |
|---|
| 770 | */ |
|---|
| 771 | protected function maybe_set_structured_output( $builder, $query_type = 'similar_name' ) { |
|---|
| 772 | // Define the JSON schema based on query type. |
|---|
| 773 | if ( 'prereview' === $query_type ) { |
|---|
| 774 | $json_schema = $this->get_prereview_schema(); |
|---|
| 775 | } else { |
|---|
| 776 | $json_schema = $this->get_similar_name_schema(); |
|---|
| 777 | } |
|---|
| 778 | |
|---|
| 779 | // Try different method names that might be used for structured output. |
|---|
| 780 | $methods = array( |
|---|
| 781 | 'withStructuredOutput', |
|---|
| 782 | 'setResponseFormat', |
|---|
| 783 | 'usingResponseFormat', |
|---|
| 784 | 'withJsonSchema', |
|---|
| 785 | 'with_structured_output', |
|---|
| 786 | 'set_response_format', |
|---|
| 787 | 'using_response_format', |
|---|
| 788 | 'with_json_schema', |
|---|
| 789 | ); |
|---|
| 790 | |
|---|
| 791 | foreach ( $methods as $method ) { |
|---|
| 792 | if ( method_exists( $builder, $method ) ) { |
|---|
| 793 | call_user_func( array( $builder, $method ), $json_schema ); |
|---|
| 794 | break; |
|---|
| 795 | } |
|---|
| 796 | } |
|---|
| 797 | |
|---|
| 798 | // Try setting response format as a property if it exists. |
|---|
| 799 | // Note: Using reflection to set property as it may not be public. |
|---|
| 800 | // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase |
|---|
| 801 | if ( property_exists( $builder, 'responseFormat' ) || property_exists( $builder, 'response_format' ) ) { |
|---|
| 802 | $prop_name = property_exists( $builder, 'responseFormat' ) ? 'responseFormat' : 'response_format'; |
|---|
| 803 | try { |
|---|
| 804 | $reflection = new \ReflectionClass( $builder ); |
|---|
| 805 | $property = $reflection->getProperty( $prop_name ); |
|---|
| 806 | $property->setAccessible( true ); |
|---|
| 807 | $property->setValue( |
|---|
| 808 | $builder, |
|---|
| 809 | array( |
|---|
| 810 | 'type' => 'json_schema', |
|---|
| 811 | 'schema' => $json_schema, |
|---|
| 812 | ) |
|---|
| 813 | ); |
|---|
| 814 | } catch ( \Exception $e ) { |
|---|
| 815 | // If reflection fails, try direct assignment. |
|---|
| 816 | if ( property_exists( $builder, $prop_name ) ) { |
|---|
| 817 | $builder->$prop_name = array( |
|---|
| 818 | 'type' => 'json_schema', |
|---|
| 819 | 'schema' => $json_schema, |
|---|
| 820 | ); |
|---|
| 821 | } |
|---|
| 822 | } |
|---|
| 823 | } |
|---|
| 824 | // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase |
|---|
| 825 | } |
|---|
| 826 | |
|---|
| 827 | /** |
|---|
| 828 | * Gets the JSON schema for similar name query. |
|---|
| 829 | * |
|---|
| 830 | * @since 1.8.0 |
|---|
| 831 | * |
|---|
| 832 | * @return array JSON schema array. |
|---|
| 833 | */ |
|---|
| 834 | protected function get_similar_name_schema() { |
|---|
| 835 | return array( |
|---|
| 836 | 'type' => 'object', |
|---|
| 837 | 'properties' => array( |
|---|
| 838 | 'name_similarity_percentage' => array( 'type' => 'number' ), |
|---|
| 839 | 'similarity_explanation' => array( 'type' => 'string' ), |
|---|
| 840 | 'confusion_existing_plugins' => array( |
|---|
| 841 | 'type' => 'array', |
|---|
| 842 | 'items' => array( |
|---|
| 843 | 'type' => 'object', |
|---|
| 844 | 'properties' => array( |
|---|
| 845 | 'name' => array( 'type' => 'string' ), |
|---|
| 846 | 'similarity_level' => array( 'type' => 'string' ), |
|---|
| 847 | 'explanation' => array( 'type' => 'string' ), |
|---|
| 848 | 'active_installations' => array( 'type' => 'string' ), |
|---|
| 849 | 'link' => array( 'type' => 'string' ), |
|---|
| 850 | ), |
|---|
| 851 | 'required' => array( 'name', 'similarity_level', 'explanation', 'active_installations', 'link' ), |
|---|
| 852 | 'additionalProperties' => false, |
|---|
| 853 | ), |
|---|
| 854 | ), |
|---|
| 855 | 'confusion_existing_others' => array( |
|---|
| 856 | 'type' => 'array', |
|---|
| 857 | 'items' => array( |
|---|
| 858 | 'type' => 'object', |
|---|
| 859 | 'properties' => array( |
|---|
| 860 | 'name' => array( 'type' => 'string' ), |
|---|
| 861 | 'similarity_level' => array( 'type' => 'string' ), |
|---|
| 862 | 'explanation' => array( 'type' => 'string' ), |
|---|
| 863 | 'link' => array( 'type' => 'string' ), |
|---|
| 864 | ), |
|---|
| 865 | 'required' => array( 'name', 'similarity_level', 'explanation', 'link' ), |
|---|
| 866 | 'additionalProperties' => false, |
|---|
| 867 | ), |
|---|
| 868 | ), |
|---|
| 869 | ), |
|---|
| 870 | 'required' => array( |
|---|
| 871 | 'name_similarity_percentage', |
|---|
| 872 | 'similarity_explanation', |
|---|
| 873 | 'confusion_existing_plugins', |
|---|
| 874 | 'confusion_existing_others', |
|---|
| 875 | ), |
|---|
| 876 | 'additionalProperties' => false, |
|---|
| 877 | ); |
|---|
| 878 | } |
|---|
| 879 | |
|---|
| 880 | /** |
|---|
| 881 | * Gets the JSON schema for pre-review query. |
|---|
| 882 | * |
|---|
| 883 | * @since 1.8.0 |
|---|
| 884 | * |
|---|
| 885 | * @return array JSON schema array. |
|---|
| 886 | */ |
|---|
| 887 | protected function get_prereview_schema() { |
|---|
| 888 | return array( |
|---|
| 889 | 'type' => 'object', |
|---|
| 890 | 'properties' => array( |
|---|
| 891 | 'possible_naming_issues' => array( 'type' => 'boolean' ), |
|---|
| 892 | 'naming_explanation' => array( 'type' => 'string' ), |
|---|
| 893 | 'disallowed' => array( 'type' => 'boolean' ), |
|---|
| 894 | 'disallowed_explanation' => array( 'type' => 'string' ), |
|---|
| 895 | 'disallowed_type' => array( |
|---|
| 896 | 'type' => 'array', |
|---|
| 897 | 'items' => array( |
|---|
| 898 | 'type' => 'string', |
|---|
| 899 | ), |
|---|
| 900 | ), |
|---|
| 901 | 'trademarks_or_project_names_array' => array( |
|---|
| 902 | 'type' => 'array', |
|---|
| 903 | 'items' => array( |
|---|
| 904 | 'type' => 'string', |
|---|
| 905 | ), |
|---|
| 906 | ), |
|---|
| 907 | 'suggested_display_name' => array( 'type' => 'string' ), |
|---|
| 908 | 'suggested_slug' => array( 'type' => 'string' ), |
|---|
| 909 | ), |
|---|
| 910 | 'required' => array( |
|---|
| 911 | 'possible_naming_issues', |
|---|
| 912 | 'naming_explanation', |
|---|
| 913 | 'disallowed', |
|---|
| 914 | 'disallowed_explanation', |
|---|
| 915 | 'disallowed_type', |
|---|
| 916 | 'trademarks_or_project_names_array', |
|---|
| 917 | 'suggested_display_name', |
|---|
| 918 | 'suggested_slug', |
|---|
| 919 | 'short_description', |
|---|
| 920 | 'plugin_category', |
|---|
| 921 | ), |
|---|
| 922 | 'additionalProperties' => false, |
|---|
| 923 | ); |
|---|
| 924 | } |
|---|
| 925 | |
|---|
| 926 | /** |
|---|
| 927 | * Stores a transient result. |
|---|
| 928 | * |
|---|
| 929 | * @since 1.8.0 |
|---|
| 930 | * |
|---|
| 931 | * @param int $user_id User ID. |
|---|
| 932 | * @param array $data Result data. |
|---|
| 933 | */ |
|---|
| 934 | protected function store_result( $user_id, $data ) { |
|---|
| 935 | set_transient( $this->get_result_transient_key( $user_id ), $data, 10 * MINUTE_IN_SECONDS ); |
|---|
| 936 | } |
|---|
| 937 | |
|---|
| 938 | /** |
|---|
| 939 | * Gets the transient key. |
|---|
| 940 | * |
|---|
| 941 | * @since 1.8.0 |
|---|
| 942 | * |
|---|
| 943 | * @param int $user_id User ID. |
|---|
| 944 | * @return string |
|---|
| 945 | */ |
|---|
| 946 | protected function get_result_transient_key( $user_id ) { |
|---|
| 947 | return 'plugin_check_namer_result_' . (int) $user_id; |
|---|
| 948 | } |
|---|
| 949 | } |
|---|