PluginProbe
Plugin Check (PCP) / trunk
Plugin Check (PCP) vtrunk
2.1.0 trunk 0.1 0.2.0 0.2.1 0.2.2 0.2.3 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 1.9.0 2.0.0 ci-artifacts
plugin-check / includes / Checker / Abstract_Check_Runner.php

Abstract_Check_Runner.php in Plugin Check (PCP) trunk, at includes/Checker/Abstract_Check_Runner.php

782 lines 19.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class WordPress\Plugin_Check\Checker\Abstract_Check_runner
4 *
5 * @package plugin-check
6 */
7
8 namespace WordPress\Plugin_Check\Checker;
9
10 use Exception;
11 use WordPress\Plugin_Check\Admin\Settings_Page;
12 use WordPress\Plugin_Check\Checker\Exception\Invalid_Check_Slug_Exception;
13 use WordPress\Plugin_Check\Checker\Preparations\Universal_Runtime_Preparation;
14 use WordPress\Plugin_Check\Traits\AI_Analyzer;
15 use WordPress\Plugin_Check\Utilities\Plugin_Request_Utility;
16
17 /**
18 * Abstract Check Runner class.
19 *
20 * @since 1.0.0
21 *
22 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
23 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
24 */
25 abstract class Abstract_Check_Runner implements Check_Runner {
26
27 use AI_Analyzer;
28
29 /**
30 * True if the class was initialized early in the WordPress load process.
31 *
32 * @since 1.0.0
33 * @var bool
34 */
35 protected $initialized_early;
36
37 /**
38 * Whether to use AI analysis for false positive detection.
39 *
40 * @since 2.0.0
41 * @var bool
42 */
43 protected $use_ai = false;
44
45 /**
46 * AI model preference for analysis.
47 *
48 * @since 2.0.0
49 * @var string
50 */
51 protected $ai_model_preference = '';
52
53 /**
54 * The check slugs to run.
55 *
56 * @since 1.0.0
57 * @var array
58 */
59 protected $check_slugs;
60
61 /**
62 * The plugin slug.
63 *
64 * @since 1.2.0
65 * @var string
66 */
67 protected $slug;
68
69 /**
70 * The check slugs to exclude.
71 *
72 * @since 1.0.0
73 * @var array
74 */
75 protected $check_exclude_slugs;
76
77 /**
78 * The plugin parameter.
79 *
80 * @since 1.0.0
81 * @var string
82 */
83 protected $plugin;
84
85 /**
86 * An instance of the Checks class.
87 *
88 * @since 1.0.0
89 * @var Checks
90 */
91 protected $checks;
92
93 /**
94 * The plugin basename to check.
95 *
96 * @since 1.0.0
97 * @var string
98 */
99 protected $plugin_basename;
100
101 /**
102 * Whether to delete the plugin folder during cleanup.
103 *
104 * Used when downloading a plugin from a URL.
105 *
106 * @since 1.1.0
107 * @var bool
108 */
109 private $delete_plugin_folder = false;
110
111 /**
112 * An instance of the Check_Repository.
113 *
114 * @since 1.0.0
115 * @var Check_Repository
116 */
117 private $check_repository;
118
119 /**
120 * Runtime environment.
121 *
122 * @since 1.0.0
123 * @var Runtime_Environment_Setup
124 */
125 protected $runtime_environment;
126
127 /**
128 * Whether to include experimental checks.
129 *
130 * @since 1.0.0
131 * @var bool
132 */
133 protected $include_experimental;
134
135 /**
136 * Checks category for the filter.
137 *
138 * @since 1.0.0
139 * @var array
140 */
141 protected $check_categories;
142
143 /**
144 * The mode to run checks in.
145 *
146 * @since 1.7.0
147 * @var string
148 */
149 protected $mode;
150
151 /**
152 * Returns the plugin parameter based on the request.
153 *
154 * @since 1.0.0
155 *
156 * @return string The plugin parameter from the request.
157 */
158 abstract protected function get_plugin_param();
159
160 /**
161 * Returns an array of Check slugs to run based on the request.
162 *
163 * @since 1.0.0
164 *
165 * @return array An array of Check slugs.
166 */
167 abstract protected function get_check_slugs_param();
168
169 /**
170 * Returns an array of Check slugs to exclude based on the request.
171 *
172 * @since 1.0.0
173 *
174 * @return array An array of Check slugs.
175 */
176 abstract protected function get_check_exclude_slugs_param();
177
178 /**
179 * Returns the include experimental parameter based on the request.
180 *
181 * @since 1.0.0
182 *
183 * @return bool Returns true to include experimental checks else false.
184 */
185 abstract protected function get_include_experimental_param();
186
187 /**
188 * Returns an array of categories for filtering the checks.
189 *
190 * @since 1.0.0
191 *
192 * @return array An array of categories.
193 */
194 abstract protected function get_categories_param();
195
196 /**
197 * Returns plugin slug parameter.
198 *
199 * @since 1.2.0
200 *
201 * @return string Plugin slug.
202 */
203 abstract protected function get_slug_param();
204
205 /**
206 * Returns the mode parameter.
207 *
208 * @since 1.7.0
209 *
210 * @return string The mode parameter.
211 */
212 abstract protected function get_mode_param();
213
214 /**
215 * Sets whether the runner class was initialized early.
216 *
217 * @since 1.0.0
218 */
219 final public function __construct() {
220 $this->initialized_early = ! did_action( 'muplugins_loaded' );
221 $this->check_repository = new Default_Check_Repository();
222 $this->runtime_environment = new Runtime_Environment_Setup();
223 }
224
225 /**
226 * Sets the check slugs to be run.
227 *
228 * @since 1.0.0
229 *
230 * @param array $check_slugs An array of check slugs to be run.
231 *
232 * @throws Exception Thrown if the checks do not match those in the original request.
233 */
234 final public function set_check_slugs( array $check_slugs ) {
235 if ( $this->initialized_early ) {
236 // Compare the check slugs to see if there was an error.
237 if ( $check_slugs !== $this->get_check_slugs_param() ) {
238 throw new Exception(
239 __( 'Invalid checks: The checks to run do not match the original request.', 'plugin-check' )
240 );
241 }
242 }
243
244 $this->check_slugs = $check_slugs;
245 }
246
247 /**
248 * Sets the check slugs to be excluded.
249 *
250 * @since 1.0.0
251 *
252 * @param array $check_slugs An array of check slugs to be excluded.
253 *
254 * @throws Exception Thrown if the checks do not match those in the original request.
255 */
256 final public function set_check_exclude_slugs( array $check_slugs ) {
257 if ( $this->initialized_early ) {
258 // Compare the check slugs to see if there was an error.
259 if ( $check_slugs !== $this->get_check_exclude_slugs_param() ) {
260 throw new Exception(
261 __( 'Invalid checks: The checks to exclude do not match the original request.', 'plugin-check' )
262 );
263 }
264 }
265
266 $this->check_exclude_slugs = $check_slugs;
267 }
268
269 /**
270 * Sets the plugin slug or basename to be checked.
271 *
272 * @since 1.0.0
273 *
274 * @param string $plugin The plugin slug or basename to be checked.
275 *
276 * @throws Exception Thrown if the plugin set does not match the original request parameter.
277 */
278 final public function set_plugin( $plugin ) {
279 if ( $this->initialized_early ) {
280 // Compare the plugin parameter to see if there was an error.
281 if ( $plugin !== $this->get_plugin_param() ) {
282 throw new Exception(
283 __( 'Invalid plugin: The plugin set does not match the original request parameter.', 'plugin-check' )
284 );
285 }
286 }
287
288 $this->plugin = $plugin;
289 }
290
291 /**
292 * Sets whether to include experimental checks in the process.
293 *
294 * @since 1.0.0
295 *
296 * @param bool $include_experimental True to include experimental checks. False to exclude.
297 *
298 * @throws Exception Thrown if the flag set does not match the original request parameter.
299 */
300 final public function set_experimental_flag( $include_experimental ) {
301 if ( $this->initialized_early ) {
302 if ( $include_experimental !== $this->get_include_experimental_param() ) {
303 throw new Exception(
304 sprintf(
305 /* translators: %s: include-experimental */
306 __( 'Invalid flag: The %s value does not match the original request parameter.', 'plugin-check' ),
307 'include-experimental'
308 )
309 );
310 }
311 }
312
313 $this->include_experimental = $include_experimental;
314 }
315
316 /**
317 * Sets whether to use AI analysis for false positive detection.
318 *
319 * @since 2.0.0
320 *
321 * @param bool $use_ai True to enable AI analysis, false to disable.
322 */
323 final public function set_use_ai( $use_ai ) {
324 $this->use_ai = (bool) $use_ai;
325 }
326
327 /**
328 * Sets the AI model preference for analysis.
329 *
330 * @since 2.0.0
331 *
332 * @param string $model_preference Model preference (e.g., 'openai::gpt-4o').
333 */
334 final public function set_ai_model_preference( $model_preference ) {
335 $this->ai_model_preference = (string) $model_preference;
336 }
337
338 /**
339 * Determines if AI analysis should be used.
340 *
341 * @since 2.0.0
342 *
343 * @return bool True if AI analysis should be used, false otherwise.
344 */
345 protected function should_use_ai() {
346 // Check if explicitly set via setter (e.g., CLI flag or checkbox).
347 return $this->use_ai;
348 }
349
350 /**
351 * Sets categories for filtering the checks.
352 *
353 * @since 1.0.0
354 *
355 * @param array $categories An array of categories for filtering.
356 *
357 * @throws Exception Thrown if the categories does not match the original request parameter.
358 */
359 final public function set_categories( $categories ) {
360 if ( $this->initialized_early ) {
361 if ( $categories !== $this->get_categories_param() ) {
362 throw new Exception(
363 sprintf(
364 /* translators: %s: categories */
365 __( 'Invalid categories: The %s value does not match the original request parameter.', 'plugin-check' ),
366 'categories'
367 )
368 );
369 }
370 }
371 $this->check_categories = $categories;
372 }
373
374 /**
375 * Prepares the environment for running the requested checks.
376 *
377 * @since 1.0.0
378 *
379 * @return callable Cleanup function to revert any changes made here.
380 *
381 * @throws Exception Thrown exception when preparation fails.
382 */
383 final public function prepare() {
384 if ( $this->initialized_early ) {
385 /*
386 * When initialized early, plugins are not loaded yet when this method is called.
387 * Therefore it could be that check slugs provided refer to addon checks that are not loaded yet.
388 * In that case, the only reliable option is to assume that it refers to an addon check and that the addon
389 * check is a runtime check. We don't know, but better to have the runtime preparations initialize
390 * unnecessarily rather than not having them when needed.
391 *
392 * The actual checks to run are retrieved later (once plugins are loaded), so if one of the provided slugs
393 * is actually invalid, the exception will still be thrown at that point.
394 */
395 try {
396 $checks = $this->get_checks_to_run();
397 $initialize_runtime = $this->has_runtime_check( $checks );
398 } catch ( Invalid_Check_Slug_Exception $e ) {
399 $initialize_runtime = true;
400 }
401 } else {
402 // When not initialized early, all checks are loaded, so we can simply see if there are runtime checks.
403 $initialize_runtime = $this->has_runtime_check( $this->get_checks_to_run() );
404 }
405
406 $cleanup_functions = array();
407 if ( $initialize_runtime ) {
408 $cleanup_functions = $this->initialize_runtime();
409 }
410
411 if ( $this->delete_plugin_folder ) {
412 $cleanup_functions = function () {
413 // It must be a directory at this point, but double check just in case.
414 if ( is_dir( $this->plugin_basename ) ) {
415 rmdir( $this->plugin_basename );
416 }
417 };
418 }
419
420 return function () use ( $cleanup_functions ) {
421 foreach ( $cleanup_functions as $cleanup_function ) {
422 $cleanup_function();
423 }
424 };
425 }
426
427 /**
428 * Runs the checks against the plugin.
429 *
430 * @since 1.0.0
431 *
432 * @return Check_Result An object containing all check results.
433 */
434 final public function run() {
435 $checks = $this->get_checks_to_run();
436 $preparations = $this->get_shared_preparations( $checks );
437 $cleanups = array();
438
439 // Prepare all shared preparations.
440 foreach ( $preparations as $preparation ) {
441 $instance = new $preparation['class']( ...$preparation['args'] );
442 $cleanups[] = $instance->prepare();
443 }
444
445 $results = $this->get_checks_instance()->run_checks( $this->get_check_context(), $checks, $this );
446
447 $ai_analysis = array();
448 $ai_stats = array();
449
450 // Run AI analysis if enabled.
451 if ( $this->should_use_ai() ) {
452 // Use CLI model preference, or fall back to saved settings.
453 $model_preference = $this->ai_model_preference;
454 if ( empty( $model_preference ) && class_exists( Settings_Page::class ) ) {
455 $model_preference = Settings_Page::get_model_preference();
456 }
457 $ai_result = $this->analyze_results_with_ai( $results, $this->get_check_context(), $model_preference );
458 if ( ! is_wp_error( $ai_result ) ) {
459 $ai_analysis = isset( $ai_result['analysis'] ) ? $ai_result['analysis'] : array();
460 $ai_stats = isset( $ai_result['stats'] ) ? $ai_result['stats'] : array();
461 }
462 }
463
464 $results->set_ai_analysis( $ai_analysis );
465 $results->set_ai_stats( $ai_stats );
466
467 if ( ! empty( $cleanups ) ) {
468 foreach ( $cleanups as $cleanup ) {
469 $cleanup();
470 }
471 }
472
473 return $results;
474 }
475
476 /**
477 * Determines if any of the checks are a runtime check.
478 *
479 * @since 1.0.0
480 *
481 * @param array $checks An array of check instances to run.
482 * @return bool Returns true if one or more checks is a runtime check.
483 */
484 private function has_runtime_check( array $checks ) {
485 foreach ( $checks as $check ) {
486 if ( $check instanceof Runtime_Check ) {
487 return true;
488 }
489 }
490
491 return false;
492 }
493
494 /**
495 * Returns all shared preparations used by the checks to run.
496 *
497 * @since 1.0.0
498 *
499 * @param array $checks An array of Check instances to run.
500 * @return array An array of Preparations to run where each item is an array with keys `class` and `args`.
501 */
502 private function get_shared_preparations( array $checks ) {
503 $shared_preparations = array();
504
505 foreach ( $checks as $check ) {
506 if ( ! $check instanceof With_Shared_Preparations ) {
507 continue;
508 }
509
510 $preparations = $check->get_shared_preparations();
511
512 foreach ( $preparations as $class => $args ) {
513 $key = $class . '::' . md5( json_encode( $args ) );
514
515 if ( ! isset( $shared_preparations[ $key ] ) ) {
516 $shared_preparations[ $key ] = array(
517 'class' => $class,
518 'args' => $args,
519 );
520 }
521 }
522 }
523
524 return array_values( $shared_preparations );
525 }
526
527 /**
528 * Returns the Check instances to run.
529 *
530 * @since 1.0.0
531 *
532 * @return array An array map of check slugs to Check instances.
533 *
534 * @throws Exception Thrown when invalid flag is passed, or Check slug does not exist.
535 */
536 final public function get_checks_to_run() {
537 $check_slugs = $this->get_check_slugs();
538 $check_flags = Check_Repository::TYPE_STATIC;
539
540 // Check if conditions are met in order to perform Runtime Checks.
541 if ( $this->allow_runtime_checks() ) {
542 $check_flags = Check_Repository::TYPE_ALL;
543 }
544
545 // Check whether to include experimental checks.
546 if ( $this->get_include_experimental() ) {
547 $check_flags = $check_flags | Check_Repository::INCLUDE_EXPERIMENTAL;
548 }
549
550 $excluded_checks = $this->get_check_exclude_slugs();
551
552 $collection = $this->check_repository->get_checks( $check_flags )
553 ->require( $check_slugs ) // Ensures all of the given slugs are valid.
554 ->include( $check_slugs ) // Ensures only the checks with the given slugs are included.
555 ->exclude( $excluded_checks ); // Exclude provided checks from list.
556
557 // Filters the checks by specific categories.
558 $categories = $this->get_categories();
559 if ( $categories ) {
560 $collection = Check_Categories::filter_checks_by_categories( $collection, $categories );
561 }
562
563 return $collection->to_map();
564 }
565
566 /**
567 * Initializes the runtime environment so that runtime checks can be run against a separate set of database tables.
568 *
569 * @since 1.3.0
570 *
571 * @return callable[] Array of cleanup functions to run after the process has completed.
572 */
573 protected function initialize_runtime(): array {
574 $preparation = new Universal_Runtime_Preparation( $this->get_check_context() );
575 return array( $preparation->prepare() );
576 }
577
578 /**
579 * Checks whether the current environment allows for runtime checks to be used.
580 *
581 * @since 1.2.0
582 *
583 * @return bool True if runtime checks are allowed, false otherwise.
584 */
585 protected function allow_runtime_checks(): bool {
586 // Ensure that is_plugin_active() is available.
587 require_once ABSPATH . 'wp-admin/includes/plugin.php';
588
589 return ( $this->initialized_early || $this->runtime_environment->can_set_up() )
590 && is_plugin_active( $this->get_plugin_basename() );
591 }
592
593 /**
594 * Creates and returns the Check instance.
595 *
596 * @since 1.0.0
597 *
598 * @return Checks An instance of the Checks class.
599 *
600 * @throws Exception Thrown if the plugin slug is invalid.
601 */
602 protected function get_checks_instance() {
603 if ( null !== $this->checks ) {
604 return $this->checks;
605 }
606
607 $this->checks = new Checks();
608
609 return $this->checks;
610 }
611
612 /**
613 * Returns the check slugs to run.
614 *
615 * @since 1.0.0
616 *
617 * @return array An array of check slugs to run.
618 */
619 private function get_check_slugs() {
620 if ( null !== $this->check_slugs ) {
621 return $this->check_slugs;
622 }
623
624 return $this->get_check_slugs_param();
625 }
626
627 /**
628 * Returns the check slugs to exclude.
629 *
630 * @since 1.0.0
631 *
632 * @return array An array of check slugs to exclude.
633 */
634 private function get_check_exclude_slugs() {
635 if ( null !== $this->check_exclude_slugs ) {
636 return $this->check_exclude_slugs;
637 }
638
639 return $this->get_check_exclude_slugs_param();
640 }
641
642 /**
643 * Returns the plugin basename.
644 *
645 * @since 1.0.0
646 *
647 * @return string The plugin basename to check.
648 */
649 final public function get_plugin_basename() {
650 if ( null === $this->plugin_basename ) {
651 $plugin = null !== $this->plugin ? $this->plugin : $this->get_plugin_param();
652
653 if ( filter_var( $plugin, FILTER_VALIDATE_URL ) ) {
654 $this->plugin_basename = Plugin_Request_Utility::download_plugin( $plugin );
655
656 $this->delete_plugin_folder = true;
657 } elseif ( Plugin_Request_Utility::is_directory_valid_plugin( $plugin ) ) {
658 $this->plugin_basename = $plugin;
659 } else {
660 $this->plugin_basename = Plugin_Request_Utility::get_plugin_basename_from_input( $plugin );
661 }
662 }
663
664 return $this->plugin_basename;
665 }
666
667 /**
668 * Returns the value for the include experimental flag.
669 *
670 * @since 1.0.0
671 *
672 * @return bool True if experimental checks are included. False if not.
673 */
674 final protected function get_include_experimental() {
675 if ( null !== $this->include_experimental ) {
676 return $this->include_experimental;
677 }
678
679 return $this->get_include_experimental_param();
680 }
681
682 /**
683 * Returns an array of categories for filtering the checks.
684 *
685 * @since 1.0.0
686 *
687 * @return array An array of categories.
688 */
689 final protected function get_categories() {
690 if ( null !== $this->check_categories ) {
691 return $this->check_categories;
692 }
693
694 return $this->get_categories_param();
695 }
696
697 /**
698 * Returns plugin slug.
699 *
700 * @since 1.2.0
701 *
702 * @return string Plugin slug.
703 */
704 final protected function get_slug() {
705 if ( null !== $this->slug ) {
706 return $this->slug;
707 }
708
709 return $this->get_slug_param();
710 }
711
712 /**
713 * Returns the mode to run checks in.
714 *
715 * @since 1.7.0
716 *
717 * @return string The check mode.
718 */
719 final protected function get_mode() {
720 if ( null !== $this->mode ) {
721 return $this->mode;
722 }
723
724 return $this->get_mode_param();
725 }
726
727 /** Gets the Check_Context for the plugin.
728 *
729 * @since 1.0.0
730 *
731 * @return Check_Context The check context for the plugin file.
732 */
733 private function get_check_context() {
734 $plugin_basename = $this->get_plugin_basename();
735 $plugin_path = is_dir( $plugin_basename ) ? $plugin_basename : WP_PLUGIN_DIR . '/' . $plugin_basename;
736 return new Check_Context( $plugin_path, $this->get_slug(), $this->get_mode() );
737 }
738
739 /**
740 * Sets the plugin slug.
741 *
742 * @since 1.2.0
743 *
744 * @param string $slug Plugin slug.
745 */
746 final public function set_slug( $slug ) {
747 if ( ! empty( $slug ) ) {
748 $this->slug = $slug;
749 } else {
750 $basename = $this->get_plugin_basename();
751
752 $this->slug = ( '.' === pathinfo( $basename, PATHINFO_DIRNAME ) ) ? $basename : dirname( $basename );
753 }
754 }
755
756 /**
757 * Sets the runtime environment setup.
758 *
759 * @since 1.0.0
760 *
761 * @param Runtime_Environment_Setup $runtime_environment_setup Runtime environment instance.
762 */
763 final public function set_runtime_environment_setup( $runtime_environment_setup ) {
764 $this->runtime_environment = $runtime_environment_setup;
765 }
766
767 /**
768 * Sets the mode to run checks in.
769 *
770 * @since 1.7.0
771 *
772 * @param string $mode The mode to run checks in.
773 */
774 final public function set_mode( $mode ) {
775 if ( ! empty( $mode ) && in_array( $mode, array( 'new', 'update' ), true ) ) {
776 $this->mode = $mode;
777 } else {
778 $this->mode = 'new';
779 }
780 }
781 }
782