JFIF $ d d 7 * Z qn )ZP 0 !1 A "#a$2Qr D8 a Ri [f \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H ~ , l s / 9 ć: [=/#f ; ) x { ٛE Q )~ = 𘙲r*2~ a _V = ' ku m F D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq 1dđ #̩ ͑ 0 ; H ] u 搂 @:~ ו K L N s h } O I R* 8 :2 ! lD JV o ( 3 = M ( z Ȱ+i *N A r6K n S l ) ! J J Ӂ* % ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c P A\IL Nj! J ۱+ O\ ͔ ] ޒ S J ȧ c%A N ol ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0 'G=i 2N +0 e 2 ]] 9 VT P O 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P < 0 x < !1AQ "Raq 02Br #S CTb ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV YT D I 6 2 '8Y2 7'\T P.6 d &˦ @V q i |8 -O Ε ] ʔ U= T L 8= ;6c | !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U#6w + +s &r+n ڐ { @ 29gL u " T Ù M=6 (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 ! OS r D 5 7 % | ?x> L | / nD 6?/8 w # [ ) L 7 +6〼T A T g ! %5 Mm Z /c -{1_Je "| ^$ 'O & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@ u6 BԒ i YN * f 0 _ GhKW u lT <nj- 0{ M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q $ H8o Q˼EV ~ k 0 h' L Pp M -M" m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @~p # !1QAa q ? AL0L x LU 4 d ; x " 16ad0 7.\ߋ 0 = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X &Q З non]я u ג./ ; gZ$ M QР 2M ]h ni 0 P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _ 1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3 6 #Ik+ v { v i ( ވ ? a e) X xy/*@Q Ǽ Ծ ) $ IWO l c G; !i k{ gOŀ L ('x T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ) X 5 c QɎMy W ? Q *!R > 6=7)Xj 5`J ] e 8%t ! + ' ! 1Q 5 !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $ v , ,m 㵜5u bV =sY+ @d { N ! dn O < .- B; _w J t 6;Q Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш e黅X ۢ F :)[ F G X ƹ / w _c MeD ,ʡc c . W D tA $ j@ : ) - #u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P 6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L ! ~ y5 g9T̅J kbM' +s :S +B) v @ M j e C f j E 0 Y\Qn z G 1 д~ W o{T 9?` Rm yhsy3 !HAD ] m c1 ~ 2 L S u7x T ;j$ `} 4-> L# vzŏ I L S ֭ T { r jGK C ; b pU= - ` Bs K. S F w4 M q] Z dH S 0 ) tLg
<?php
/**
* Class WordPress\Plugin_Check\Checker\Abstract_Check_runner
*
* @package plugin-check
*/
namespace WordPress\Plugin_Check\Checker;
use Exception;
use WordPress\Plugin_Check\Admin\Settings_Page;
use WordPress\Plugin_Check\Checker\Exception\Invalid_Check_Slug_Exception;
use WordPress\Plugin_Check\Checker\Preparations\Universal_Runtime_Preparation;
use WordPress\Plugin_Check\Traits\AI_Analyzer;
use WordPress\Plugin_Check\Utilities\Plugin_Request_Utility;
/**
* Abstract Check Runner class.
*
* @since 1.0.0
*
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
*/
abstract class Abstract_Check_Runner implements Check_Runner {
use AI_Analyzer;
/**
* True if the class was initialized early in the WordPress load process.
*
* @since 1.0.0
* @var bool
*/
protected $initialized_early;
/**
* Whether to use AI analysis for false positive detection.
*
* @since 2.0.0
* @var bool
*/
protected $use_ai = false;
/**
* AI model preference for analysis.
*
* @since 2.0.0
* @var string
*/
protected $ai_model_preference = '';
/**
* The check slugs to run.
*
* @since 1.0.0
* @var array
*/
protected $check_slugs;
/**
* The plugin slug.
*
* @since 1.2.0
* @var string
*/
protected $slug;
/**
* The check slugs to exclude.
*
* @since 1.0.0
* @var array
*/
protected $check_exclude_slugs;
/**
* The plugin parameter.
*
* @since 1.0.0
* @var string
*/
protected $plugin;
/**
* An instance of the Checks class.
*
* @since 1.0.0
* @var Checks
*/
protected $checks;
/**
* The plugin basename to check.
*
* @since 1.0.0
* @var string
*/
protected $plugin_basename;
/**
* Whether to delete the plugin folder during cleanup.
*
* Used when downloading a plugin from a URL.
*
* @since 1.1.0
* @var bool
*/
private $delete_plugin_folder = false;
/**
* An instance of the Check_Repository.
*
* @since 1.0.0
* @var Check_Repository
*/
private $check_repository;
/**
* Runtime environment.
*
* @since 1.0.0
* @var Runtime_Environment_Setup
*/
protected $runtime_environment;
/**
* Whether to include experimental checks.
*
* @since 1.0.0
* @var bool
*/
protected $include_experimental;
/**
* Checks category for the filter.
*
* @since 1.0.0
* @var array
*/
protected $check_categories;
/**
* The mode to run checks in.
*
* @since 1.7.0
* @var string
*/
protected $mode;
/**
* Returns the plugin parameter based on the request.
*
* @since 1.0.0
*
* @return string The plugin parameter from the request.
*/
abstract protected function get_plugin_param();
/**
* Returns an array of Check slugs to run based on the request.
*
* @since 1.0.0
*
* @return array An array of Check slugs.
*/
abstract protected function get_check_slugs_param();
/**
* Returns an array of Check slugs to exclude based on the request.
*
* @since 1.0.0
*
* @return array An array of Check slugs.
*/
abstract protected function get_check_exclude_slugs_param();
/**
* Returns the include experimental parameter based on the request.
*
* @since 1.0.0
*
* @return bool Returns true to include experimental checks else false.
*/
abstract protected function get_include_experimental_param();
/**
* Returns an array of categories for filtering the checks.
*
* @since 1.0.0
*
* @return array An array of categories.
*/
abstract protected function get_categories_param();
/**
* Returns plugin slug parameter.
*
* @since 1.2.0
*
* @return string Plugin slug.
*/
abstract protected function get_slug_param();
/**
* Returns the mode parameter.
*
* @since 1.7.0
*
* @return string The mode parameter.
*/
abstract protected function get_mode_param();
/**
* Sets whether the runner class was initialized early.
*
* @since 1.0.0
*/
final public function __construct() {
$this->initialized_early = ! did_action( 'muplugins_loaded' );
$this->check_repository = new Default_Check_Repository();
$this->runtime_environment = new Runtime_Environment_Setup();
}
/**
* Sets the check slugs to be run.
*
* @since 1.0.0
*
* @param array $check_slugs An array of check slugs to be run.
*
* @throws Exception Thrown if the checks do not match those in the original request.
*/
final public function set_check_slugs( array $check_slugs ) {
if ( $this->initialized_early ) {
// Compare the check slugs to see if there was an error.
if ( $check_slugs !== $this->get_check_slugs_param() ) {
throw new Exception(
__( 'Invalid checks: The checks to run do not match the original request.', 'plugin-check' )
);
}
}
$this->check_slugs = $check_slugs;
}
/**
* Sets the check slugs to be excluded.
*
* @since 1.0.0
*
* @param array $check_slugs An array of check slugs to be excluded.
*
* @throws Exception Thrown if the checks do not match those in the original request.
*/
final public function set_check_exclude_slugs( array $check_slugs ) {
if ( $this->initialized_early ) {
// Compare the check slugs to see if there was an error.
if ( $check_slugs !== $this->get_check_exclude_slugs_param() ) {
throw new Exception(
__( 'Invalid checks: The checks to exclude do not match the original request.', 'plugin-check' )
);
}
}
$this->check_exclude_slugs = $check_slugs;
}
/**
* Sets the plugin slug or basename to be checked.
*
* @since 1.0.0
*
* @param string $plugin The plugin slug or basename to be checked.
*
* @throws Exception Thrown if the plugin set does not match the original request parameter.
*/
final public function set_plugin( $plugin ) {
if ( $this->initialized_early ) {
// Compare the plugin parameter to see if there was an error.
if ( $plugin !== $this->get_plugin_param() ) {
throw new Exception(
__( 'Invalid plugin: The plugin set does not match the original request parameter.', 'plugin-check' )
);
}
}
$this->plugin = $plugin;
}
/**
* Sets whether to include experimental checks in the process.
*
* @since 1.0.0
*
* @param bool $include_experimental True to include experimental checks. False to exclude.
*
* @throws Exception Thrown if the flag set does not match the original request parameter.
*/
final public function set_experimental_flag( $include_experimental ) {
if ( $this->initialized_early ) {
if ( $include_experimental !== $this->get_include_experimental_param() ) {
throw new Exception(
sprintf(
/* translators: %s: include-experimental */
__( 'Invalid flag: The %s value does not match the original request parameter.', 'plugin-check' ),
'include-experimental'
)
);
}
}
$this->include_experimental = $include_experimental;
}
/**
* Sets whether to use AI analysis for false positive detection.
*
* @since 2.0.0
*
* @param bool $use_ai True to enable AI analysis, false to disable.
*/
final public function set_use_ai( $use_ai ) {
$this->use_ai = (bool) $use_ai;
}
/**
* Sets the AI model preference for analysis.
*
* @since 2.0.0
*
* @param string $model_preference Model preference (e.g., 'openai::gpt-4o').
*/
final public function set_ai_model_preference( $model_preference ) {
$this->ai_model_preference = (string) $model_preference;
}
/**
* Determines if AI analysis should be used.
*
* @since 2.0.0
*
* @return bool True if AI analysis should be used, false otherwise.
*/
protected function should_use_ai() {
// Check if explicitly set via setter (e.g., CLI flag or checkbox).
return $this->use_ai;
}
/**
* Sets categories for filtering the checks.
*
* @since 1.0.0
*
* @param array $categories An array of categories for filtering.
*
* @throws Exception Thrown if the categories does not match the original request parameter.
*/
final public function set_categories( $categories ) {
if ( $this->initialized_early ) {
if ( $categories !== $this->get_categories_param() ) {
throw new Exception(
sprintf(
/* translators: %s: categories */
__( 'Invalid categories: The %s value does not match the original request parameter.', 'plugin-check' ),
'categories'
)
);
}
}
$this->check_categories = $categories;
}
/**
* Prepares the environment for running the requested checks.
*
* @since 1.0.0
*
* @return callable Cleanup function to revert any changes made here.
*
* @throws Exception Thrown exception when preparation fails.
*/
final public function prepare() {
if ( $this->initialized_early ) {
/*
* When initialized early, plugins are not loaded yet when this method is called.
* Therefore it could be that check slugs provided refer to addon checks that are not loaded yet.
* In that case, the only reliable option is to assume that it refers to an addon check and that the addon
* check is a runtime check. We don't know, but better to have the runtime preparations initialize
* unnecessarily rather than not having them when needed.
*
* The actual checks to run are retrieved later (once plugins are loaded), so if one of the provided slugs
* is actually invalid, the exception will still be thrown at that point.
*/
try {
$checks = $this->get_checks_to_run();
$initialize_runtime = $this->has_runtime_check( $checks );
} catch ( Invalid_Check_Slug_Exception $e ) {
$initialize_runtime = true;
}
} else {
// When not initialized early, all checks are loaded, so we can simply see if there are runtime checks.
$initialize_runtime = $this->has_runtime_check( $this->get_checks_to_run() );
}
$cleanup_functions = array();
if ( $initialize_runtime ) {
$cleanup_functions = $this->initialize_runtime();
}
if ( $this->delete_plugin_folder ) {
$cleanup_functions = function () {
// It must be a directory at this point, but double check just in case.
if ( is_dir( $this->plugin_basename ) ) {
rmdir( $this->plugin_basename );
}
};
}
return function () use ( $cleanup_functions ) {
foreach ( $cleanup_functions as $cleanup_function ) {
$cleanup_function();
}
};
}
/**
* Runs the checks against the plugin.
*
* @since 1.0.0
*
* @return Check_Result An object containing all check results.
*/
final public function run() {
$checks = $this->get_checks_to_run();
$preparations = $this->get_shared_preparations( $checks );
$cleanups = array();
// Prepare all shared preparations.
foreach ( $preparations as $preparation ) {
$instance = new $preparation['class']( ...$preparation['args'] );
$cleanups[] = $instance->prepare();
}
$results = $this->get_checks_instance()->run_checks( $this->get_check_context(), $checks, $this );
$ai_analysis = array();
$ai_stats = array();
// Run AI analysis if enabled.
if ( $this->should_use_ai() ) {
// Use CLI model preference, or fall back to saved settings.
$model_preference = $this->ai_model_preference;
if ( empty( $model_preference ) && class_exists( Settings_Page::class ) ) {
$model_preference = Settings_Page::get_model_preference();
}
$ai_result = $this->analyze_results_with_ai( $results, $this->get_check_context(), $model_preference );
if ( ! is_wp_error( $ai_result ) ) {
$ai_analysis = isset( $ai_result['analysis'] ) ? $ai_result['analysis'] : array();
$ai_stats = isset( $ai_result['stats'] ) ? $ai_result['stats'] : array();
}
}
$results->set_ai_analysis( $ai_analysis );
$results->set_ai_stats( $ai_stats );
if ( ! empty( $cleanups ) ) {
foreach ( $cleanups as $cleanup ) {
$cleanup();
}
}
return $results;
}
/**
* Determines if any of the checks are a runtime check.
*
* @since 1.0.0
*
* @param array $checks An array of check instances to run.
* @return bool Returns true if one or more checks is a runtime check.
*/
private function has_runtime_check( array $checks ) {
foreach ( $checks as $check ) {
if ( $check instanceof Runtime_Check ) {
return true;
}
}
return false;
}
/**
* Returns all shared preparations used by the checks to run.
*
* @since 1.0.0
*
* @param array $checks An array of Check instances to run.
* @return array An array of Preparations to run where each item is an array with keys `class` and `args`.
*/
private function get_shared_preparations( array $checks ) {
$shared_preparations = array();
foreach ( $checks as $check ) {
if ( ! $check instanceof With_Shared_Preparations ) {
continue;
}
$preparations = $check->get_shared_preparations();
foreach ( $preparations as $class => $args ) {
$key = $class . '::' . md5( json_encode( $args ) );
if ( ! isset( $shared_preparations[ $key ] ) ) {
$shared_preparations[ $key ] = array(
'class' => $class,
'args' => $args,
);
}
}
}
return array_values( $shared_preparations );
}
/**
* Returns the Check instances to run.
*
* @since 1.0.0
*
* @return array An array map of check slugs to Check instances.
*
* @throws Exception Thrown when invalid flag is passed, or Check slug does not exist.
*/
final public function get_checks_to_run() {
$check_slugs = $this->get_check_slugs();
$check_flags = Check_Repository::TYPE_STATIC;
// Check if conditions are met in order to perform Runtime Checks.
if ( $this->allow_runtime_checks() ) {
$check_flags = Check_Repository::TYPE_ALL;
}
// Check whether to include experimental checks.
if ( $this->get_include_experimental() ) {
$check_flags = $check_flags | Check_Repository::INCLUDE_EXPERIMENTAL;
}
$excluded_checks = $this->get_check_exclude_slugs();
$collection = $this->check_repository->get_checks( $check_flags )
->require( $check_slugs ) // Ensures all of the given slugs are valid.
->include( $check_slugs ) // Ensures only the checks with the given slugs are included.
->exclude( $excluded_checks ); // Exclude provided checks from list.
// Filters the checks by specific categories.
$categories = $this->get_categories();
if ( $categories ) {
$collection = Check_Categories::filter_checks_by_categories( $collection, $categories );
}
return $collection->to_map();
}
/**
* Initializes the runtime environment so that runtime checks can be run against a separate set of database tables.
*
* @since 1.3.0
*
* @return callable[] Array of cleanup functions to run after the process has completed.
*/
protected function initialize_runtime(): array {
$preparation = new Universal_Runtime_Preparation( $this->get_check_context() );
return array( $preparation->prepare() );
}
/**
* Checks whether the current environment allows for runtime checks to be used.
*
* @since 1.2.0
*
* @return bool True if runtime checks are allowed, false otherwise.
*/
protected function allow_runtime_checks(): bool {
// Ensure that is_plugin_active() is available.
require_once ABSPATH . 'wp-admin/includes/plugin.php';
return ( $this->initialized_early || $this->runtime_environment->can_set_up() )
&& is_plugin_active( $this->get_plugin_basename() );
}
/**
* Creates and returns the Check instance.
*
* @since 1.0.0
*
* @return Checks An instance of the Checks class.
*
* @throws Exception Thrown if the plugin slug is invalid.
*/
protected function get_checks_instance() {
if ( null !== $this->checks ) {
return $this->checks;
}
$this->checks = new Checks();
return $this->checks;
}
/**
* Returns the check slugs to run.
*
* @since 1.0.0
*
* @return array An array of check slugs to run.
*/
private function get_check_slugs() {
if ( null !== $this->check_slugs ) {
return $this->check_slugs;
}
return $this->get_check_slugs_param();
}
/**
* Returns the check slugs to exclude.
*
* @since 1.0.0
*
* @return array An array of check slugs to exclude.
*/
private function get_check_exclude_slugs() {
if ( null !== $this->check_exclude_slugs ) {
return $this->check_exclude_slugs;
}
return $this->get_check_exclude_slugs_param();
}
/**
* Returns the plugin basename.
*
* @since 1.0.0
*
* @return string The plugin basename to check.
*/
final public function get_plugin_basename() {
if ( null === $this->plugin_basename ) {
$plugin = null !== $this->plugin ? $this->plugin : $this->get_plugin_param();
if ( filter_var( $plugin, FILTER_VALIDATE_URL ) ) {
$this->plugin_basename = Plugin_Request_Utility::download_plugin( $plugin );
$this->delete_plugin_folder = true;
} elseif ( Plugin_Request_Utility::is_directory_valid_plugin( $plugin ) ) {
$this->plugin_basename = $plugin;
} else {
$this->plugin_basename = Plugin_Request_Utility::get_plugin_basename_from_input( $plugin );
}
}
return $this->plugin_basename;
}
/**
* Returns the value for the include experimental flag.
*
* @since 1.0.0
*
* @return bool True if experimental checks are included. False if not.
*/
final protected function get_include_experimental() {
if ( null !== $this->include_experimental ) {
return $this->include_experimental;
}
return $this->get_include_experimental_param();
}
/**
* Returns an array of categories for filtering the checks.
*
* @since 1.0.0
*
* @return array An array of categories.
*/
final protected function get_categories() {
if ( null !== $this->check_categories ) {
return $this->check_categories;
}
return $this->get_categories_param();
}
/**
* Returns plugin slug.
*
* @since 1.2.0
*
* @return string Plugin slug.
*/
final protected function get_slug() {
if ( null !== $this->slug ) {
return $this->slug;
}
return $this->get_slug_param();
}
/**
* Returns the mode to run checks in.
*
* @since 1.7.0
*
* @return string The check mode.
*/
final protected function get_mode() {
if ( null !== $this->mode ) {
return $this->mode;
}
return $this->get_mode_param();
}
/** Gets the Check_Context for the plugin.
*
* @since 1.0.0
*
* @return Check_Context The check context for the plugin file.
*/
private function get_check_context() {
$plugin_basename = $this->get_plugin_basename();
$plugin_path = is_dir( $plugin_basename ) ? $plugin_basename : WP_PLUGIN_DIR . '/' . $plugin_basename;
return new Check_Context( $plugin_path, $this->get_slug(), $this->get_mode() );
}
/**
* Sets the plugin slug.
*
* @since 1.2.0
*
* @param string $slug Plugin slug.
*/
final public function set_slug( $slug ) {
if ( ! empty( $slug ) ) {
$this->slug = $slug;
} else {
$basename = $this->get_plugin_basename();
$this->slug = ( '.' === pathinfo( $basename, PATHINFO_DIRNAME ) ) ? $basename : dirname( $basename );
}
}
/**
* Sets the runtime environment setup.
*
* @since 1.0.0
*
* @param Runtime_Environment_Setup $runtime_environment_setup Runtime environment instance.
*/
final public function set_runtime_environment_setup( $runtime_environment_setup ) {
$this->runtime_environment = $runtime_environment_setup;
}
/**
* Sets the mode to run checks in.
*
* @since 1.7.0
*
* @param string $mode The mode to run checks in.
*/
final public function set_mode( $mode ) {
if ( ! empty( $mode ) && in_array( $mode, array( 'new', 'update' ), true ) ) {
$this->mode = $mode;
} else {
$this->mode = 'new';
}
}
}