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

Plugin_Header_Fields_Check.php in Plugin Check (PCP) 1.5.0, at includes/Checker/Checks/Plugin_Repo/Plugin_Header_Fields_Check.php

551 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Plugin_Header_Fields_Check.
4 *
5 * @package plugin-check
6 */
7
8 namespace WordPress\Plugin_Check\Checker\Checks\Plugin_Repo;
9
10 use Exception;
11 use WordPress\Plugin_Check\Checker\Check_Categories;
12 use WordPress\Plugin_Check\Checker\Check_Result;
13 use WordPress\Plugin_Check\Checker\Static_Check;
14 use WordPress\Plugin_Check\Traits\Amend_Check_Result;
15 use WordPress\Plugin_Check\Traits\License_Utils;
16 use WordPress\Plugin_Check\Traits\Stable_Check;
17 use WordPress\Plugin_Check\Traits\Version_Utils;
18
19 /**
20 * Check for plugin header fields.
21 *
22 * @since 1.2.0
23 */
24 class Plugin_Header_Fields_Check implements Static_Check {
25
26 use Amend_Check_Result;
27 use License_Utils;
28 use Stable_Check;
29 use Version_Utils;
30
31 /**
32 * Gets the categories for the check.
33 *
34 * Every check must have at least one category.
35 *
36 * @since 1.2.0
37 *
38 * @return array The categories for the check.
39 */
40 public function get_categories() {
41 return array( Check_Categories::CATEGORY_PLUGIN_REPO );
42 }
43
44 /**
45 * Amends the given result by running the check on the associated plugin.
46 *
47 * @since 1.2.0
48 *
49 * @param Check_Result $result The check result to amend, including the plugin context to check.
50 *
51 * @throws Exception Thrown when the check fails with a critical error (unrelated to any errors detected as part of the check).
52 *
53 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
54 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
55 * @SuppressWarnings(PHPMD.NPathComplexity)
56 */
57 public function run( Check_Result $result ) {
58 $plugin_main_file = $result->plugin()->main_file();
59
60 $labels = array(
61 'Name' => 'Plugin Name',
62 'PluginURI' => 'Plugin URI',
63 'Version' => 'Version',
64 'Description' => 'Description',
65 'Author' => 'Author',
66 'AuthorURI' => 'Author URI',
67 'TextDomain' => 'Text Domain',
68 'DomainPath' => 'Domain Path',
69 'Network' => 'Network',
70 'RequiresWP' => 'Requires at least',
71 'RequiresPHP' => 'Requires PHP',
72 'UpdateURI' => 'Update URI',
73 'RequiresPlugins' => 'Requires Plugins',
74 'License' => 'License',
75 'LicenseURI' => 'License URI',
76 );
77
78 $restricted_labels = array(
79 'RestrictedLabel' => 'Restricted Label',
80 ); // Reserved for future use.
81
82 $plugin_header = $this->get_plugin_data( $plugin_main_file, array_merge( $labels, $restricted_labels ) );
83
84 if ( ! empty( $plugin_header['Name'] ) ) {
85 if ( in_array( $plugin_header['Name'], array( 'Plugin Name', 'My Basics Plugin' ), true ) ) {
86 $this->add_result_warning_for_file(
87 $result,
88 sprintf(
89 /* translators: %s: plugin header field */
90 __( 'The "%s" header in the plugin file is not valid.', 'plugin-check' ),
91 esc_html( $labels['Name'] )
92 ),
93 'plugin_header_invalid_plugin_name',
94 $plugin_main_file,
95 0,
96 0,
97 '',
98 6
99 );
100 } else {
101 $valid_chars_count = preg_match_all( '/[a-z0-9]/i', $plugin_header['Name'] );
102
103 if ( intval( $valid_chars_count ) < 5 ) {
104 $this->add_result_error_for_file(
105 $result,
106 sprintf(
107 /* translators: %s: plugin header field */
108 __( 'The "%s" header in the plugin file is not valid. It needs to contain at least 5 latin letters (a-Z) and/or numbers. This is necessary because the initial plugin slug is generated from the name.', 'plugin-check' ),
109 esc_html( $labels['Name'] )
110 ),
111 'plugin_header_unsupported_plugin_name',
112 $plugin_main_file,
113 0,
114 0,
115 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields',
116 7
117 );
118 }
119 }
120 }
121
122 if ( ! empty( $plugin_header['PluginURI'] ) ) {
123 if ( true !== $this->is_valid_url( $plugin_header['PluginURI'] ) ) {
124 $this->add_result_warning_for_file(
125 $result,
126 sprintf(
127 /* translators: %s: plugin header field */
128 __( 'The "%s" header in the plugin file is not valid.', 'plugin-check' ),
129 esc_html( $labels['PluginURI'] )
130 ),
131 'plugin_header_invalid_plugin_uri',
132 $plugin_main_file,
133 0,
134 0,
135 '',
136 6
137 );
138 } elseif ( preg_match( '/\/\/(example\.com|example\.net|example\.org)\//', $plugin_header['PluginURI'], $matches ) ) {
139 $this->add_result_warning_for_file(
140 $result,
141 sprintf(
142 /* translators: 1: plugin header field, 2: domain */
143 __( 'The "%1$s" header in the plugin file is not valid. Discouraged domain "%2$s" found. This is the homepage of the plugin, which should be a unique URL, preferably on your own website. ', 'plugin-check' ),
144 esc_html( $labels['PluginURI'] ),
145 esc_html( $matches[1] ),
146 ),
147 'plugin_header_invalid_plugin_uri_domain',
148 $plugin_main_file,
149 0,
150 0,
151 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields',
152 6
153 );
154 }
155 }
156
157 if ( empty( $plugin_header['Description'] ) ) {
158 $this->add_result_error_for_file(
159 $result,
160 sprintf(
161 /* translators: %s: plugin header field */
162 __( 'The "%s" header is missing in the plugin file.', 'plugin-check' ),
163 esc_html( $labels['Description'] )
164 ),
165 'plugin_header_missing_plugin_description',
166 $plugin_main_file,
167 0,
168 0,
169 __( 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/', 'plugin-check' ),
170 7
171 );
172 } else {
173 if (
174 str_contains( $plugin_header['Description'], 'This is a short description of what the plugin does' )
175 || str_contains( $plugin_header['Description'], 'Here is a short description of the plugin' )
176 || str_contains( $plugin_header['Description'], 'Handle the basics with this plugin' )
177 ) {
178 $this->add_result_warning_for_file(
179 $result,
180 sprintf(
181 /* translators: %s: plugin header field */
182 __( 'The "%s" header in the plugin file should not contain default text.', 'plugin-check' ),
183 esc_html( $labels['Description'] )
184 ),
185 'plugin_header_invalid_plugin_description',
186 $plugin_main_file,
187 0,
188 0,
189 '',
190 6
191 );
192 }
193 }
194
195 if ( empty( $plugin_header['Version'] ) ) {
196 $this->add_result_error_for_file(
197 $result,
198 sprintf(
199 /* translators: %s: plugin header field */
200 __( 'The "%s" header is missing in the plugin file.', 'plugin-check' ),
201 esc_html( $labels['Version'] )
202 ),
203 'plugin_header_missing_plugin_version',
204 $plugin_main_file,
205 0,
206 0,
207 __( 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/', 'plugin-check' ),
208 7
209 );
210 } else {
211 if ( ! preg_match( '/^[a-z0-9.-]+$/i', $plugin_header['Version'] ) ) {
212 $this->add_result_error_for_file(
213 $result,
214 sprintf(
215 /* translators: %s: plugin header field */
216 __( 'The "%s" header in the plugin file should only contain numbers, letters, periods, and hyphens.', 'plugin-check' ),
217 esc_html( $labels['Version'] )
218 ),
219 'plugin_header_invalid_plugin_version',
220 $plugin_main_file,
221 0,
222 0,
223 __( 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/', 'plugin-check' ),
224 7
225 );
226 }
227 }
228
229 if ( ! empty( $plugin_header['AuthorURI'] ) ) {
230 if ( true !== $this->is_valid_url( $plugin_header['AuthorURI'] ) ) {
231 $this->add_result_error_for_file(
232 $result,
233 sprintf(
234 /* translators: %s: plugin header field */
235 __( 'The "%s" header in the plugin file is not valid.', 'plugin-check' ),
236 esc_html( $labels['AuthorURI'] )
237 ),
238 'plugin_header_invalid_author_uri',
239 $plugin_main_file,
240 0,
241 0,
242 '',
243 7
244 );
245 }
246 }
247
248 if ( ! empty( $plugin_header['Network'] ) ) {
249 if ( 'true' !== strtolower( $plugin_header['Network'] ) ) {
250 $this->add_result_warning_for_file(
251 $result,
252 sprintf(
253 /* translators: %s: plugin header field */
254 __( 'The "%s" header in the plugin file is not valid. Can only be set to true, and should be left out when not needed.', 'plugin-check' ),
255 esc_html( $labels['Network'] )
256 ),
257 'plugin_header_invalid_network',
258 $plugin_main_file,
259 0,
260 0,
261 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields',
262 6
263 );
264 }
265 }
266
267 if ( ! empty( $plugin_header['RequiresWP'] ) ) {
268 $latest_wp_version = $this->get_wordpress_stable_version();
269
270 if ( ! preg_match( '!^\d+\.\d(\.\d+)?$!', $plugin_header['RequiresWP'] ) ) {
271 $previous_wp_version = $this->get_wordpress_relative_major_version( $latest_wp_version, -1 );
272
273 $this->add_result_error_for_file(
274 $result,
275 sprintf(
276 /* translators: 1: plugin header field, 2: Example version 6.7, 3: Example version 6.6 */
277 __( 'The "%1$s" header in the plugin file should only contain a WordPress version such as "%2$s" or "%3$s".', 'plugin-check' ),
278 esc_html( $labels['RequiresWP'] ),
279 esc_html( $latest_wp_version ),
280 esc_html( $previous_wp_version )
281 ),
282 'plugin_header_invalid_requires_wp',
283 $plugin_main_file,
284 0,
285 0,
286 '',
287 7
288 );
289 } else {
290 $acceptable_min_wp_version = $this->get_wordpress_relative_major_version( $latest_wp_version, 1 );
291
292 if ( version_compare( $plugin_header['RequiresWP'], $acceptable_min_wp_version, '>' ) ) {
293 $this->add_result_error_for_file(
294 $result,
295 sprintf(
296 /* translators: 1: plugin header field, 2: currently used version */
297 __( '<strong>%1$s: %2$s.</strong><br>The "%1$s" value in your plugin header is not valid. This version of WordPress does not exist (yet).', 'plugin-check' ),
298 esc_html( $labels['RequiresWP'] ),
299 esc_html( $plugin_header['RequiresWP'] )
300 ),
301 'plugin_header_nonexistent_requires_wp',
302 $plugin_main_file,
303 0,
304 0,
305 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields',
306 7
307 );
308 }
309 }
310 }
311 if ( ! empty( $plugin_header['RequiresPHP'] ) ) {
312 if ( ! preg_match( '!^\d+(\.\d+){1,2}$!', $plugin_header['RequiresPHP'] ) ) {
313 $this->add_result_error_for_file(
314 $result,
315 sprintf(
316 /* translators: 1: plugin header field; 2: Example version 5.2.4. 3: Example version 7.0. */
317 __( 'The "%1$s" header in the plugin file should only contain a PHP version such as "%2$s" or "%3$s".', 'plugin-check' ),
318 esc_html( $labels['RequiresPHP'] ),
319 '5.2.4',
320 '7.0'
321 ),
322 'plugin_header_invalid_requires_php',
323 $plugin_main_file,
324 0,
325 0,
326 '',
327 7
328 );
329 }
330 }
331
332 if ( ! empty( $plugin_header['RequiresPlugins'] ) ) {
333 if ( ! preg_match( '/^[a-z0-9-]+(?:,\s*[a-z0-9-]+)*$/', $plugin_header['RequiresPlugins'] ) ) {
334 $this->add_result_error_for_file(
335 $result,
336 sprintf(
337 /* translators: %s: plugin header field */
338 __( 'The "%s" header in the plugin file must contain a comma-separated list of WordPress.org-formatted slugs.', 'plugin-check' ),
339 esc_html( $labels['RequiresPlugins'] )
340 ),
341 'plugin_header_invalid_requires_plugins',
342 $plugin_main_file,
343 0,
344 0,
345 '',
346 7
347 );
348 }
349 }
350
351 if ( empty( $plugin_header['License'] ) ) {
352 $this->add_result_error_for_file(
353 $result,
354 sprintf(
355 /* translators: %s: plugin header field */
356 __( '<strong>Missing "%s" in Plugin Header.</strong><br>Please update your Plugin Header with a valid GPLv2 (or later) compatible license.', 'plugin-check' ),
357 esc_html( $labels['License'] )
358 ),
359 'plugin_header_no_license',
360 $plugin_main_file,
361 0,
362 0,
363 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#no-gpl-compatible-license-declared',
364 9
365 );
366 } else {
367 $plugin_license = $this->get_normalized_license( $plugin_header['License'] );
368 if ( ! $this->is_license_gpl_compatible( $plugin_license ) ) {
369 $this->add_result_error_for_file(
370 $result,
371 sprintf(
372 /* translators: 1: plugin header field, 2: license */
373 __( '<strong>Invalid %1$s: %2$s.</strong><br>Please update your Plugin Header with a valid GPLv2 (or later) compatible license.', 'plugin-check' ),
374 esc_html( $labels['License'] ),
375 esc_html( $plugin_header['License'] )
376 ),
377 'plugin_header_invalid_license',
378 $plugin_main_file,
379 0,
380 0,
381 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#no-gpl-compatible-license-declared',
382 9
383 );
384 }
385 }
386
387 $found_headers = array();
388
389 foreach ( $restricted_labels as $restricted_key => $restricted_label ) {
390 if ( array_key_exists( $restricted_key, $plugin_header ) && ! empty( $plugin_header[ $restricted_key ] ) ) {
391 $found_headers[ $restricted_key ] = $restricted_label;
392 }
393 }
394
395 if ( ! empty( $found_headers ) ) {
396 $this->add_result_error_for_file(
397 $result,
398 sprintf(
399 /* translators: %s: header fields */
400 __( 'Restricted plugin header field(s) found: %s', 'plugin-check' ),
401 "'" . implode( "', '", array_values( $found_headers ) ) . "'"
402 ),
403 'plugin_header_restricted_fields',
404 $plugin_main_file,
405 0,
406 0,
407 '',
408 7
409 );
410 }
411
412 if ( ! $result->plugin()->is_single_file_plugin() ) {
413 if ( ! empty( $plugin_header['TextDomain'] ) ) {
414 $plugin_slug = $result->plugin()->slug();
415
416 if ( $plugin_slug !== $plugin_header['TextDomain'] ) {
417 $this->add_result_warning_for_file(
418 $result,
419 sprintf(
420 /* translators: 1: plugin header field, 2: plugin header text domain, 3: plugin slug */
421 __( 'The "%1$s" header in the plugin file does not match the slug. Found "%2$s", expected "%3$s".', 'plugin-check' ),
422 esc_html( $labels['TextDomain'] ),
423 esc_html( $plugin_header['TextDomain'] ),
424 esc_html( $plugin_slug )
425 ),
426 'textdomain_mismatch',
427 $plugin_main_file,
428 0,
429 0,
430 'https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/',
431 6
432 );
433 }
434 }
435
436 if ( ! empty( $plugin_header['DomainPath'] ) ) {
437 if ( ! str_starts_with( $plugin_header['DomainPath'], '/' ) ) {
438 $this->add_result_warning_for_file(
439 $result,
440 sprintf(
441 /* translators: %s: plugin header field */
442 __( 'The "%s" header in the plugin file must start with forward slash.', 'plugin-check' ),
443 esc_html( $labels['DomainPath'] )
444 ),
445 'plugin_header_invalid_domain_path',
446 $plugin_main_file,
447 0,
448 0,
449 'https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#domain-path',
450 6
451 );
452 }
453
454 $domain_path = trim( $plugin_header['DomainPath'], '/' );
455
456 $target_path = wp_normalize_path( $result->plugin()->path() . $domain_path );
457
458 if ( ! is_dir( $target_path ) ) {
459 $this->add_result_warning_for_file(
460 $result,
461 sprintf(
462 /* translators: 1: plugin header field, 2: domain path */
463 __( 'The "%1$s" header in the plugin file must point to an existing folder. Found: "%2$s"', 'plugin-check' ),
464 esc_html( $labels['DomainPath'] ),
465 $domain_path
466 ),
467 'plugin_header_nonexistent_domain_path',
468 $plugin_main_file,
469 0,
470 0,
471 'https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#domain-path',
472 6
473 );
474 }
475 }
476 }
477 }
478
479 /**
480 * Checks if URL is valid.
481 *
482 * @since 1.2.0
483 *
484 * @param string $url URL.
485 * @return bool true if the URL is valid, otherwise false.
486 */
487 private function is_valid_url( $url ) {
488 if ( filter_var( $url, FILTER_VALIDATE_URL ) !== $url || ! str_starts_with( $url, 'http' ) ) {
489 return false;
490 }
491
492 // Detect duplicated protocol (e.g., "https://http://example.com/").
493 $parsed_url = wp_parse_url( $url );
494 if ( isset( $parsed_url['scheme'] ) && str_contains( substr( $url, strlen( $parsed_url['scheme'] ) + 3 ), '://' ) ) {
495 return false;
496 }
497
498 return true;
499 }
500
501 /**
502 * Parses the plugin contents to retrieve plugin's metadata.
503 *
504 * @since 1.2.0
505 *
506 * @param string $plugin_file Absolute path to the main plugin file.
507 * @param array $default_headers List of headers, in the format `array( 'HeaderKey' => 'Header Name' )`.
508 * @return string[] Array of file header values keyed by header name.
509 */
510 private function get_plugin_data( $plugin_file, $default_headers ) {
511 $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' );
512
513 // If no text domain is defined fall back to the plugin slug.
514 if ( ! $plugin_data['TextDomain'] ) {
515 $plugin_slug = dirname( plugin_basename( $plugin_file ) );
516
517 if ( '.' !== $plugin_slug && ! str_contains( $plugin_slug, '/' ) ) {
518 $plugin_data['TextDomain'] = $plugin_slug;
519 }
520 }
521
522 return $plugin_data;
523 }
524
525 /**
526 * Gets the description for the check.
527 *
528 * Every check must have a short description explaining what the check does.
529 *
530 * @since 1.2.0
531 *
532 * @return string Description.
533 */
534 public function get_description(): string {
535 return __( 'Checks adherence to the Headers requirements.', 'plugin-check' );
536 }
537
538 /**
539 * Gets the documentation URL for the check.
540 *
541 * Every check must have a URL with further information about the check.
542 *
543 * @since 1.2.0
544 *
545 * @return string The documentation URL.
546 */
547 public function get_documentation_url(): string {
548 return __( 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/', 'plugin-check' );
549 }
550 }
551