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

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