PluginProbe
Plugin Check (PCP) / 1.8.0
Plugin Check (PCP) v1.8.0
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) 1.8.0, at includes/Checker/Abstract_Check_Runner.php

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