├── .distignore ├── LICENSE ├── assets ├── css │ └── plugin-check-admin.css └── js │ └── plugin-check-admin.js ├── behat.yml ├── cli.php ├── codecov.yml ├── composer.json ├── drop-ins └── object-cache.copy.php ├── includes ├── Admin │ ├── Admin_AJAX.php │ └── Admin_Page.php ├── CLI │ └── Plugin_Check_Command.php ├── Checker │ ├── AJAX_Runner.php │ ├── Abstract_Check_Runner.php │ ├── CLI_Runner.php │ ├── Check.php │ ├── Check_Categories.php │ ├── Check_Collection.php │ ├── Check_Context.php │ ├── Check_Repository.php │ ├── Check_Result.php │ ├── Check_Runner.php │ ├── Checks.php │ ├── Checks │ │ ├── Abstract_File_Check.php │ │ ├── Abstract_PHP_CodeSniffer_Check.php │ │ ├── Abstract_Runtime_Check.php │ │ ├── General │ │ │ └── I18n_Usage_Check.php │ │ ├── Performance │ │ │ ├── Enqueued_Resources_Check.php │ │ │ ├── Enqueued_Scripts_In_Footer_Check.php │ │ │ ├── Enqueued_Scripts_Scope_Check.php │ │ │ ├── Enqueued_Scripts_Size_Check.php │ │ │ ├── Enqueued_Styles_Scope_Check.php │ │ │ ├── Enqueued_Styles_Size_Check.php │ │ │ ├── Image_Functions_Check.php │ │ │ ├── Non_Blocking_Scripts_Check.php │ │ │ └── Performant_WP_Query_Params_Check.php │ │ ├── Plugin_Repo │ │ │ ├── Code_Obfuscation_Check.php │ │ │ ├── File_Type_Check.php │ │ │ ├── Localhost_Check.php │ │ │ ├── No_Unfiltered_Uploads_Check.php │ │ │ ├── Offloading_Files_Check.php │ │ │ ├── Plugin_Header_Fields_Check.php │ │ │ ├── Plugin_Readme_Check.php │ │ │ ├── Plugin_Review_PHPCS_Check.php │ │ │ ├── Plugin_Updater_Check.php │ │ │ ├── Setting_Sanitization_Check.php │ │ │ └── Trademarks_Check.php │ │ └── Security │ │ │ ├── Direct_DB_Queries_Check.php │ │ │ └── Late_Escaping_Check.php │ ├── Default_Check_Collection.php │ ├── Default_Check_Repository.php │ ├── Empty_Check_Repository.php │ ├── Exception │ │ └── Invalid_Check_Slug_Exception.php │ ├── Preparation.php │ ├── Preparations │ │ ├── Demo_Posts_Creation_Preparation.php │ │ ├── Force_Single_Plugin_Preparation.php │ │ ├── Universal_Runtime_Preparation.php │ │ ├── Use_Custom_DB_Tables_Preparation.php │ │ └── Use_Minimal_Theme_Preparation.php │ ├── Runtime_Check.php │ ├── Runtime_Environment_Setup.php │ ├── Static_Check.php │ └── With_Shared_Preparations.php ├── Lib │ └── Readme │ │ └── Parser.php ├── Plugin_Context.php ├── Plugin_Main.php ├── Traits │ ├── Amend_Check_Result.php │ ├── Amend_DB_Base_Prefix.php │ ├── Experimental_Check.php │ ├── File_Editor_URL.php │ ├── Find_Readme.php │ ├── License_Utils.php │ ├── Stable_Check.php │ ├── URL_Aware.php │ └── Version_Utils.php └── Utilities │ └── Plugin_Request_Utility.php ├── patches └── @wordpress+env+10.17.0.patch ├── phpcs-rulesets └── plugin-review.xml ├── phpcs-sniffs ├── .gitignore ├── PluginCheck │ ├── Sniffs │ │ └── CodeAnalysis │ │ │ ├── DiscouragedFunctionsSniff.php │ │ │ ├── EnqueuedResourceOffloadingSniff.php │ │ │ ├── ImageFunctionsSniff.php │ │ │ ├── LocalhostSniff.php │ │ │ ├── OffloadingSniff.php │ │ │ ├── RequiredFunctionParametersSniff.php │ │ │ └── SettingSanitizationSniff.php │ ├── Tests │ │ ├── AbstractSniffUnitTest.php │ │ └── CodeAnalysis │ │ │ ├── DiscouragedFunctionsUnitTest.inc │ │ │ ├── DiscouragedFunctionsUnitTest.php │ │ │ ├── EnqueuedResourceOffloadingUnitTest.inc │ │ │ ├── EnqueuedResourceOffloadingUnitTest.php │ │ │ ├── ImageFunctionsUnitTest.inc │ │ │ ├── ImageFunctionsUnitTest.php │ │ │ ├── LocalhostUnitTest.inc │ │ │ ├── LocalhostUnitTest.php │ │ │ ├── OffloadingUnitTest.inc │ │ │ ├── OffloadingUnitTest.php │ │ │ ├── RequiredFunctionParametersUnitTest.inc │ │ │ ├── RequiredFunctionParametersUnitTest.php │ │ │ ├── SettingSanitizationUnitTest.inc │ │ │ └── SettingSanitizationUnitTest.php │ └── ruleset.xml ├── Tests │ └── bootstrap.php ├── composer.json ├── composer.lock ├── phpcs.xml.dist └── phpunit.xml.dist ├── phpmd.xml ├── plugin.php ├── readme.txt ├── runtime-content └── themes │ └── wp-empty-theme │ ├── comments.php │ ├── footer.php │ ├── functions.php │ ├── header.php │ ├── inc │ ├── back-compat.php │ ├── template-functions.php │ └── template-tags.php │ ├── index.php │ ├── min-style.css │ ├── searchform.php │ └── style.css └── templates ├── admin-page.php ├── results-complete.php ├── results-row.php └── results-table.php /.distignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .idea 4 | .wordpress-org 5 | build 6 | docs 7 | node_modules 8 | patches 9 | # Distributed via vendor/plugin-check/phpcs-sniffs instead 10 | /phpcs-sniffs/ 11 | tests 12 | 13 | *.DS_Store 14 | .DS_Store 15 | .distignore 16 | .editorconfig 17 | .eslintrc.js 18 | .gherkin-lintignore 19 | .gherkin-lintrc 20 | .gitattributes 21 | .gitignore 22 | .nvmrc 23 | .phpunit.result.cache 24 | .typos.toml 25 | .wp-env.json 26 | .wp-env.override.json 27 | behat.yml 28 | codecov.yml 29 | composer.lock 30 | CONTRIBUTING.md 31 | package.json 32 | package-lock.json 33 | phpcs.xml 34 | phpcs.xml.dist 35 | phpmd.xml 36 | phpstan.neon 37 | phpstan.neon.dist 38 | phpunit.xml 39 | phpunit.xml.dist 40 | plugin-check.iml 41 | README.md 42 | SECURITY.md 43 | -------------------------------------------------------------------------------- /assets/css/plugin-check-admin.css: -------------------------------------------------------------------------------- 1 | /* Responsive table */ 2 | @media screen and (max-width: 782px) { 3 | #plugin-check__submit { 4 | margin-top: 1em; 5 | } 6 | 7 | table#plugin-check__categories td { 8 | padding-bottom: .5em; 9 | } 10 | 11 | table.plugin-check__results-table { 12 | border: 0; 13 | } 14 | 15 | table.plugin-check__results-table caption { 16 | font-size: 1.3em; 17 | } 18 | 19 | table.plugin-check__results-table thead { 20 | border: none; 21 | clip: rect(0 0 0 0); 22 | height: 1px; 23 | margin: -1px; 24 | overflow: hidden; 25 | padding: 0; 26 | position: absolute; 27 | width: 1px; 28 | } 29 | 30 | table.plugin-check__results-table tr { 31 | border-bottom: 3px solid #ddd; 32 | display: block; 33 | } 34 | 35 | table.plugin-check__results-table td, 36 | table.plugin-check__results-table code { 37 | font-size: .9em; 38 | } 39 | 40 | table.plugin-check__results-table td { 41 | border-bottom: 1px solid #ddd; 42 | display: block; 43 | text-align: right; 44 | } 45 | 46 | table.plugin-check__results-table td::before { 47 | content: attr(data-label); 48 | float: left; 49 | font-weight: bold; 50 | } 51 | 52 | .rtl table.plugin-check__results-table td { 53 | text-align: left; 54 | } 55 | 56 | .rtl table.plugin-check__results-table td::before { 57 | float: right; 58 | } 59 | 60 | table.plugin-check__results-table td:last-child { 61 | border-bottom: 0; 62 | } 63 | } -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - WordPress\Plugin_Check\Behat_Utils\FeatureContext 6 | paths: 7 | - tests/behat/features 8 | -------------------------------------------------------------------------------- /cli.php: -------------------------------------------------------------------------------- 1 | function () { 50 | if ( 51 | file_exists( ABSPATH . 'wp-content/object-cache.php' ) && 52 | false !== strpos( file_get_contents( ABSPATH . 'wp-content/object-cache.php' ), 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) 53 | ) { 54 | unlink( ABSPATH . 'wp-content/object-cache.php' ); 55 | } 56 | }, 57 | ) 58 | ); 59 | 60 | /* 61 | * Add hook to set up the object-cache.php drop-in file. 62 | * 63 | * Runs after wp-config.php is loaded and thus ABSPATH is defined, 64 | * but before any plugins are actually loaded. 65 | */ 66 | WP_CLI::add_hook( 67 | 'after_wp_config_load', 68 | function () { 69 | if ( CLI_Runner::is_plugin_check() ) { 70 | if ( ! file_exists( ABSPATH . 'wp-content/object-cache.php' ) ) { 71 | if ( ! copy( __DIR__ . '/drop-ins/object-cache.copy.php', ABSPATH . 'wp-content/object-cache.php' ) ) { 72 | WP_CLI::error( 'Unable to copy object-cache.php file.' ); 73 | } 74 | } 75 | } 76 | } 77 | ); 78 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # Overall settings for PR integration via codecov.io 2 | # See https://docs.codecov.com/docs/codecovyml-reference 3 | 4 | # Separate PR statuses for project-level and patch-level coverage 5 | # See https://docs.codecov.com/docs/commit-status 6 | coverage: 7 | status: 8 | # Project-level coverage 9 | project: 10 | default: 11 | base: auto 12 | # Disable once code base is more mature. 13 | informational: true 14 | only_pulls: true 15 | target: auto 16 | threshold: 5% 17 | 18 | # Patch-level coverage (how well is the PR tested) 19 | patch: 20 | default: 21 | base: auto 22 | informational: true 23 | only_pulls: true 24 | target: auto 25 | threshold: 50% 26 | 27 | # Pull request comments 28 | # See https://docs.codecov.com/docs/pull-request-comments 29 | comment: false 30 | 31 | # See https://docs.codecov.com/docs/ignoring-paths 32 | ignore: 33 | - drop-ins 34 | - runtime-content 35 | - tests 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress/plugin-check", 3 | "description": "Plugin Check is a WordPress.org tool which provides checks to help plugins meet the directory requirements and follow various best practices.", 4 | "license": "GPL-2.0-or-later", 5 | "type": "wordpress-plugin", 6 | "require": { 7 | "php": ">=7.4", 8 | "ext-json": "*", 9 | "automattic/vipwpcs": "^3.0.0", 10 | "composer/installers": "^2.2", 11 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", 12 | "plugin-check/phpcs-sniffs": "@dev", 13 | "wp-coding-standards/wpcs": "^3.1.0" 14 | }, 15 | "require-dev": { 16 | "phpcompatibility/php-compatibility": "^9.3", 17 | "phpmd/phpmd": "^2.9", 18 | "phpstan/extension-installer": "^1.2", 19 | "phpstan/phpstan": "^1.10", 20 | "slevomat/coding-standard": "^8.18", 21 | "szepeviktor/phpstan-wordpress": "^1.1", 22 | "wp-cli/extension-command": "^2.1", 23 | "wp-cli/wp-cli": "^2.8", 24 | "wp-cli/wp-cli-tests": "^4.2.9", 25 | "wp-cli/language-command": "^2.0", 26 | "wp-cli/i18n-command": "^2.6", 27 | "wp-cli/entity-command": "^2.8", 28 | "wp-phpunit/wp-phpunit": "^6.3", 29 | "yoast/phpunit-polyfills": "^1.0" 30 | }, 31 | "repositories": [ 32 | { 33 | "type": "path", 34 | "url": "./phpcs-sniffs", 35 | "options": { 36 | "symlink": false 37 | } 38 | } 39 | ], 40 | "autoload": { 41 | "psr-4": { 42 | "WordPress\\Plugin_Check\\": "includes/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "WordPress\\Plugin_Check\\Behat_Utils\\": "tests/behat/includes", 48 | "WordPress\\Plugin_Check\\Test_Data\\": "tests/phpunit/testdata/Checks", 49 | "WordPress\\Plugin_Check\\Test_Utils\\": "tests/phpunit/utils" 50 | } 51 | }, 52 | "config": { 53 | "allow-plugins": { 54 | "composer/installers": true, 55 | "cweagans/composer-patches": false, 56 | "dealerdirect/phpcodesniffer-composer-installer": true, 57 | "phpstan/extension-installer": true 58 | }, 59 | "platform": { 60 | "php": "7.4" 61 | } 62 | }, 63 | "scripts": { 64 | "behat": "BEHAT_FEATURES_FOLDER=tests/behat/features run-behat-tests", 65 | "behat-rerun": "BEHAT_FEATURES_FOLDER=tests/behat/features rerun-behat-tests", 66 | "format": "phpcbf --standard=phpcs.xml.dist", 67 | "lint": "phpcs --standard=phpcs.xml.dist", 68 | "phpmd": "phpmd . text phpmd.xml", 69 | "phpstan": "phpstan analyse --memory-limit=2048M", 70 | "prepare-behat-tests": "install-package-tests", 71 | "test": "phpunit" 72 | }, 73 | "scripts-descriptions": { 74 | "behat": "Run functional tests", 75 | "behat-rerun": "Re-run failed functional tests", 76 | "format": "Detect and automatically fix most coding standards issues", 77 | "lint": "Detect coding standards issues", 78 | "phpmd": "Run PHP mess detector", 79 | "phpstan": "Run static analysis", 80 | "prepare-behat-tests": "Prepare functional tests", 81 | "test": "Run unit tests" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /drop-ins/object-cache.copy.php: -------------------------------------------------------------------------------- 1 | count( $_SERVER['argv'] ) ) { 40 | return false; 41 | } 42 | 43 | if ( 44 | 'plugin' === $_SERVER['argv'][1] && 45 | 'check' === $_SERVER['argv'][2] 46 | ) { 47 | return true; 48 | } 49 | 50 | return false; 51 | } 52 | 53 | /** 54 | * Returns the plugin parameter based on the request. 55 | * 56 | * @since 1.0.0 57 | * 58 | * @return string The plugin parameter. 59 | * 60 | * @throws Exception Thrown if the plugin parameter is empty. 61 | */ 62 | protected function get_plugin_param() { 63 | // Exclude first three reserved elements. 64 | $params = array_slice( $_SERVER['argv'], 3 ); 65 | 66 | // Remove associative arguments. 67 | $params = array_filter( 68 | $params, 69 | static function ( $val ) { 70 | return ! str_starts_with( $val, '--' ); 71 | } 72 | ); 73 | 74 | // Use only first element. We don't support checking multiple plugins at once yet! 75 | $plugin = count( $params ) > 0 ? reset( $params ) : ''; 76 | 77 | if ( empty( $plugin ) ) { 78 | throw new Exception( 79 | __( 'Invalid plugin: Plugin parameter must not be empty.', 'plugin-check' ) 80 | ); 81 | } 82 | 83 | return $plugin; 84 | } 85 | 86 | /** 87 | * Returns an array of Check slugs to run based on the request. 88 | * 89 | * @since 1.0.0 90 | * 91 | * @return array An array of Check slugs to run. 92 | */ 93 | protected function get_check_slugs_param() { 94 | $checks = array(); 95 | 96 | foreach ( $_SERVER['argv'] as $value ) { 97 | if ( false !== strpos( $value, '--checks=' ) ) { 98 | $checks = wp_parse_list( str_replace( '--checks=', '', $value ) ); 99 | break; 100 | } 101 | } 102 | 103 | return $checks; 104 | } 105 | 106 | /** 107 | * Returns an array of Check slugs to exclude based on the request. 108 | * 109 | * @since 1.0.0 110 | * 111 | * @return array An array of Check slugs to run. 112 | */ 113 | protected function get_check_exclude_slugs_param() { 114 | $checks = array(); 115 | 116 | foreach ( $_SERVER['argv'] as $value ) { 117 | if ( false !== strpos( $value, '--exclude-checks=' ) ) { 118 | $checks = wp_parse_list( str_replace( '--exclude-checks=', '', $value ) ); 119 | break; 120 | } 121 | } 122 | 123 | return $checks; 124 | } 125 | 126 | /** 127 | * Returns the include experimental parameter based on the request. 128 | * 129 | * @since 1.0.0 130 | * 131 | * @return bool Returns true to include experimental checks else false. 132 | */ 133 | protected function get_include_experimental_param() { 134 | if ( in_array( '--include-experimental', $_SERVER['argv'], true ) ) { 135 | return true; 136 | } 137 | 138 | return false; 139 | } 140 | 141 | /** 142 | * Returns an array of categories for filtering the checks. 143 | * 144 | * @since 1.0.0 145 | * 146 | * @return array An array of categories. 147 | */ 148 | protected function get_categories_param() { 149 | $categories = array(); 150 | 151 | foreach ( $_SERVER['argv'] as $value ) { 152 | if ( false !== strpos( $value, '--categories=' ) ) { 153 | $categories = wp_parse_list( str_replace( '--categories=', '', $value ) ); 154 | break; 155 | } 156 | } 157 | 158 | return $categories; 159 | } 160 | 161 | /** 162 | * Returns plugin slug parameter. 163 | * 164 | * @since 1.2.0 165 | * 166 | * @return string Plugin slug parameter. 167 | */ 168 | protected function get_slug_param() { 169 | $slug = ''; 170 | 171 | foreach ( $_SERVER['argv'] as $value ) { 172 | if ( false !== strpos( $value, '--slug=' ) ) { 173 | $slug = str_replace( '--slug=', '', $value ); 174 | break; 175 | } 176 | } 177 | 178 | return $slug; 179 | } 180 | 181 | /** 182 | * Initializes the runtime environment so that runtime checks can be run against a separate set of database tables. 183 | * 184 | * @since 1.3.0 185 | * 186 | * @return callable[] Array of cleanup functions to run after the process has completed. 187 | */ 188 | protected function initialize_runtime(): array { 189 | /* 190 | * Since for WP-CLI all checks are run in a single process, we should set up the runtime environment (i.e. 191 | * install the separate database tables) as part of this step. 192 | * This way it runs before the regular runtime preparations, just like it does for the AJAX based flow, where 193 | * they are invoked in a separate request prior to the requests performing actual checks. 194 | */ 195 | $runtime_setup = new Runtime_Environment_Setup(); 196 | $runtime_setup->set_up(); 197 | 198 | $cleanup_functions = parent::initialize_runtime(); 199 | $cleanup_functions[] = function () use ( $runtime_setup ) { 200 | $runtime_setup->clean_up(); 201 | }; 202 | 203 | return $cleanup_functions; 204 | } 205 | 206 | /** 207 | * Checks whether the current environment allows for runtime checks to be used. 208 | * 209 | * @since 1.2.0 210 | * 211 | * @return bool True if runtime checks are allowed, false otherwise. 212 | */ 213 | protected function allow_runtime_checks(): bool { 214 | /* 215 | * For WP-CLI, everything happens in one request. So if the runner was not initialized early, we won't be 216 | * able to set that up, since the object-cache.php drop-in would only become effective in subsequent requests. 217 | */ 218 | if ( ! $this->initialized_early ) { 219 | return false; 220 | } 221 | 222 | return parent::allow_runtime_checks(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /includes/Checker/Check.php: -------------------------------------------------------------------------------- 1 | __( 'General', 'plugin-check' ), 56 | self::CATEGORY_PLUGIN_REPO => __( 'Plugin Repo', 'plugin-check' ), 57 | self::CATEGORY_SECURITY => __( 'Security', 'plugin-check' ), 58 | self::CATEGORY_PERFORMANCE => __( 'Performance', 'plugin-check' ), 59 | self::CATEGORY_ACCESSIBILITY => __( 'Accessibility', 'plugin-check' ), 60 | ); 61 | 62 | /** 63 | * Filters the check categories. 64 | * 65 | * @since 1.0.2 66 | * 67 | * @param array $default_categories Associative array of category slugs to labels. 68 | */ 69 | $check_categories = (array) apply_filters( 'wp_plugin_check_categories', $default_categories ); 70 | 71 | return $check_categories; 72 | } 73 | 74 | /** 75 | * Returns an array of checks. 76 | * 77 | * @since 1.0.0 78 | * 79 | * @param Check_Collection $collection Check collection. 80 | * @param array $categories An array of categories to filter by. 81 | * @return Check_Collection Filtered check collection. 82 | */ 83 | public static function filter_checks_by_categories( Check_Collection $collection, array $categories ): Check_Collection { 84 | return $collection->filter( 85 | static function ( $check ) use ( $categories ) { 86 | // Return true if at least one of the check categories is among the filter categories. 87 | return (bool) array_intersect( $check->get_categories(), $categories ); 88 | } 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /includes/Checker/Check_Collection.php: -------------------------------------------------------------------------------- 1 | $check_obj` pairs. 37 | */ 38 | public function to_map(): array; 39 | 40 | /** 41 | * Returns a new check collection containing the subset of checks based on the given check filter function. 42 | * 43 | * @since 1.0.0 44 | * 45 | * @phpstan-param callable(Check,string): bool $filter_fn 46 | * 47 | * @param callable $filter_fn Filter function that accepts a Check object and a Check slug and 48 | * should return a boolean for whether to include the check in the new collection. 49 | * @return Check_Collection New check collection, effectively a subset of this one. 50 | */ 51 | public function filter( callable $filter_fn ): Check_Collection; 52 | 53 | /** 54 | * Returns a new check collection containing the subset of checks based on the given check slugs. 55 | * 56 | * If the given list is empty, the same collection will be returned without any change. 57 | * 58 | * @since 1.0.0 59 | * 60 | * @param array $check_slugs List of slugs to limit to only those. If empty, the same collection is returned. 61 | * @return Check_Collection New check collection, effectively a subset of this one. 62 | */ 63 | public function include( array $check_slugs ): Check_Collection; 64 | 65 | /** 66 | * Returns a new check collection excluding the provided checks. 67 | * 68 | * If the given list is empty, the same collection will be returned without any change. 69 | * 70 | * @since 1.0.0 71 | * 72 | * @param array $check_slugs List of slugs to exclude. If empty, the same collection is returned. 73 | * @return Check_Collection New check collection, effectively a subset of this one. 74 | */ 75 | public function exclude( array $check_slugs ): Check_Collection; 76 | 77 | /** 78 | * Throws an exception if any of the given check slugs are not present, or returns the same collection otherwise. 79 | * 80 | * @since 1.0.0 81 | * 82 | * @param array $check_slugs List of slugs to limit to only those. If empty, the same collection is returned. 83 | * @return Check_Collection The unchanged check collection. 84 | * 85 | * @throws Invalid_Check_Slug_Exception Thrown when any of the given check slugs is not present in the collection. 86 | */ 87 | public function require( array $check_slugs ): Check_Collection; 88 | } 89 | -------------------------------------------------------------------------------- /includes/Checker/Check_Context.php: -------------------------------------------------------------------------------- 1 | check_context = $check_context; 66 | } 67 | 68 | /** 69 | * Returns the context for the plugin to check. 70 | * 71 | * @since 1.0.0 72 | * 73 | * @return Check_Context Plugin context instance. 74 | */ 75 | public function plugin() { 76 | return $this->check_context; 77 | } 78 | 79 | /** 80 | * Adds an error or warning to the respective stack. 81 | * 82 | * @since 1.0.0 83 | * 84 | * @param bool $error Whether it is an error message. 85 | * @param string $message The message. 86 | * @param array $args { 87 | * Additional message arguments. 88 | * 89 | * @type string $code Violation code according to the message. Default empty string. 90 | * @type string $file The file in which the message occurred. Default empty string (unknown file). 91 | * @type int $line The line on which the message occurred. Default 0 (unknown line). 92 | * @type int $column The column on which the message occurred. Default 0 (unknown column). 93 | * @type string $link View in code editor link. Default empty string. 94 | * } 95 | */ 96 | public function add_message( $error, $message, $args = array() ) { 97 | $defaults = array( 98 | 'code' => '', 99 | 'file' => '', 100 | 'line' => 0, 101 | 'column' => 0, 102 | 'link' => '', 103 | 'docs' => '', 104 | 'severity' => 5, 105 | ); 106 | 107 | $data = array_merge( 108 | array( 109 | 'message' => $message, 110 | ), 111 | $defaults, 112 | array_intersect_key( $args, $defaults ) 113 | ); 114 | 115 | $file = str_replace( $this->plugin()->path( '/' ), '', $data['file'] ); 116 | $line = $data['line']; 117 | $column = $data['column']; 118 | unset( $data['line'], $data['column'], $data['file'] ); 119 | 120 | if ( $error ) { 121 | if ( ! isset( $this->errors[ $file ] ) ) { 122 | $this->errors[ $file ] = array(); 123 | } 124 | if ( ! isset( $this->errors[ $file ][ $line ] ) ) { 125 | $this->errors[ $file ][ $line ] = array(); 126 | } 127 | if ( ! isset( $this->errors[ $file ][ $line ][ $column ] ) ) { 128 | $this->errors[ $file ][ $line ][ $column ] = array(); 129 | } 130 | $this->errors[ $file ][ $line ][ $column ][] = $data; 131 | ++$this->error_count; 132 | } else { 133 | if ( ! isset( $this->warnings[ $file ] ) ) { 134 | $this->warnings[ $file ] = array(); 135 | } 136 | if ( ! isset( $this->warnings[ $file ][ $line ] ) ) { 137 | $this->warnings[ $file ][ $line ] = array(); 138 | } 139 | if ( ! isset( $this->warnings[ $file ][ $line ][ $column ] ) ) { 140 | $this->warnings[ $file ][ $line ][ $column ] = array(); 141 | } 142 | $this->warnings[ $file ][ $line ][ $column ][] = $data; 143 | ++$this->warning_count; 144 | } 145 | } 146 | 147 | /** 148 | * Returns all errors. 149 | * 150 | * @since 1.0.0 151 | * 152 | * @return array All errors with their data. 153 | */ 154 | public function get_errors() { 155 | return $this->errors; 156 | } 157 | 158 | /** 159 | * Returns all warnings. 160 | * 161 | * @since 1.0.0 162 | * 163 | * @return array All warnings with their data. 164 | */ 165 | public function get_warnings() { 166 | return $this->warnings; 167 | } 168 | 169 | /** 170 | * Returns the number of errors. 171 | * 172 | * @since 1.0.0 173 | * 174 | * @return int Number of errors found. 175 | */ 176 | public function get_error_count() { 177 | return $this->error_count; 178 | } 179 | 180 | /** 181 | * Returns the number of warnings. 182 | * 183 | * @since 1.0.0 184 | * 185 | * @return int Number of warnings found. 186 | */ 187 | public function get_warning_count() { 188 | return $this->warning_count; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /includes/Checker/Check_Runner.php: -------------------------------------------------------------------------------- 1 | run_check_with_result( $check, $result ); 46 | } 47 | ); 48 | 49 | return $result; 50 | } 51 | 52 | /** 53 | * Runs a given check with the given result object to amend. 54 | * 55 | * @since 1.0.0 56 | * 57 | * @param Check $check The check to run. 58 | * @param Check_Result $result The result object to amend. 59 | * 60 | * @throws Exception Thrown when check fails with critical error. 61 | */ 62 | private function run_check_with_result( Check $check, Check_Result $result ) { 63 | // If $check implements Preparation interface, ensure the preparation and clean up is run. 64 | if ( $check instanceof Preparation ) { 65 | $cleanup = $check->prepare(); 66 | 67 | try { 68 | $check->run( $result ); 69 | } catch ( Exception $e ) { 70 | // Run clean up in case of any exception thrown from check. 71 | $cleanup(); 72 | throw $e; 73 | } 74 | 75 | $cleanup(); 76 | return; 77 | } 78 | 79 | // Otherwise, just run the check. 80 | $check->run( $result ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Abstract_Runtime_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 51 | 'standard' => 'WordPress', 52 | 'sniffs' => 'WordPress.WP.EnqueuedResources', 53 | ); 54 | } 55 | 56 | /** 57 | * Gets the description for the check. 58 | * 59 | * Every check must have a short description explaining what the check does. 60 | * 61 | * @since 1.1.0 62 | * 63 | * @return string Description. 64 | */ 65 | public function get_description(): string { 66 | return __( 'Checks whether scripts and styles are properly enqueued using the recommended way.', 'plugin-check' ); 67 | } 68 | 69 | /** 70 | * Gets the documentation URL for the check. 71 | * 72 | * Every check must have a URL with further information about the check. 73 | * 74 | * @since 1.1.0 75 | * 76 | * @return string The documentation URL. 77 | */ 78 | public function get_documentation_url(): string { 79 | return __( 'https://developer.wordpress.org/plugins/', 'plugin-check' ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Performance/Enqueued_Scripts_In_Footer_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 48 | 'standard' => 'WordPress', 49 | 'sniffs' => 'WordPress.WP.EnqueuedResourceParameters', 50 | ); 51 | } 52 | 53 | /** 54 | * Gets the description for the check. 55 | * 56 | * Every check must have a short description explaining what the check does. 57 | * 58 | * @since 1.1.0 59 | * 60 | * @return string Description. 61 | */ 62 | public function get_description(): string { 63 | return __( 'Checks whether a loading strategy is explicitly set for JavaScript files, as loading scripts in the footer is usually desired.', 'plugin-check' ); 64 | } 65 | 66 | /** 67 | * Gets the documentation URL for the check. 68 | * 69 | * Every check must have a URL with further information about the check. 70 | * 71 | * @since 1.1.0 72 | * 73 | * @return string The documentation URL. 74 | */ 75 | public function get_documentation_url(): string { 76 | return __( 'https://developer.wordpress.org/plugins/', 'plugin-check' ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Performance/Image_Functions_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 48 | 'standard' => 'PluginCheck', 49 | 'sniffs' => 'PluginCheck.CodeAnalysis.ImageFunctions', 50 | ); 51 | } 52 | 53 | /** 54 | * Gets the description for the check. 55 | * 56 | * Every check must have a short description explaining what the check does. 57 | * 58 | * @since 1.3.0 59 | * 60 | * @return string Description. 61 | */ 62 | public function get_description(): string { 63 | return __( 'Checks whether images are inserted using recommended functions.', 'plugin-check' ); 64 | } 65 | 66 | /** 67 | * Gets the documentation URL for the check. 68 | * 69 | * Every check must have a URL with further information about the check. 70 | * 71 | * @since 1.3.0 72 | * 73 | * @return string The documentation URL. 74 | */ 75 | public function get_documentation_url(): string { 76 | return __( 'https://developer.wordpress.org/plugins/', 'plugin-check' ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Performance/Performant_WP_Query_Params_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 48 | 'standard' => 'WordPress,WordPressVIPMinimum', 49 | 'sniffs' => 'WordPress.DB.SlowDBQuery,WordPressVIPMinimum.Performance.WPQueryParams', 50 | ); 51 | } 52 | 53 | /** 54 | * Gets the description for the check. 55 | * 56 | * Every check must have a short description explaining what the check does. 57 | * 58 | * @since 1.1.0 59 | * 60 | * @return string Description. 61 | */ 62 | public function get_description(): string { 63 | return sprintf( 64 | /* translators: %s WP_Query */ 65 | __( 'Checks for potentially slow database queries when using %s', 'plugin-check' ), 66 | 'WP_Query' 67 | ); 68 | } 69 | 70 | /** 71 | * Gets the documentation URL for the check. 72 | * 73 | * Every check must have a URL with further information about the check. 74 | * 75 | * @since 1.1.0 76 | * 77 | * @return string The documentation URL. 78 | */ 79 | public function get_documentation_url(): string { 80 | return __( 'https://developer.wordpress.org/apis/database/', 'plugin-check' ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Plugin_Repo/Localhost_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 50 | 'standard' => 'PluginCheck', 51 | 'sniffs' => 'PluginCheck.CodeAnalysis.Localhost', 52 | ); 53 | } 54 | 55 | /** 56 | * Gets the description for the check. 57 | * 58 | * Every check must have a short description explaining what the check does. 59 | * 60 | * @since 1.1.0 61 | * 62 | * @return string Description. 63 | */ 64 | public function get_description(): string { 65 | return sprintf( 66 | /* translators: %s: Localhost/127.0.0.1 */ 67 | __( 'Detects the usage of %s in the plugin.', 'plugin-check' ), 68 | 'Localhost/127.0.0.1' 69 | ); 70 | } 71 | 72 | /** 73 | * Gets the documentation URL for the check. 74 | * 75 | * Every check must have a URL with further information about the check. 76 | * 77 | * @since 1.1.0 78 | * 79 | * @return string The documentation URL. 80 | */ 81 | public function get_documentation_url(): string { 82 | return __( 'https://make.wordpress.org/plugins/handbook/performing-reviews/review-checklist/', 'plugin-check' ); 83 | } 84 | 85 | /** 86 | * Amends the given result with a message for the specified file, including error information. 87 | * 88 | * @since 1.3.0 89 | * 90 | * @param Check_Result $result The check result to amend, including the plugin context to check. 91 | * @param bool $error Whether it is an error or notice. 92 | * @param string $message Error message. 93 | * @param string $code Error code. 94 | * @param string $file Absolute path to the file where the issue was found. 95 | * @param int $line The line on which the message occurred. Default is 0 (unknown line). 96 | * @param int $column The column on which the message occurred. Default is 0 (unknown column). 97 | * @param string $docs URL for further information about the message. 98 | * @param int $severity Severity level. Default is 5. 99 | */ 100 | protected function add_result_message_for_file( Check_Result $result, $error, $message, $code, $file, $line = 0, $column = 0, string $docs = '', $severity = 5 ) { 101 | // Override default severity. 102 | $severity = 8; 103 | 104 | parent::add_result_message_for_file( $result, $error, $message, $code, $file, $line, $column, $docs, $severity ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Plugin_Repo/No_Unfiltered_Uploads_Check.php: -------------------------------------------------------------------------------- 1 | add_result_error_for_file( 52 | $result, 53 | sprintf( 54 | /* translators: %s: ALLOW_UNFILTERED_UPLOADS */ 55 | __( '%s is not permitted.
Setting this constant to true will allow the user to upload any type of file (including PHP and other executables), creating serious potential security risks.', 'plugin-check' ), 56 | 'ALLOW_UNFILTERED_UPLOADS' 57 | ), 58 | 'allow_unfiltered_uploads_detected', 59 | $file, 60 | 0, 61 | 0, 62 | 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#files-unfiltered-uploads', 63 | 7 64 | ); 65 | } 66 | } 67 | 68 | /** 69 | * Gets the description for the check. 70 | * 71 | * Every check must have a short description explaining what the check does. 72 | * 73 | * @since 1.1.0 74 | * 75 | * @return string Description. 76 | */ 77 | public function get_description(): string { 78 | return sprintf( 79 | /* translators: %s: ALLOW_UNFILTERED_UPLOADS */ 80 | __( 'Detects disallowed usage of %s.', 'plugin-check' ), 81 | 'ALLOW_UNFILTERED_UPLOADS' 82 | ); 83 | } 84 | 85 | /** 86 | * Gets the documentation URL for the check. 87 | * 88 | * Every check must have a URL with further information about the check. 89 | * 90 | * @since 1.1.0 91 | * 92 | * @return string The documentation URL. 93 | */ 94 | public function get_documentation_url(): string { 95 | return __( 'https://make.wordpress.org/plugins/handbook/performing-reviews/review-checklist/', 'plugin-check' ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Plugin_Repo/Offloading_Files_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 58 | 'standard' => 'PluginCheck', 59 | 'sniffs' => 'PluginCheck.CodeAnalysis.EnqueuedResourceOffloading,PluginCheck.CodeAnalysis.Offloading', 60 | ); 61 | } 62 | 63 | /** 64 | * Gets the description for the check. 65 | * 66 | * Every check must have a short description explaining what the check does. 67 | * 68 | * @since 1.2.0. 69 | * 70 | * @return string Description. 71 | */ 72 | public function get_description(): string { 73 | return __( 'Prevents using remote services that are not necessary.', 'plugin-check' ); 74 | } 75 | 76 | /** 77 | * Gets the documentation URL for the check. 78 | * 79 | * Every check must have a URL with further information about the check. 80 | * 81 | * @since 1.2.0. 82 | * 83 | * @return string The documentation URL. 84 | */ 85 | public function get_documentation_url(): string { 86 | return __( 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#calling-files-remotely', 'plugin-check' ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Plugin_Repo/Plugin_Review_PHPCS_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 48 | 'standard' => WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'phpcs-rulesets/plugin-review.xml', 49 | ); 50 | } 51 | 52 | /** 53 | * Gets the description for the check. 54 | * 55 | * Every check must have a short description explaining what the check does. 56 | * 57 | * @since 1.1.0 58 | * 59 | * @return string Description. 60 | */ 61 | public function get_description(): string { 62 | return __( 'Runs PHP_CodeSniffer to detect certain best practices plugins should follow for submission on WordPress.org.', 'plugin-check' ); 63 | } 64 | 65 | /** 66 | * Gets the documentation URL for the check. 67 | * 68 | * Every check must have a URL with further information about the check. 69 | * 70 | * @since 1.1.0 71 | * 72 | * @return string The documentation URL. 73 | */ 74 | public function get_documentation_url(): string { 75 | return __( 'https://developer.wordpress.org/plugins/plugin-basics/best-practices/', 'plugin-check' ); 76 | } 77 | 78 | /** 79 | * Amends the given result with a message for the specified file, including error information. 80 | * 81 | * @since 1.6.0 82 | * 83 | * @param Check_Result $result The check result to amend, including the plugin context to check. 84 | * @param bool $error Whether it is an error or notice. 85 | * @param string $message Error message. 86 | * @param string $code Error code. 87 | * @param string $file Absolute path to the file where the issue was found. 88 | * @param int $line The line on which the message occurred. Default is 0 (unknown line). 89 | * @param int $column The column on which the message occurred. Default is 0 (unknown column). 90 | * @param string $docs URL for further information about the message. 91 | * @param int $severity Severity level. Default is 5. 92 | */ 93 | protected function add_result_message_for_file( Check_Result $result, $error, $message, $code, $file, $line = 0, $column = 0, string $docs = '', $severity = 5 ) { 94 | if ( 'PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound' === $code ) { 95 | $message .= ' ' . esc_html__( 'When your plugin is hosted on WordPress.org, you no longer need to manually include this function call for translations under your plugin slug. WordPress will automatically load the translations for you as needed.', 'plugin-check' ); 96 | $docs = 'https://make.wordpress.org/core/2016/07/06/i18n-improvements-in-4-6/'; 97 | } 98 | 99 | parent::add_result_message_for_file( $result, $error, $message, $code, $file, $line, $column, $docs, $severity ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Plugin_Repo/Setting_Sanitization_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 50 | 'standard' => 'PluginCheck', 51 | 'sniffs' => 'PluginCheck.CodeAnalysis.SettingSanitization', 52 | ); 53 | } 54 | 55 | /** 56 | * Gets the description for the check. 57 | * 58 | * Every check must have a short description explaining what the check does. 59 | * 60 | * @since 1.4.0 61 | * 62 | * @return string Description. 63 | */ 64 | public function get_description(): string { 65 | return __( 'Ensures sanitization in register_setting().', 'plugin-check' ); 66 | } 67 | 68 | /** 69 | * Gets the documentation URL for the check. 70 | * 71 | * Every check must have a URL with further information about the check. 72 | * 73 | * @since 1.4.0 74 | * 75 | * @return string The documentation URL. 76 | */ 77 | public function get_documentation_url(): string { 78 | return __( 'https://developer.wordpress.org/reference/functions/register_setting/', 'plugin-check' ); 79 | } 80 | 81 | /** 82 | * Amends the given result with a message for the specified file, including error information. 83 | * 84 | * @since 1.4.0 85 | * 86 | * @param Check_Result $result The check result to amend, including the plugin context to check. 87 | * @param bool $error Whether it is an error or notice. 88 | * @param string $message Error message. 89 | * @param string $code Error code. 90 | * @param string $file Absolute path to the file where the issue was found. 91 | * @param int $line The line on which the message occurred. Default is 0 (unknown line). 92 | * @param int $column The column on which the message occurred. Default is 0 (unknown column). 93 | * @param string $docs URL for further information about the message. 94 | * @param int $severity Severity level. Default is 5. 95 | */ 96 | protected function add_result_message_for_file( Check_Result $result, $error, $message, $code, $file, $line = 0, $column = 0, string $docs = '', $severity = 5 ) { 97 | // Update severity. 98 | switch ( $code ) { 99 | case 'PluginCheck.CodeAnalysis.SettingSanitization.register_settingMissing': 100 | case 'PluginCheck.CodeAnalysis.SettingSanitization.register_settingInvalid': 101 | $severity = 7; 102 | break; 103 | 104 | default: 105 | break; 106 | } 107 | 108 | // Add docs link. 109 | $docs = __( 'https://developer.wordpress.org/reference/functions/register_setting/', 'plugin-check' ); 110 | 111 | parent::add_result_message_for_file( $result, $error, $message, $code, $file, $line, $column, $docs, $severity ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Security/Direct_DB_Queries_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 51 | 'standard' => 'WordPress', 52 | 'sniffs' => 'WordPress.DB.DirectDatabaseQuery', 53 | ); 54 | } 55 | 56 | /** 57 | * Gets the description for the check. 58 | * 59 | * Every check must have a short description explaining what the check does. 60 | * 61 | * @since 1.1.0 62 | * 63 | * @return string Description. 64 | */ 65 | public function get_description(): string { 66 | return __( 'Checks the usage of direct database queries, which should be avoided.', 'plugin-check' ); 67 | } 68 | 69 | /** 70 | * Gets the documentation URL for the check. 71 | * 72 | * Every check must have a URL with further information about the check. 73 | * 74 | * @since 1.1.0 75 | * 76 | * @return string The documentation URL. 77 | */ 78 | public function get_documentation_url(): string { 79 | return __( 'https://developer.wordpress.org/apis/database/', 'plugin-check' ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /includes/Checker/Checks/Security/Late_Escaping_Check.php: -------------------------------------------------------------------------------- 1 | 'php', 51 | 'standard' => 'WordPress', 52 | 'sniffs' => 'WordPress.Security.EscapeOutput', 53 | ); 54 | } 55 | 56 | /** 57 | * Gets the description for the check. 58 | * 59 | * Every check must have a short description explaining what the check does. 60 | * 61 | * @since 1.1.0 62 | * 63 | * @return string Description. 64 | */ 65 | public function get_description(): string { 66 | return __( 'Checks that all output is escaped before being sent to the browser.', 'plugin-check' ); 67 | } 68 | 69 | /** 70 | * Gets the documentation URL for the check. 71 | * 72 | * Every check must have a URL with further information about the check. 73 | * 74 | * @since 1.1.0 75 | * 76 | * @return string The documentation URL. 77 | */ 78 | public function get_documentation_url(): string { 79 | return __( 'https://developer.wordpress.org/apis/security/escaping/', 'plugin-check' ); 80 | } 81 | 82 | /** 83 | * Amends the given result with a message for the specified file, including error information. 84 | * 85 | * @since 1.3.0 86 | * 87 | * @param Check_Result $result The check result to amend, including the plugin context to check. 88 | * @param bool $error Whether it is an error or notice. 89 | * @param string $message Error message. 90 | * @param string $code Error code. 91 | * @param string $file Absolute path to the file where the issue was found. 92 | * @param int $line The line on which the message occurred. Default is 0 (unknown line). 93 | * @param int $column The column on which the message occurred. Default is 0 (unknown column). 94 | * @param string $docs URL for further information about the message. 95 | * @param int $severity Severity level. Default is 5. 96 | */ 97 | protected function add_result_message_for_file( Check_Result $result, $error, $message, $code, $file, $line = 0, $column = 0, string $docs = '', $severity = 5 ) { 98 | switch ( $code ) { 99 | case 'WordPress.Security.EscapeOutput.OutputNotEscaped': 100 | $docs = __( 'https://developer.wordpress.org/apis/security/escaping/#escaping-functions', 'plugin-check' ); 101 | break; 102 | 103 | case 'WordPress.Security.EscapeOutput.UnsafePrintingFunction': 104 | $docs = __( 'https://developer.wordpress.org/apis/security/escaping/#escaping-with-localization', 'plugin-check' ); 105 | break; 106 | 107 | case 'WordPress.Security.EscapeOutput.UnsafeSearchQuery': 108 | $docs = __( 'https://developer.wordpress.org/reference/functions/get_search_query/', 'plugin-check' ); 109 | break; 110 | 111 | default: 112 | $docs = __( 'https://developer.wordpress.org/apis/security/escaping/', 'plugin-check' ); 113 | break; 114 | } 115 | 116 | parent::add_result_message_for_file( $result, $error, $message, $code, $file, $line, $column, $docs, $severity ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /includes/Checker/Default_Check_Repository.php: -------------------------------------------------------------------------------- 1 | fully_initialized = did_action( 'plugins_loaded' ) > 0; 35 | $this->register_default_checks(); 36 | } 37 | 38 | /** 39 | * Returns an array of checks. 40 | * 41 | * @since 1.0.0 42 | * 43 | * @param int $flags The check type flag. 44 | * @return Check_Collection Check collection providing an indexed array of check instances. 45 | */ 46 | public function get_checks( $flags = self::TYPE_ALL ) { 47 | // Once plugins have been loaded, re-initialize the checks. 48 | if ( ! $this->fully_initialized && did_action( 'plugins_loaded' ) ) { 49 | $this->fully_initialized = true; 50 | $this->runtime_checks = array(); 51 | $this->static_checks = array(); 52 | $this->register_default_checks(); 53 | } 54 | 55 | return parent::get_checks( $flags ); 56 | } 57 | 58 | /** 59 | * Registers Checks. 60 | * 61 | * @since 1.0.0 62 | */ 63 | private function register_default_checks() { 64 | /** 65 | * Filters the available plugin check classes. 66 | * 67 | * @since 1.0.0 68 | * 69 | * @param array $checks An array map of check slugs to Check instances. 70 | */ 71 | $checks = apply_filters( 72 | 'wp_plugin_check_checks', 73 | array( 74 | 'i18n_usage' => new Checks\General\I18n_Usage_Check(), 75 | 'enqueued_scripts_size' => new Checks\Performance\Enqueued_Scripts_Size_Check(), 76 | 'enqueued_styles_size' => new Checks\Performance\Enqueued_Styles_Size_Check(), 77 | 'code_obfuscation' => new Checks\Plugin_Repo\Code_Obfuscation_Check(), 78 | 'file_type' => new Checks\Plugin_Repo\File_Type_Check(), 79 | 'plugin_header_fields' => new Checks\Plugin_Repo\Plugin_Header_Fields_Check(), 80 | 'late_escaping' => new Checks\Security\Late_Escaping_Check(), 81 | 'plugin_updater' => new Checks\Plugin_Repo\Plugin_Updater_Check(), 82 | 'plugin_review_phpcs' => new Checks\Plugin_Repo\Plugin_Review_PHPCS_Check(), 83 | 'direct_db_queries' => new Checks\Security\Direct_DB_Queries_Check(), 84 | 'performant_wp_query_params' => new Checks\Performance\Performant_WP_Query_Params_Check(), 85 | 'enqueued_scripts_in_footer' => new Checks\Performance\Enqueued_Scripts_In_Footer_Check(), 86 | 'enqueued_resources' => new Checks\Performance\Enqueued_Resources_Check(), 87 | 'plugin_readme' => new Checks\Plugin_Repo\Plugin_Readme_Check(), 88 | 'enqueued_styles_scope' => new Checks\Performance\Enqueued_Styles_Scope_Check(), 89 | 'enqueued_scripts_scope' => new Checks\Performance\Enqueued_Scripts_Scope_Check(), 90 | 'localhost' => new Checks\Plugin_Repo\Localhost_Check(), 91 | 'no_unfiltered_uploads' => new Checks\Plugin_Repo\No_Unfiltered_Uploads_Check(), 92 | 'trademarks' => new Checks\Plugin_Repo\Trademarks_Check(), 93 | 'non_blocking_scripts' => new Checks\Performance\Non_Blocking_Scripts_Check(), 94 | 'offloading_files' => new Checks\Plugin_Repo\Offloading_Files_Check(), 95 | 'image_functions' => new Checks\Performance\Image_Functions_Check(), 96 | 'setting_sanitization' => new Checks\Plugin_Repo\Setting_Sanitization_Check(), 97 | ) 98 | ); 99 | 100 | foreach ( $checks as $slug => $check ) { 101 | $this->register_check( $slug, $check ); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /includes/Checker/Empty_Check_Repository.php: -------------------------------------------------------------------------------- 1 | runtime_checks[ $slug ] ) || isset( $this->static_checks[ $slug ] ) ) { 57 | throw new Exception( 58 | sprintf( 59 | /* translators: %s: The Check slug. */ 60 | __( 'Check slug "%s" is already in use.', 'plugin-check' ), 61 | $slug 62 | ) 63 | ); 64 | } 65 | 66 | if ( ! $check->get_categories() ) { 67 | throw new Exception( 68 | sprintf( 69 | /* translators: %s: The Check slug. */ 70 | __( 'Check with slug "%s" has no categories associated with it.', 'plugin-check' ), 71 | $slug 72 | ) 73 | ); 74 | } 75 | 76 | $check_array = $check instanceof Runtime_Check ? 'runtime_checks' : 'static_checks'; 77 | $this->{$check_array}[ $slug ] = $check; 78 | } 79 | 80 | /** 81 | * Returns an array of checks. 82 | * 83 | * @since 1.0.0 84 | * 85 | * @param int $flags The check type flag. 86 | * @return Check_Collection Check collection providing an indexed array of check instances. 87 | */ 88 | public function get_checks( $flags = self::TYPE_ALL ) { 89 | $checks = array(); 90 | 91 | if ( $flags & self::TYPE_STATIC ) { 92 | $checks += $this->static_checks; 93 | } 94 | 95 | if ( $flags & self::TYPE_RUNTIME ) { 96 | $checks += $this->runtime_checks; 97 | } 98 | 99 | // Return all checks, including experimental if requested. 100 | if ( $flags & self::INCLUDE_EXPERIMENTAL ) { 101 | return new Default_Check_Collection( $checks ); 102 | } 103 | 104 | // Remove experimental checks before returning. 105 | return ( new Default_Check_Collection( $checks ) )->filter( 106 | static function ( Check $check ) { 107 | return $check->get_stability() !== Check::STABILITY_EXPERIMENTAL; 108 | } 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /includes/Checker/Exception/Invalid_Check_Slug_Exception.php: -------------------------------------------------------------------------------- 1 | posts = $posts; 37 | } 38 | 39 | /** 40 | * Creates the demo posts in the database to be us 41 | * 42 | * @since 1.0.0 43 | * 44 | * @return callable Cleanup function to revert changes made by theme and plugin preparation classes. 45 | * 46 | * @throws Exception Thrown when preparation fails. 47 | */ 48 | public function prepare() { 49 | $post_ids = array(); 50 | 51 | foreach ( $this->posts as $postarr ) { 52 | $post_id = wp_insert_post( $postarr, true ); 53 | 54 | if ( is_wp_error( $post_id ) ) { 55 | throw new Exception( $post_id->get_error_message() ); 56 | } 57 | 58 | $post_ids[] = $post_id; 59 | } 60 | 61 | // Return the cleanup function. 62 | return function () use ( $post_ids ) { 63 | foreach ( $post_ids as $post_id ) { 64 | wp_delete_post( $post_id, true ); 65 | } 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /includes/Checker/Preparations/Force_Single_Plugin_Preparation.php: -------------------------------------------------------------------------------- 1 | plugin_basename = $plugin_basename; 40 | } 41 | 42 | /** 43 | * Runs this preparation step for the environment and returns a cleanup function. 44 | * 45 | * @since 1.0.0 46 | * 47 | * @return callable Cleanup function to revert any changes made here. 48 | * 49 | * @throws Exception Thrown when preparation fails. 50 | */ 51 | public function prepare() { 52 | 53 | add_filter( 'option_active_plugins', array( $this, 'filter_active_plugins' ) ); 54 | add_filter( 'default_option_active_plugins', array( $this, 'filter_active_plugins' ) ); 55 | 56 | // Return the cleanup function. 57 | return function () { 58 | remove_filter( 'option_active_plugins', array( $this, 'filter_active_plugins' ) ); 59 | remove_filter( 'default_option_active_plugins', array( $this, 'filter_active_plugins' ) ); 60 | }; 61 | } 62 | 63 | /** 64 | * Filters active plugins to only include required ones. 65 | * 66 | * This means: 67 | * 68 | * * The plugin being tested 69 | * * All dependencies of the plugin being tested 70 | * * Plugin Check itself 71 | * * All plugins depending on Plugin Check (they could be adding new checks) 72 | * 73 | * @since 1.0.0 74 | * @since 1.2.0 Now includes dependencies and dependents. 75 | * 76 | * @param mixed $active_plugins List of active plugins. 77 | * @return mixed List of active plugins. 78 | */ 79 | public function filter_active_plugins( $active_plugins ) { 80 | if ( ! is_array( $active_plugins ) ) { 81 | return $active_plugins; 82 | } 83 | 84 | // The plugin being tested isn't actually active yet. 85 | if ( ! in_array( $this->plugin_basename, $active_plugins, true ) ) { 86 | return $active_plugins; 87 | } 88 | 89 | if ( defined( 'WP_PLUGIN_CHECK_MAIN_FILE' ) ) { 90 | $plugin_check_file = WP_PLUGIN_CHECK_MAIN_FILE; 91 | } else { 92 | $plugin_check_file = basename( dirname( __DIR__, 3 ) ) . '/plugin.php'; 93 | } 94 | 95 | $plugin_check_basename = plugin_basename( $plugin_check_file ); 96 | 97 | $new_active_plugins = array( 98 | $this->plugin_basename, // Plugin to test. 99 | $plugin_check_basename, // Plugin Check itself. 100 | ); 101 | 102 | // Plugin dependencies support was added in WordPress 6.5. 103 | if ( class_exists( 'WP_Plugin_Dependencies' ) ) { 104 | WP_Plugin_Dependencies::initialize(); 105 | 106 | $new_active_plugins = array_merge( 107 | $new_active_plugins, 108 | WP_Plugin_Dependencies::get_dependencies( $this->plugin_basename ) 109 | ); 110 | 111 | $new_active_plugins = array_merge( 112 | $new_active_plugins, 113 | // Include any dependents of Plugin Check, but only if they were already active. 114 | array_filter( 115 | WP_Plugin_Dependencies::get_dependents( dirname( $plugin_check_basename ) ), 116 | static function ( $dependent ) use ( $active_plugins ) { 117 | return in_array( $dependent, $active_plugins, true ); 118 | } 119 | ) 120 | ); 121 | } 122 | 123 | // Removes duplicates, for example if Plugin Check is the plugin being tested. 124 | return array_unique( $new_active_plugins ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /includes/Checker/Preparations/Universal_Runtime_Preparation.php: -------------------------------------------------------------------------------- 1 | check_context = $check_context; 38 | } 39 | 40 | /** 41 | * Runs preparation step for the environment by modifying the plugins and theme to use, 42 | * and returns a closure as a cleanup function. 43 | * 44 | * This preparation needs to be called very early in the WordPress lifecycle, before 45 | * plugins are loaded, e.g. from a drop-in like `object-cache.php`. 46 | * 47 | * @since 1.0.0 48 | * 49 | * @return callable Cleanup function to revert changes made by theme and plugin preparation classes. 50 | * 51 | * @throws Exception Thrown when preparation fails. 52 | */ 53 | public function prepare() { 54 | 55 | $cleanup_functions = array(); 56 | 57 | if ( ! defined( 'WP_PLUGIN_CHECK_PLUGIN_DIR_PATH' ) ) { 58 | $plugins_dir = defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : WP_CONTENT_DIR . '/plugins'; 59 | $theme_folder = $plugins_dir . '/plugin-check/runtime-content/themes'; 60 | } else { 61 | $theme_folder = WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'runtime-content/themes'; 62 | } 63 | 64 | $use_custom_db_tables_preparation = new Use_Custom_DB_Tables_Preparation(); 65 | $cleanup_functions[] = $use_custom_db_tables_preparation->prepare(); 66 | 67 | $use_minimal_theme_preparation = new Use_Minimal_Theme_Preparation( 'wp-empty-theme', $theme_folder ); 68 | $cleanup_functions[] = $use_minimal_theme_preparation->prepare(); 69 | 70 | $force_single_plugin_preparation = new Force_Single_Plugin_Preparation( $this->check_context->basename() ); 71 | $cleanup_functions[] = $force_single_plugin_preparation->prepare(); 72 | 73 | // Revert order so that earlier preparations are cleaned up later. 74 | $cleanup_functions = array_reverse( $cleanup_functions ); 75 | 76 | // Return the cleanup function. 77 | return function () use ( $cleanup_functions ) { 78 | 79 | foreach ( $cleanup_functions as $cleanup_function ) { 80 | $cleanup_function(); 81 | } 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /includes/Checker/Preparations/Use_Custom_DB_Tables_Preparation.php: -------------------------------------------------------------------------------- 1 | amend_db_base_prefix(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /includes/Checker/Preparations/Use_Minimal_Theme_Preparation.php: -------------------------------------------------------------------------------- 1 | theme_slug = $theme_slug; 48 | $this->themes_dir = $themes_dir; 49 | } 50 | 51 | /** 52 | * Runs this preparation step for the environment and returns a cleanup function. 53 | * 54 | * @since 1.0.0 55 | * 56 | * @global array $wp_theme_directories 57 | * 58 | * @return callable Cleanup function to revert any changes made here. 59 | * 60 | * @throws Exception Thrown when preparation fails. 61 | */ 62 | public function prepare() { 63 | // Override the theme slug and name. 64 | add_filter( 'template', array( $this, 'get_theme_slug' ) ); 65 | add_filter( 'stylesheet', array( $this, 'get_theme_slug' ) ); 66 | add_filter( 'pre_option_template', array( $this, 'get_theme_slug' ) ); 67 | add_filter( 'pre_option_stylesheet', array( $this, 'get_theme_slug' ) ); 68 | add_filter( 'pre_option_current_theme', array( $this, 'get_theme_name' ) ); 69 | 70 | // Override the theme directory. 71 | add_filter( 'pre_option_template_root', array( $this, 'get_theme_root' ) ); 72 | add_filter( 'pre_option_stylesheet_root', array( $this, 'get_theme_root' ) ); 73 | 74 | // Registers the custom themes directory if relevant. 75 | if ( ! empty( $this->themes_dir ) ) { 76 | register_theme_directory( $this->themes_dir ); 77 | 78 | // Force new directory scan to ensure the test theme directory is available. 79 | search_theme_directories( true ); 80 | } 81 | 82 | // Return the cleanup function. 83 | return function () { 84 | global $wp_theme_directories; 85 | 86 | remove_filter( 'template', array( $this, 'get_theme_slug' ) ); 87 | remove_filter( 'stylesheet', array( $this, 'get_theme_slug' ) ); 88 | remove_filter( 'pre_option_template', array( $this, 'get_theme_slug' ) ); 89 | remove_filter( 'pre_option_stylesheet', array( $this, 'get_theme_slug' ) ); 90 | remove_filter( 'pre_option_current_theme', array( $this, 'get_theme_name' ) ); 91 | 92 | remove_filter( 'pre_option_template_root', array( $this, 'get_theme_root' ) ); 93 | remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_theme_root' ) ); 94 | 95 | if ( ! empty( $this->themes_dir ) ) { 96 | $index = array_search( untrailingslashit( $this->themes_dir ), $wp_theme_directories, true ); 97 | if ( false !== $index ) { 98 | array_splice( $wp_theme_directories, $index, 1 ); 99 | $wp_theme_directories = array_values( $wp_theme_directories ); 100 | 101 | // Force new directory scan to remove the test theme directory. 102 | search_theme_directories( true ); 103 | } 104 | } 105 | }; 106 | } 107 | 108 | /** 109 | * Gets the theme slug. 110 | * 111 | * Used as a filter callback. 112 | * 113 | * @since 1.0.0 114 | * 115 | * @return string The theme slug. 116 | */ 117 | public function get_theme_slug() { 118 | return $this->theme_slug; 119 | } 120 | 121 | /** 122 | * Gets the theme name. 123 | * 124 | * Used as a filter callback. 125 | * 126 | * @since 1.0.0 127 | * 128 | * @return string The theme name. 129 | * 130 | * @throws Exception Thrown if theme does not exist for some reason. 131 | */ 132 | public function get_theme_name() { 133 | $theme = wp_get_theme( $this->theme_slug, $this->themes_dir ); 134 | if ( ! $theme->exists() ) { 135 | throw new Exception( 136 | __( 'Invalid theme: Theme does not exist for some reason.', 'plugin-check' ) 137 | ); 138 | } 139 | return $theme->display( 'Name' ); 140 | } 141 | 142 | /** 143 | * Gets the theme root. 144 | * 145 | * Used as a filter callback. 146 | * 147 | * @since 1.0.0 148 | * 149 | * @return string The theme root. 150 | */ 151 | public function get_theme_root() { 152 | return get_raw_theme_root( $this->theme_slug, true ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /includes/Checker/Runtime_Check.php: -------------------------------------------------------------------------------- 1 | $constructor_args pairs. If the class does not 23 | * need any constructor arguments, it would just be an empty array. 24 | */ 25 | public function get_shared_preparations(); 26 | } 27 | -------------------------------------------------------------------------------- /includes/Plugin_Main.php: -------------------------------------------------------------------------------- 1 | context = new Plugin_Context( $main_file ); 37 | } 38 | 39 | /** 40 | * Returns the Plugin Context. 41 | * 42 | * @since 1.0.0 43 | * 44 | * @return Plugin_Context 45 | */ 46 | public function context() { 47 | return $this->context; 48 | } 49 | 50 | /** 51 | * Registers WordPress hooks for the plugin. 52 | * 53 | * @since 1.0.0 54 | * 55 | * @global Plugin_Context $context The plugin context instance. 56 | */ 57 | public function add_hooks() { 58 | if ( defined( 'WP_CLI' ) && WP_CLI ) { 59 | global $context; 60 | 61 | // Setup the CLI command. 62 | $context = $this->context(); 63 | require_once WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'cli.php'; 64 | } 65 | 66 | $admin_ajax = new Admin_AJAX(); 67 | // Create the Admin page. 68 | $admin_page = new Admin_Page( $admin_ajax ); 69 | $admin_page->add_hooks(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /includes/Traits/Amend_Check_Result.php: -------------------------------------------------------------------------------- 1 | add_message( 39 | (bool) $error, 40 | $message, 41 | array( 42 | 'code' => $code, 43 | 'file' => str_replace( $result->plugin()->path(), '', $file ), 44 | 'line' => $line, 45 | 'column' => $column, 46 | 'link' => $this->get_file_editor_url( $result, $file, $line ), 47 | 'docs' => $docs, 48 | 'severity' => $severity, 49 | ) 50 | ); 51 | } 52 | 53 | /** 54 | * Amends the given result with an error message for the specified file. 55 | * 56 | * @since 1.0.0 57 | * 58 | * @param Check_Result $result The check result to amend, including the plugin context to check. 59 | * @param string $message Error message. 60 | * @param string $code Error code. 61 | * @param string $file Absolute path to the file where the error was found. 62 | * @param int $line The line on which the error occurred. Default is 0 (unknown line). 63 | * @param int $column The column on which the error occurred. Default is 0 (unknown column). 64 | * @param string $docs URL for further information about the message. 65 | * @param int $severity Severity level. Default is 5. 66 | */ 67 | protected function add_result_error_for_file( Check_Result $result, $message, $code, $file, $line = 0, $column = 0, string $docs = '', $severity = 5 ) { 68 | $this->add_result_message_for_file( $result, true, $message, $code, $file, $line, $column, $docs, $severity ); 69 | } 70 | 71 | /** 72 | * Amends the given result with a warning message for the specified file. 73 | * 74 | * @since 1.0.0 75 | * 76 | * @param Check_Result $result The check result to amend, including the plugin context to check. 77 | * @param string $message Error message. 78 | * @param string $code Error code. 79 | * @param string $file Absolute path to the file where the warning was found. 80 | * @param int $line The line on which the warning occurred. Default is 0 (unknown line). 81 | * @param int $column The column on which the warning occurred. Default is 0 (unknown column). 82 | * @param string $docs URL for further information about the message. 83 | * @param int $severity Severity level. Default is 5. 84 | */ 85 | protected function add_result_warning_for_file( Check_Result $result, $message, $code, $file, $line = 0, $column = 0, string $docs = '', $severity = 5 ) { 86 | $this->add_result_message_for_file( $result, false, $message, $code, $file, $line, $column, $docs, $severity ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /includes/Traits/Amend_DB_Base_Prefix.php: -------------------------------------------------------------------------------- 1 | base_prefix` 50 | * property has been set. Therefore we need to rely on `$wpdb->base_prefix`, which should always be already 51 | * set, even when PCP is initializing early. 52 | */ 53 | // @phpstan-ignore-next-line isset.property 54 | if ( ! isset( $wpdb->base_prefix ) ) { 55 | throw new RuntimeException( 56 | esc_html__( 'Cannot amend database table prefix as wpdb appears to not be initialized yet.', 'plugin-check' ) 57 | ); 58 | } 59 | 60 | $old_prefix = $wpdb->set_prefix( $wpdb->base_prefix . $base_prefix_suffix ); 61 | 62 | return function () use ( $old_prefix ) { 63 | global $wpdb; 64 | 65 | $wpdb->set_prefix( $old_prefix ); 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /includes/Traits/Experimental_Check.php: -------------------------------------------------------------------------------- 1 | plugin()->path( '/' ); 34 | $plugin_slug = $result->plugin()->slug(); 35 | $filename = str_replace( $plugin_path, '', $filename ); 36 | /** 37 | * Filters the template for the URL for linking to an external editor to open a file for editing. 38 | * 39 | * Users of IDEs that support opening files in via web protocols can use this filter to override 40 | * the edit link to result in their editor opening rather than the plugin editor. 41 | * 42 | * The initial filtered value is null, requiring extension plugins to supply the URL template 43 | * string themselves. If no template string is provided, links to the plugin editors will 44 | * be provided if available. For example, for an extension plugin to cause file edit links to 45 | * open in an IDE, the following filters can be used: 46 | * 47 | * # PhpStorm 48 | * add_filter( 'wp_plugin_check_validation_error_source_file_editor_url_template', function () { 49 | * return 'phpstorm://open?file={{file}}&line={{line}}'; 50 | * } ); 51 | * 52 | * # VS Code 53 | * add_filter( 'wp_plugin_check_validation_error_source_file_editor_url_template', function () { 54 | * return 'vscode://file/{{file}}:{{line}}'; 55 | * } ); 56 | * 57 | * For a template to be considered, the string '{{file}}' must be present in the filtered value. 58 | * 59 | * @since 1.0.0 60 | * 61 | * @param string|null $editor_url_template Editor URL template. default null. 62 | */ 63 | $editor_url_template = apply_filters( 'wp_plugin_check_validation_error_source_file_editor_url_template', null ); 64 | 65 | // Supply the file path to the editor template. 66 | if ( is_string( $editor_url_template ) && str_contains( $editor_url_template, '{{file}}' ) ) { 67 | $file_path = WP_PLUGIN_DIR . '/' . $plugin_slug; 68 | if ( $plugin_slug !== $filename ) { 69 | $file_path .= '/' . $filename; 70 | } 71 | 72 | if ( file_exists( $file_path ) ) { 73 | /** 74 | * Filters the file path to be opened in an external editor for a given PHPCS error source. 75 | * 76 | * This is useful to map the file path from inside of a Docker container or VM to the host machine. 77 | * 78 | * @since 1.0.0 79 | * 80 | * @param string|null $editor_url_template Editor URL template. 81 | * @param array $source Source information. 82 | */ 83 | $file_path = apply_filters( 'wp_plugin_check_validation_error_source_file_path', $file_path, array( $plugin_slug, $filename, $line ) ); 84 | if ( $file_path ) { 85 | $edit_url = str_replace( 86 | array( 87 | '{{file}}', 88 | '{{line}}', 89 | ), 90 | array( 91 | rawurlencode( $file_path ), 92 | $line, 93 | ), 94 | $editor_url_template 95 | ); 96 | } 97 | } 98 | } 99 | 100 | // Fall back to using the plugin editor if no external editor is offered. 101 | if ( ! $edit_url && current_user_can( 'edit_plugins' ) ) { 102 | $file = ''; 103 | 104 | if ( $result->plugin()->is_single_file_plugin() ) { 105 | $file = $filename; 106 | } elseif ( $result->plugin()->is_file_editable( $filename ) ) { 107 | $file = $plugin_slug . '/' . $filename; 108 | } 109 | 110 | if ( ! empty( $file ) ) { 111 | $query_args = array( 112 | 'plugin' => rawurlencode( $result->plugin()->basename() ), 113 | 'file' => rawurlencode( $file ), 114 | ); 115 | if ( $line ) { 116 | $query_args['line'] = $line; 117 | } 118 | return add_query_arg( 119 | $query_args, 120 | admin_url( 'plugin-editor.php' ) 121 | ); 122 | } 123 | } 124 | return $edit_url; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /includes/Traits/Find_Readme.php: -------------------------------------------------------------------------------- 1 | array(), 59 | 'post' => array(), 60 | 'server' => array(), 61 | 'global_vars' => array(), 62 | ); 63 | 64 | /** 65 | * Backups the original values for any global state that may be modified to be restored later. 66 | * 67 | * @since 1.0.0 68 | */ 69 | protected function backup_globals() { 70 | $this->global_values = array( 71 | 'get' => $_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended 72 | 'post' => $_POST, // phpcs:ignore WordPress.Security.NonceVerification.Missing 73 | 'server' => $_SERVER, 74 | ); 75 | 76 | $global_vars = array(); 77 | $global_keys = array_merge( $this->query_globals, $this->wp_globals ); 78 | 79 | foreach ( $global_keys as $query_global ) { 80 | if ( isset( $GLOBALS[ $query_global ] ) ) { 81 | $global_vars[ $query_global ] = $GLOBALS[ $query_global ]; 82 | } 83 | } 84 | 85 | $this->global_values['global_vars'] = $global_vars; 86 | } 87 | 88 | /** 89 | * Restores the original values for any global state that may have been modified. 90 | * 91 | * @since 1.0.0 92 | */ 93 | protected function restore_globals() { 94 | $_GET = $this->global_values['get']; 95 | $_POST = $this->global_values['post']; 96 | $_SERVER = $this->global_values['server']; 97 | 98 | $global_vars = $this->global_values['global_vars']; 99 | $global_keys = array_merge( $this->query_globals, $this->wp_globals ); 100 | 101 | foreach ( $global_keys as $query_global ) { 102 | if ( isset( $global_vars[ $query_global ] ) ) { 103 | $GLOBALS[ $query_global ] = $global_vars[ $query_global ]; 104 | } else { 105 | unset( $GLOBALS[ $query_global ] ); 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * Sets the global state to as if a given URL has been requested. 112 | * 113 | * @since 1.0.0 114 | * 115 | * @param string $url URL to simulate request for. 116 | * 117 | * @SuppressWarnings(PHPMD.NPathComplexity) 118 | */ 119 | protected function go_to( $url ) { 120 | /* 121 | * Note: the WP and WP_Query classes like to silently fetch parameters 122 | * from all over the place (globals, GET, etc), which makes it tricky 123 | * to run them more than once without very carefully clearing everything. 124 | */ 125 | $_GET = array(); 126 | $_POST = array(); 127 | 128 | foreach ( $this->query_globals as $v ) { 129 | if ( isset( $GLOBALS[ $v ] ) ) { 130 | unset( $GLOBALS[ $v ] ); 131 | } 132 | } 133 | 134 | $parts = wp_parse_url( $url ); 135 | if ( isset( $parts['scheme'] ) ) { 136 | $req = isset( $parts['path'] ) ? $parts['path'] : ''; 137 | if ( isset( $parts['query'] ) ) { 138 | $req .= '?' . $parts['query']; 139 | // Parse the URL query vars into $_GET. 140 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended 141 | parse_str( $parts['query'], $_GET ); 142 | } 143 | } else { 144 | $req = $url; 145 | } 146 | 147 | if ( ! isset( $parts['query'] ) ) { 148 | $parts['query'] = ''; 149 | } 150 | 151 | $_SERVER['REQUEST_URI'] = $req; 152 | unset( $_SERVER['PATH_INFO'] ); 153 | 154 | unset( $GLOBALS['wp_query'], $GLOBALS['wp_the_query'] ); 155 | $GLOBALS['wp_the_query'] = new \WP_Query(); 156 | $GLOBALS['wp_query'] = $GLOBALS['wp_the_query']; 157 | 158 | $public_query_vars = $GLOBALS['wp']->public_query_vars; 159 | $private_query_vars = $GLOBALS['wp']->private_query_vars; 160 | 161 | $GLOBALS['wp'] = new \WP(); 162 | $GLOBALS['wp']->public_query_vars = $public_query_vars; 163 | $GLOBALS['wp']->private_query_vars = $private_query_vars; 164 | 165 | // Clean up query vars. 166 | foreach ( $GLOBALS['wp']->public_query_vars as $v ) { 167 | unset( $GLOBALS[ $v ] ); 168 | } 169 | 170 | foreach ( $GLOBALS['wp']->private_query_vars as $v ) { 171 | unset( $GLOBALS[ $v ] ); 172 | } 173 | 174 | // Set up query vars for taxonomies and post types. 175 | foreach ( get_taxonomies( array(), 'objects' ) as $t ) { 176 | if ( $t->publicly_queryable && ! empty( $t->query_var ) ) { 177 | $GLOBALS['wp']->add_query_var( $t->query_var ); 178 | } 179 | } 180 | 181 | foreach ( get_post_types( array(), 'objects' ) as $t ) { 182 | if ( is_post_type_viewable( $t ) && ! empty( $t->query_var ) ) { 183 | $GLOBALS['wp']->add_query_var( $t->query_var ); 184 | } 185 | } 186 | 187 | $GLOBALS['wp']->main( $parts['query'] ); 188 | } 189 | 190 | /** 191 | * Simulate all the urls like WP and run the cleanup function. 192 | * 193 | * @since 1.0.0 194 | * 195 | * @param array $urls An array of URLs to run. 196 | * @param callable $callback Callback function to run for each URL. 197 | */ 198 | protected function run_for_urls( $urls, $callback ) { 199 | if ( ! empty( $urls ) ) { 200 | foreach ( $urls as $url ) { 201 | $this->go_to( $url ); 202 | $callback( $url ); 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /includes/Traits/Version_Utils.php: -------------------------------------------------------------------------------- 1 | get_latest_version_info( 'current' ); 26 | 27 | // Strip off any -alpha, -RC, -beta suffixes. 28 | list( $version, ) = explode( '-', $version ); 29 | 30 | if ( preg_match( '#^\d.\d#', $version, $matches ) ) { 31 | $version = $matches[0]; 32 | } 33 | 34 | return $version; 35 | } 36 | 37 | /** 38 | * Returns WordPress latest version. 39 | * 40 | * @since 1.3.1 41 | * 42 | * @return string WordPress latest version. 43 | */ 44 | protected function get_wordpress_latest_version(): string { 45 | $version = $this->get_latest_version_info( 'current' ); 46 | 47 | return $version ?? get_bloginfo( 'version' ); 48 | } 49 | 50 | /** 51 | * Returns relative WordPress major version. 52 | * 53 | * @since 1.3.1 54 | * 55 | * @param string $version WordPress major version. 56 | * @param int $steps Steps to find relative version. Defaults to 1 for next major version. 57 | * @return string Relative WordPress major version. 58 | */ 59 | protected function get_wordpress_relative_major_version( string $version, int $steps = 1 ): string { 60 | if ( 0 === $steps ) { 61 | return $version; 62 | } 63 | 64 | $new_version = floatval( $version ) + ( 0.1 * $steps ); 65 | 66 | return (string) number_format( $new_version, 1 ); 67 | } 68 | 69 | /** 70 | * Returns specific information. 71 | * 72 | * @since 1.3.1 73 | * 74 | * @param string $key The information key to retrieve. 75 | * @return mixed The requested information. 76 | */ 77 | private function get_latest_version_info( string $key ) { 78 | $info = get_transient( 'wp_plugin_check_latest_version_info' ); 79 | 80 | if ( false === $info ) { 81 | $response = wp_remote_get( 'https://api.wordpress.org/core/version-check/1.7/' ); 82 | 83 | if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) { 84 | $body = json_decode( wp_remote_retrieve_body( $response ), true ); 85 | 86 | if ( isset( $body['offers'] ) && ! empty( $body['offers'] ) ) { 87 | $info = reset( $body['offers'] ); 88 | set_transient( 'wp_plugin_check_latest_version_info', $info, DAY_IN_SECONDS ); 89 | } 90 | } 91 | } 92 | 93 | return array_key_exists( $key, $info ) ? $info[ $key ] : null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /patches/@wordpress+env+10.17.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@wordpress/env/lib/init-config.js b/node_modules/@wordpress/env/lib/init-config.js 2 | index 318bcae..8628137 100644 3 | --- a/node_modules/@wordpress/env/lib/init-config.js 4 | +++ b/node_modules/@wordpress/env/lib/init-config.js 5 | @@ -244,14 +244,6 @@ RUN export COMPOSER_HASH=\`curl -sS https://composer.github.io/installer.sig\` & 6 | RUN php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer 7 | RUN rm /tmp/composer-setup.php`; 8 | 9 | - // Install any Composer packages we might need globally. 10 | - // Make sure to do this as the user and ensure the binaries are available in the $PATH. 11 | - dockerFileContent += ` 12 | -USER $HOST_UID:$HOST_GID 13 | -ENV PATH="\${PATH}:/home/$HOST_USERNAME/.composer/vendor/bin" 14 | -RUN composer global require --dev phpunit/phpunit:"^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" 15 | -USER root`; 16 | - 17 | return dockerFileContent; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /phpcs-sniffs/.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | phpunit.xml 4 | phpcs.xml 5 | .phpcs.xml 6 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/DiscouragedFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | array( 36 | 'version' => '4.6', 37 | ), 38 | ); 39 | 40 | /** 41 | * Groups of functions to discourage. 42 | * 43 | * @since 1.6.0 44 | * 45 | * @return array 46 | */ 47 | public function getGroups() { 48 | // Make sure all array keys are lowercase. 49 | $this->discouraged_functions = array_change_key_case( $this->discouraged_functions, \CASE_LOWER ); 50 | 51 | return array( 52 | 'discouraged_functions' => array( 53 | 'functions' => array_keys( $this->discouraged_functions ), 54 | ), 55 | ); 56 | } 57 | 58 | /** 59 | * Process a matched token. 60 | * 61 | * @since 1.6.0 62 | * 63 | * @param int $stackPtr The position of the current token in the stack. 64 | * @param string $group_name The name of the group which was matched. Will always be 'discouraged_functions'. 65 | * @param string $matched_content The token content (function name) which was matched in lowercase. 66 | * 67 | * @return void 68 | */ 69 | public function process_matched_token( $stackPtr, $group_name, $matched_content ) { 70 | $this->set_minimum_wp_version(); 71 | 72 | $message = '%s() has been discouraged since WordPress version %s.'; 73 | 74 | $data = array( 75 | $this->tokens[ $stackPtr ]['content'], 76 | $this->discouraged_functions[ $matched_content ]['version'], 77 | ); 78 | 79 | if ( ! empty( $this->discouraged_functions[ $matched_content ]['alt'] ) ) { 80 | $message .= ' Use %s instead.'; 81 | $data[] = $this->discouraged_functions[ $matched_content ]['alt']; 82 | } 83 | 84 | if ( $this->wp_version_compare( $this->discouraged_functions[ $matched_content ]['version'], $this->minimum_wp_version, '<' ) ) { 85 | MessageHelper::addMessage( 86 | $this->phpcsFile, 87 | $message, 88 | $stackPtr, 89 | false, 90 | MessageHelper::stringToErrorcode( $matched_content . 'Found' ), 91 | $data 92 | ); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/EnqueuedResourceOffloadingSniff.php: -------------------------------------------------------------------------------- 1 | Key is function name, value irrelevant. 45 | */ 46 | protected $target_functions = array( 47 | 'wp_register_script' => true, 48 | 'wp_enqueue_script' => true, 49 | 'wp_register_style' => true, 50 | 'wp_enqueue_style' => true, 51 | ); 52 | 53 | /** 54 | * False + the empty tokens array. 55 | * 56 | * This array is enriched with the $emptyTokens array in the register() method. 57 | * 58 | * @var array 59 | */ 60 | private $false_tokens = array( 61 | \T_FALSE => \T_FALSE, 62 | ); 63 | 64 | /** 65 | * Token codes which are "safe" to accept to determine whether a version would evaluate to `false`. 66 | * 67 | * This array is enriched with the several of the PHPCS token arrays in the register() method. 68 | * 69 | * @var array 70 | */ 71 | private $safe_tokens = array( 72 | \T_NULL => \T_NULL, 73 | \T_FALSE => \T_FALSE, 74 | \T_TRUE => \T_TRUE, 75 | \T_LNUMBER => \T_LNUMBER, 76 | \T_DNUMBER => \T_DNUMBER, 77 | \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, 78 | \T_START_NOWDOC => \T_START_NOWDOC, 79 | \T_NOWDOC => \T_NOWDOC, 80 | \T_END_NOWDOC => \T_END_NOWDOC, 81 | \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS, 82 | \T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS, 83 | \T_STRING_CONCAT => \T_STRING_CONCAT, 84 | ); 85 | 86 | /** 87 | * Returns an array of tokens this test wants to listen for. 88 | * 89 | * Overloads and calls the parent method to allow for adding additional tokens to the $safe_tokens property. 90 | * 91 | * @return array 92 | */ 93 | public function register() { 94 | $this->false_tokens += Tokens::$emptyTokens; 95 | 96 | $this->safe_tokens += Tokens::$emptyTokens; 97 | $this->safe_tokens += Tokens::$assignmentTokens; 98 | $this->safe_tokens += Tokens::$comparisonTokens; 99 | $this->safe_tokens += Tokens::$operators; 100 | $this->safe_tokens += Tokens::$booleanOperators; 101 | $this->safe_tokens += Tokens::$castTokens; 102 | 103 | return parent::register(); 104 | } 105 | 106 | /** 107 | * Process the parameters of a matched function. 108 | * 109 | * @since 1.1.0 110 | * 111 | * @param int $stackPtr The position of the current token in the stack. 112 | * @param string $group_name The name of the group which was matched. 113 | * @param string $matched_content The token content (function name) which was matched 114 | * in lowercase. 115 | * @param array $parameters Array with information about the parameters. 116 | * 117 | * @return void 118 | */ 119 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 120 | $src_param = PassedParameters::getParameterFromStack( $parameters, 2, 'src' ); 121 | 122 | if ( false === $src_param ) { 123 | return; 124 | } 125 | 126 | // Known offloading services. 127 | $look_known_offloading_services = array( 128 | 'code\.jquery\.com', 129 | '(?phpcsFile->findNext( Tokens::$emptyTokens, $src_param['start'], ( $src_param['end'] + 1 ), true ); 160 | if ( false === $error_ptr ) { 161 | $error_ptr = $src_param['start']; 162 | } 163 | 164 | $type = 'script'; 165 | if ( strpos( $matched_content, '_style' ) !== false ) { 166 | $type = 'style'; 167 | } 168 | 169 | $src_string = $src_param['clean']; 170 | 171 | $matches = array(); 172 | if ( preg_match( $pattern, $src_string, $matches, PREG_OFFSET_CAPTURE ) > 0 ) { 173 | $this->phpcsFile->addError( 174 | 'Found call to %s() with external resource. Offloading %ss to your servers or any remote service is disallowed.', 175 | $error_ptr, 176 | 'OffloadedContent', 177 | array( $matched_content, $type ) 178 | ); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/ImageFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | tokens[ $stackPtr ]['content']; 50 | if ( \T_INLINE_HTML !== $this->tokens[ $stackPtr ]['code'] ) { 51 | try { 52 | $end_ptr = TextStrings::getEndOfCompleteTextString( $this->phpcsFile, $stackPtr ); 53 | $content = TextStrings::getCompleteTextString( $this->phpcsFile, $stackPtr ); 54 | } catch ( RuntimeException $e ) { 55 | // Parse error/live coding. 56 | return; 57 | } 58 | } 59 | 60 | if ( preg_match_all( '#]*(?<=src=)#', $content, $matches, \PREG_OFFSET_CAPTURE ) > 0 ) { 61 | foreach ( $matches[0] as $match ) { 62 | $this->phpcsFile->addWarning( 63 | 'Images should be added using wp_get_attachment_image() or similar functions', 64 | $this->find_token_in_multiline_string( $stackPtr, $content, $match[1] ), 65 | 'NonEnqueuedImage' 66 | ); 67 | } 68 | } 69 | 70 | return ( $end_ptr + 1 ); 71 | } 72 | 73 | /** 74 | * Find the exact token on which the error should be reported for multi-line strings. 75 | * 76 | * @param int $stackPtr The position of the current token in the stack. 77 | * @param string $content The complete, potentially multi-line, text string. 78 | * @param int $match_offset The offset within the content at which the match was found. 79 | * 80 | * @return int The stack pointer to the token containing the start of the match. 81 | */ 82 | private function find_token_in_multiline_string( $stackPtr, $content, $match_offset ) { 83 | $newline_count = 0; 84 | if ( $match_offset > 0 ) { 85 | $newline_count = substr_count( $content, "\n", 0, $match_offset ); 86 | } 87 | 88 | // Account for heredoc/nowdoc text starting at the token *after* the opener. 89 | if ( isset( Tokens::$heredocTokens[ $this->tokens[ $stackPtr ]['code'] ] ) === true ) { 90 | ++$newline_count; 91 | } 92 | 93 | return ( $stackPtr + $newline_count ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/LocalhostSniff.php: -------------------------------------------------------------------------------- 1 | tokens[ $stackPtr ]['content']; 47 | 48 | if ( false === stripos( $content, '//' ) ) { 49 | return; 50 | } 51 | 52 | if ( preg_match_all( '#https?:\/\/(localhost|127.0.0.1|(.*\.local(host)?))\/#', $content, $matches, PREG_OFFSET_CAPTURE ) > 0 ) { 53 | foreach ( $matches[0] as $match ) { 54 | $this->phpcsFile->addError( 55 | 'Do not use Localhost/127.0.0.1 in your code. Found: %s', 56 | $this->find_token_in_multiline_string( $stackPtr, $content, $match[1] ), 57 | 'Found', 58 | array( $match[0] ) 59 | ); 60 | } 61 | } 62 | 63 | return ( $end_ptr + 1 ); 64 | } 65 | 66 | /** 67 | * Find the exact token on which the error should be reported for multi-line strings. 68 | * 69 | * @since 1.3.0 70 | * 71 | * @param int $stackPtr The position of the current token in the stack. 72 | * @param string $content The complete, potentially multi-line, text string. 73 | * @param int $match_offset The offset within the content at which the match was found. 74 | * @return int The stack pointer to the token containing the start of the match. 75 | */ 76 | private function find_token_in_multiline_string( $stackPtr, $content, $match_offset ) { 77 | $newline_count = 0; 78 | if ( $match_offset > 0 ) { 79 | $newline_count = substr_count( $content, "\n", 0, $match_offset ); 80 | } 81 | 82 | // Account for heredoc/nowdoc text starting at the token *after* the opener. 83 | if ( isset( Tokens::$heredocTokens[ $this->tokens[ $stackPtr ]['code'] ] ) === true ) { 84 | ++$newline_count; 85 | } 86 | 87 | return ( $stackPtr + $newline_count ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/OffloadingSniff.php: -------------------------------------------------------------------------------- 1 | tokens[ $stackPtr ]['content']; 51 | 52 | if ( \T_INLINE_HTML !== $this->tokens[ $stackPtr ]['code'] ) { 53 | try { 54 | $end_ptr = TextStrings::getEndOfCompleteTextString( $this->phpcsFile, $stackPtr ); 55 | $content = TextStrings::getCompleteTextString( $this->phpcsFile, $stackPtr ); 56 | } catch ( RuntimeException $e ) { 57 | // Parse error/live coding. 58 | return; 59 | } 60 | } 61 | 62 | if ( empty( trim( $content ) ) ) { 63 | return; 64 | } 65 | 66 | // Only match HTML markup not arbitrary strings, as those could be covered by EnqueuedResourceOffloadingSniff already. 67 | 68 | if ( 69 | false === strpos( $content, ' 0 ) { 114 | foreach ( $matches[0] as $match ) { 115 | $this->phpcsFile->addError( 116 | 'Offloading images, js, css, and other scripts to your servers or any remote service is disallowed.', 117 | $this->find_token_in_multiline_string( $stackPtr, $content, $match[1] ), 118 | 'OffloadedContent' 119 | ); 120 | } 121 | return ( $end_ptr + 1 ); 122 | } 123 | 124 | // Known offloading extensions. 125 | $look_known_offloading_ext = array( 126 | 'css', 127 | 'svg', 128 | 'jpg', 129 | 'jpeg', 130 | 'gif', 131 | 'png', 132 | 'webm', 133 | 'mp4', 134 | 'mpg', 135 | 'mpeg', 136 | 'mp3', 137 | 'json', 138 | ); 139 | 140 | $offloading_ext = '\.' . implode( '|\.', $look_known_offloading_ext ); 141 | $pattern = '/(https?:\/\/[www\.]?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*(' . $offloading_ext . '){1})[\/]?([\?|#]{1}[-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)?[\s|\'|"]/'; 142 | 143 | $matches = array(); 144 | if ( preg_match_all( $pattern, $content, $matches, PREG_OFFSET_CAPTURE ) > 0 ) { 145 | foreach ( $matches[0] as $match ) { 146 | $this->phpcsFile->addError( 147 | 'Offloading images, js, css, and other scripts to your servers or any remote service is disallowed.', 148 | $this->find_token_in_multiline_string( $stackPtr, $content, $match[1] ), 149 | 'OffloadedContent' 150 | ); 151 | } 152 | } 153 | 154 | return ( $end_ptr + 1 ); 155 | } 156 | 157 | /** 158 | * Find the exact token on which the error should be reported for multi-line strings. 159 | * 160 | * @param int $stackPtr The position of the current token in the stack. 161 | * @param string $content The complete, potentially multi-line, text string. 162 | * @param int $match_offset The offset within the content at which the match was found. 163 | * 164 | * @return int The stack pointer to the token containing the start of the match. 165 | */ 166 | private function find_token_in_multiline_string( $stackPtr, $content, $match_offset ) { 167 | $newline_count = 0; 168 | if ( $match_offset > 0 ) { 169 | $newline_count = substr_count( $content, "\n", 0, $match_offset ); 170 | } 171 | 172 | // Account for heredoc/nowdoc text starting at the token *after* the opener. 173 | if ( isset( Tokens::$heredocTokens[ $this->tokens[ $stackPtr ]['code'] ] ) === true ) { 174 | ++$newline_count; 175 | } 176 | 177 | return ( $stackPtr + $newline_count ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/RequiredFunctionParametersSniff.php: -------------------------------------------------------------------------------- 1 | > Function name as key, array with target parameter and name as value. 32 | */ 33 | protected $target_functions = array( 34 | 'parse_str' => array( 35 | 'position' => 2, 36 | 'name' => 'result', 37 | ), 38 | ); 39 | 40 | /** 41 | * Processes this test, when one of its tokens is encountered. 42 | * 43 | * @since 1.3.0 44 | * 45 | * @param int $stackPtr The position of the current token in the stack. 46 | * @return int|void Integer stack pointer to skip forward or void to continue normal file processing. 47 | */ 48 | public function process_token( $stackPtr ) { 49 | if ( isset( $this->target_functions[ strtolower( $this->tokens[ $stackPtr ]['content'] ) ] ) ) { 50 | // Disallow excluding function groups for this sniff. 51 | $this->exclude = array(); 52 | 53 | return parent::process_token( $stackPtr ); 54 | } 55 | } 56 | 57 | /** 58 | * Process the parameters of a matched function call. 59 | * 60 | * @since 1.3.0 61 | * 62 | * @param int $stackPtr The position of the current token in the stack. 63 | * @param string $group_name The name of the group which was matched. 64 | * @param string $matched_content The token content (function name) which was matched in lowercase. 65 | * @param array $parameters Array with information about the parameters. 66 | * @return void 67 | */ 68 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 69 | $target_param = $this->target_functions[ $matched_content ]; 70 | 71 | $found_param = PassedParameters::getParameterFromStack( $parameters, $target_param['position'], $target_param['name'] ); 72 | 73 | if ( false === $found_param ) { 74 | $error_code = MessageHelper::stringToErrorCode( $matched_content . '_' . $target_param['name'], true ); 75 | 76 | $this->phpcsFile->addError( 77 | 'The "%s" parameter for function %s() is missing.', 78 | $stackPtr, 79 | $error_code . 'Missing', 80 | array( $target_param['name'], $matched_content ) 81 | ); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/SettingSanitizationSniff.php: -------------------------------------------------------------------------------- 1 | Key is function name, value irrelevant. 41 | */ 42 | 43 | protected $target_functions = array( 44 | 'register_setting' => true, 45 | ); 46 | 47 | /** 48 | * Process the parameters of a matched function. 49 | * 50 | * @since 1.4.0 51 | * 52 | * @param int $stackPtr The position of the current token in the stack. 53 | * @param string $group_name The name of the group which was matched. 54 | * @param string $matched_content The token content (function name) which was matched in lowercase. 55 | * @param array $parameters Array with information about the parameters. 56 | * 57 | * @return void 58 | */ 59 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 60 | $third_param = PassedParameters::getParameterFromStack( $parameters, 3, 'args' ); 61 | $error_code = MessageHelper::stringToErrorCode( $matched_content, true ); 62 | 63 | if ( false === $third_param ) { 64 | $this->phpcsFile->addError( 65 | 'Sanitization missing for %s().', 66 | $stackPtr, 67 | $error_code . 'Missing', 68 | array( $matched_content ) 69 | ); 70 | 71 | return; 72 | } 73 | 74 | $content = TextStrings::stripQuotes( $third_param['clean'] ); 75 | 76 | if ( is_numeric( $content ) || in_array( strtolower( $content ), array( 'true', 'false', 'null' ), true ) ) { 77 | $this->phpcsFile->addError( 78 | 'Invalid sanitization provided for %s().', 79 | $stackPtr, 80 | $error_code . 'Invalid', 81 | array( $matched_content ) 82 | ); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Tests/AbstractSniffUnitTest.php: -------------------------------------------------------------------------------- 1 | get_sniff_fqcn(); 79 | if ( ! isset( $current_ruleset->sniffs[ $sniff_fqcn ] ) ) { 80 | throw new \RuntimeException( $error_message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- this is non-production code. 81 | } 82 | 83 | $sniff = $current_ruleset->sniffs[ $sniff_fqcn ]; 84 | $this->set_sniff_parameters( $sniff ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/DiscouragedFunctionsUnitTest.inc: -------------------------------------------------------------------------------- 1 | => 23 | */ 24 | public function getErrorList() { 25 | return array(); 26 | } 27 | 28 | /** 29 | * Returns the lines where warnings should occur. 30 | * 31 | * @return array => 32 | */ 33 | public function getWarningList() { 34 | return array( 35 | 6 => 1, 36 | ); 37 | } 38 | 39 | /** 40 | * Returns the fully qualified class name (FQCN) of the sniff. 41 | * 42 | * @return string The fully qualified class name of the sniff. 43 | */ 44 | protected function get_sniff_fqcn() { 45 | return DiscouragedFunctionsSniff::class; 46 | } 47 | 48 | /** 49 | * Sets the parameters for the sniff. 50 | * 51 | * @throws \RuntimeException If unable to set the ruleset parameters required for the test. 52 | * 53 | * @param Sniff $sniff The sniff being tested. 54 | */ 55 | public function set_sniff_parameters( Sniff $sniff ) { 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/EnqueuedResourceOffloadingUnitTest.inc: -------------------------------------------------------------------------------- 1 | => 23 | */ 24 | public function getErrorList() { 25 | return array( 26 | 5 => 1, 27 | 13 => 1, 28 | ); 29 | } 30 | 31 | /** 32 | * Returns the lines where warnings should occur. 33 | * 34 | * @return array => 35 | */ 36 | public function getWarningList() { 37 | return array(); 38 | } 39 | 40 | /** 41 | * Returns the fully qualified class name (FQCN) of the sniff. 42 | * 43 | * @return string The fully qualified class name of the sniff. 44 | */ 45 | protected function get_sniff_fqcn() { 46 | return EnqueuedResourceOffloadingSniff::class; 47 | } 48 | 49 | /** 50 | * Sets the parameters for the sniff. 51 | * 52 | * @throws \RuntimeException If unable to set the ruleset parameters required for the test. 53 | * 54 | * @param Sniff $sniff The sniff being tested. 55 | */ 56 | public function set_sniff_parameters( Sniff $sniff ) { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/ImageFunctionsUnitTest.inc: -------------------------------------------------------------------------------- 1 | 2 | 3 | img src="https://example.com/image.jpeg" />'; 8 | 9 | $double_quoted = ""; 10 | 11 | $double_quoted = ""; 12 | 13 | $content = <<<'EOD' 14 | 15 | EOD; 16 | 17 | // Test multi-line text string. 18 | echo ''; 20 | 21 | // Test multi-line text string with multiple issues. 22 | echo ' 24 | 25 | 27 | '; 28 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/ImageFunctionsUnitTest.php: -------------------------------------------------------------------------------- 1 | => 21 | */ 22 | public function getErrorList() { 23 | return array(); 24 | } 25 | 26 | /** 27 | * Returns the lines where warnings should occur. 28 | * 29 | * @return array => 30 | */ 31 | public function getWarningList() { 32 | return array( 33 | 1 => 1, 34 | 7 => 1, 35 | 9 => 1, 36 | 11 => 1, 37 | 14 => 1, 38 | 18 => 1, 39 | 22 => 1, 40 | 24 => 1, 41 | 25 => 1, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/LocalhostUnitTest.inc: -------------------------------------------------------------------------------- 1 | array( 15 | 'link' => esc_url( 'https://staging.local/resources' ), // Error. 16 | ), 17 | ); 18 | 19 | // This file contains https://localhost/test-plugin-localhost-with-errors url. Good. 20 | 21 | /* 22 | * Another comment https://localhost/test-plugin-localhost-with-errors URL. 23 | */ 24 | 25 | $multiple_urls = 'Custom URL https://docker.localhost/example and https://localhost/example.php here.'; // Error. 26 | ?> 27 | 28 | " alt="" /> 29 | 30 |

URL in inline HTML is http://localhost/example2.php bad.

31 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/LocalhostUnitTest.php: -------------------------------------------------------------------------------- 1 | => 23 | */ 24 | public function getErrorList() { 25 | return array( 26 | 3 => 1, 27 | 4 => 1, 28 | 5 => 1, 29 | 6 => 1, 30 | 8 => 1, 31 | 9 => 1, 32 | 10 => 1, 33 | 11 => 1, 34 | 15 => 1, 35 | 25 => 2, 36 | 28 => 1, 37 | 30 => 1, 38 | ); 39 | } 40 | 41 | /** 42 | * Returns the lines where warnings should occur. 43 | * 44 | * @return array => 45 | */ 46 | public function getWarningList() { 47 | return array(); 48 | } 49 | 50 | /** 51 | * Returns the fully qualified class name (FQCN) of the sniff. 52 | * 53 | * @return string The fully qualified class name of the sniff. 54 | */ 55 | protected function get_sniff_fqcn() { 56 | return LocalhostSniff::class; 57 | } 58 | 59 | /** 60 | * Sets the parameters for the sniff. 61 | * 62 | * @throws \RuntimeException If unable to set the ruleset parameters required for the test. 63 | * 64 | * @param Sniff $sniff The sniff being tested. 65 | */ 66 | public function set_sniff_parameters( Sniff $sniff ) { 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/OffloadingUnitTest.inc: -------------------------------------------------------------------------------- 1 | 2 | 3 |