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_Readme_Check.php

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

964 lines 30.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Plugin_Readme_Check.
4 *
5 * @package plugin-check
6 */
7
8 namespace WordPress\Plugin_Check\Checker\Checks\Plugin_Repo;
9
10 use WordPress\Plugin_Check\Checker\Check_Categories;
11 use WordPress\Plugin_Check\Checker\Check_Result;
12 use WordPress\Plugin_Check\Checker\Checks\Abstract_File_Check;
13 use WordPress\Plugin_Check\Traits\Amend_Check_Result;
14 use WordPress\Plugin_Check\Traits\Find_Readme;
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 use WordPressdotorg\Plugin_Directory\Readme\Parser;
19
20 /**
21 * Check the plugins readme file and contents.
22 *
23 * @since 1.0.0
24 *
25 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
26 */
27 class Plugin_Readme_Check extends Abstract_File_Check {
28
29 use Amend_Check_Result;
30 use Find_Readme;
31 use Stable_Check;
32 use License_Utils;
33 use Version_Utils;
34
35 /**
36 * Gets the categories for the check.
37 *
38 * Every check must have at least one category.
39 *
40 * @since 1.0.0
41 *
42 * @return array The categories for the check.
43 */
44 public function get_categories() {
45 return array( Check_Categories::CATEGORY_PLUGIN_REPO );
46 }
47
48 /**
49 * Check the readme file.
50 *
51 * @since 1.0.0
52 *
53 * @param Check_Result $result The Check Result to amend.
54 * @param array $files Array of plugin files.
55 */
56 protected function check_files( Check_Result $result, array $files ) {
57
58 // Check if single file plugin, then bail early.
59 if ( $result->plugin()->is_single_file_plugin() ) {
60 return;
61 }
62
63 $plugin_relative_path = $result->plugin()->path();
64
65 // Filter the readme files.
66 $readme = $this->filter_files_for_readme( $files, $plugin_relative_path );
67
68 // If the readme file does not exist, add an error and skip other tests.
69 if ( empty( $readme ) ) {
70 $this->add_result_error_for_file(
71 $result,
72 __( 'The plugin readme.txt does not exist.', 'plugin-check' ),
73 'no_plugin_readme',
74 'readme.txt',
75 0,
76 0,
77 '',
78 9
79 );
80
81 return;
82 }
83
84 $readme_file = reset( $readme );
85
86 $parser = new Parser( $readme_file );
87
88 // Check the readme file for plugin name.
89 $this->check_name( $result, $readme_file, $parser );
90
91 // Check the readme file for missing headers.
92 $this->check_headers( $result, $readme_file, $parser );
93
94 // Check the readme file for default text.
95 $this->check_default_text( $result, $readme_file, $parser );
96
97 // Check the readme file for a valid license.
98 $this->check_license( $result, $readme_file, $parser );
99
100 // Check the readme file for a valid version.
101 $this->check_stable_tag( $result, $readme_file, $parser );
102
103 // Check the readme file for upgrade notice.
104 $this->check_upgrade_notice( $result, $readme_file, $parser );
105
106 // Check the readme file for warnings.
107 $this->check_for_warnings( $result, $readme_file, $parser );
108
109 // Check the readme file for donate link.
110 $this->check_for_donate_link( $result, $readme_file, $parser );
111
112 // Check the readme file for contributors.
113 $this->check_for_contributors( $result, $readme_file );
114
115 // Check the readme file for requires headers.
116 $this->check_requires_headers( $result, $readme_file, $parser );
117 }
118
119 /**
120 * Checks the readme file for plugin name.
121 *
122 * @since 1.0.0
123 *
124 * @param Check_Result $result The Check Result to amend.
125 * @param string $readme_file Readme file.
126 * @param Parser $parser The Parser object.
127 */
128 private function check_name( Check_Result $result, string $readme_file, Parser $parser ) {
129 if ( isset( $parser->warnings['invalid_plugin_name_header'] ) && false === $parser->name ) {
130 $this->add_result_error_for_file(
131 $result,
132 sprintf(
133 /* translators: %s: Example plugin name header */
134 __( 'Plugin name header in your readme is missing or invalid. Please update your readme with a valid plugin name header. Eg: "%s"', 'plugin-check' ),
135 '=== Example Name ==='
136 ),
137 'invalid_plugin_name',
138 $readme_file,
139 0,
140 0,
141 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#incomplete-readme',
142 9
143 );
144 } elseif ( empty( $parser->name ) ) {
145 $this->add_result_error_for_file(
146 $result,
147 sprintf(
148 /* translators: %s: Example plugin name header */
149 __( 'We cannot find a plugin name in your readme. Please update your readme with a valid plugin name header. Eg: "%s"', 'plugin-check' ),
150 '=== Example Name ==='
151 ),
152 'empty_plugin_name',
153 $readme_file,
154 0,
155 0,
156 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#incomplete-readme',
157 9
158 );
159 } else {
160 $plugin_data = get_plugin_data( $result->plugin()->main_file(), false, false );
161
162 $plugin_readme_name = html_entity_decode( $parser->name, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
163 $plugin_header_name = html_entity_decode( $plugin_data['Name'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
164
165 if ( $plugin_readme_name !== $plugin_header_name ) {
166 $this->add_result_warning_for_file(
167 $result,
168 sprintf(
169 /* translators: 1: Plugin name, 2: Name in plugin header */
170 __( 'Plugin name "%1$s" is different from the name declared in plugin header "%2$s".', 'plugin-check' ),
171 $plugin_readme_name,
172 $plugin_header_name
173 ),
174 'mismatched_plugin_name',
175 $readme_file,
176 0,
177 0,
178 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#incomplete-readme',
179 7
180 );
181
182 }
183 }
184 }
185
186 /**
187 * Checks the readme file for missing headers.
188 *
189 * @since 1.0.2
190 *
191 * @param Check_Result $result The Check Result to amend.
192 * @param string $readme_file Readme file.
193 * @param Parser $parser The Parser object.
194 *
195 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
196 */
197 private function check_headers( Check_Result $result, string $readme_file, Parser $parser ) {
198 $ignored_warnings = $this->get_ignored_warnings( $parser );
199
200 $fields = array(
201 'tested' => array(
202 'label' => __( 'Tested up to', 'plugin-check' ),
203 'ignore_key' => 'tested_header_ignored',
204 ),
205 'contributors' => array(
206 'label' => __( 'Contributors', 'plugin-check' ),
207 'ignore_key' => 'contributor_ignored',
208 ),
209 );
210
211 $parser_warnings = is_array( $parser->warnings ) ? $parser->warnings : array();
212
213 foreach ( $fields as $field_key => $field ) {
214 if ( ! in_array( $field['ignore_key'], $ignored_warnings, true ) && ! isset( $parser_warnings[ $field['ignore_key'] ] ) ) {
215
216 if ( ! empty( $parser->{$field_key} ) && 'tested' === $field_key ) {
217 list( $tested_upto, ) = explode( '-', $parser->{$field_key} );
218
219 $tested_upto_major = $tested_upto;
220 if ( preg_match( '#^\d.\d#', $tested_upto, $matches ) ) {
221 $tested_upto_major = $matches[0];
222 }
223
224 if ( preg_match( '/^\d+\.\d+\.\d+/', $tested_upto ) ) {
225 $this->add_result_error_for_file(
226 $result,
227 sprintf(
228 /* translators: %s: currently used version */
229 __( '<strong>Tested up to: %1$s</strong><br>The version number should only include major versions %2$s.', 'plugin-check' ),
230 $tested_upto,
231 $tested_upto_major
232 ),
233 'invalid_tested_upto_minor',
234 $readme_file,
235 0,
236 0,
237 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
238 7
239 );
240 }
241
242 $latest_wordpress_version = $this->get_wordpress_stable_version();
243 if ( version_compare( $tested_upto_major, $latest_wordpress_version, '<' ) ) {
244 $this->add_result_error_for_file(
245 $result,
246 sprintf(
247 /* translators: 1: currently used version, 2: latest stable WordPress version, 3: 'Tested up to' */
248 __( '<strong>Tested up to: %1$s &lt; %2$s.</strong><br>The "%3$s" value in your plugin is not set to the current version of WordPress. This means your plugin will not show up in searches, as we require plugins to be compatible and documented as tested up to the most recent version of WordPress.', 'plugin-check' ),
249 $tested_upto_major,
250 $latest_wordpress_version,
251 'Tested up to'
252 ),
253 'outdated_tested_upto_header',
254 $readme_file,
255 0,
256 0,
257 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
258 7
259 );
260 } elseif ( version_compare( $tested_upto_major, number_format( (float) $latest_wordpress_version + 0.1, 1 ), '>' ) ) {
261 $this->add_result_error_for_file(
262 $result,
263 sprintf(
264 /* translators: 1: currently used version, 2: 'Tested up to' */
265 __( '<strong>Tested up to: %1$s.</strong><br>The "%2$s" value in your plugin is not valid. This version of WordPress does not exist (yet).', 'plugin-check' ),
266 $tested_upto_major,
267 'Tested up to'
268 ),
269 'nonexistent_tested_upto_header',
270 $readme_file,
271 0,
272 0,
273 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
274 7
275 );
276 }
277 } else {
278 if ( empty( $parser->{$field_key} ) ) {
279 $this->add_result_error_for_file(
280 $result,
281 sprintf(
282 /* translators: %s: readme header field */
283 __( 'The "%s" header is missing in the readme file.', 'plugin-check' ),
284 $field['label']
285 ),
286 'missing_readme_header_' . $field_key,
287 $readme_file,
288 0,
289 0,
290 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information'
291 );
292 }
293 }
294 }
295 }
296 }
297
298 /**
299 * Checks the readme file for default text.
300 *
301 * @since 1.0.0
302 *
303 * @param Check_Result $result The Check Result to amend.
304 * @param string $readme_file Readme file.
305 * @param Parser $parser The Parser object.
306 */
307 private function check_default_text( Check_Result $result, string $readme_file, Parser $parser ) {
308 $short_description = $parser->short_description;
309 $tags = $parser->tags;
310 $donate_link = $parser->donate_link;
311
312 if (
313 in_array( 'tag1', $tags, true )
314 || str_contains( $short_description, 'Here is a short description of the plugin.' )
315 || str_contains( $donate_link, '//example.com/' )
316 ) {
317 $this->add_result_error_for_file(
318 $result,
319 __( '<strong>The readme appears to contain default text.</strong><br>This means your readme has to have headers as well as a proper description and documentation as to how it works and how one can use it.', 'plugin-check' ),
320 'default_readme_text',
321 $readme_file,
322 0,
323 0,
324 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#incomplete-readme',
325 7
326 );
327 }
328 }
329
330 /**
331 * Checks the readme file for a valid license.
332 *
333 * @since 1.0.0
334 *
335 * @param Check_Result $result The Check Result to amend.
336 * @param string $readme_file Readme file.
337 * @param Parser $parser The Parser object.
338 */
339 private function check_license( Check_Result $result, string $readme_file, Parser $parser ) {
340 $license = $parser->license;
341 $matches_license = array();
342 $plugin_main_file = $result->plugin()->main_file();
343
344 // Filter the readme files.
345 if ( empty( $license ) ) {
346 $this->add_result_error_for_file(
347 $result,
348 sprintf(
349 /* translators: %s: readme header field */
350 __( '<strong>Missing "%s".</strong><br>Please update your readme with a valid GPLv2 (or later) compatible license.', 'plugin-check' ),
351 'License'
352 ),
353 'no_license',
354 $readme_file,
355 0,
356 0,
357 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#no-gpl-compatible-license-declared',
358 9
359 );
360
361 return;
362 } else {
363 $license = $this->get_normalized_license( $license );
364 }
365
366 // Test for a valid SPDX license identifier.
367 if ( ! $this->is_license_valid_identifier( $license ) ) {
368 $this->add_result_error_for_file(
369 $result,
370 __( '<strong>Your plugin has an invalid license declared.</strong><br>Please update your readme with a valid SPDX license identifier.', 'plugin-check' ),
371 'invalid_license',
372 $readme_file,
373 0,
374 0,
375 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#no-gpl-compatible-license-declared',
376 9
377 );
378 }
379
380 $plugin_license = '';
381
382 $pattern = preg_quote( 'License', '/' );
383 $has_license = self::file_preg_match( "/(*ANYCRLF)^.*$pattern\s*:\s*(.*)$/im", array( $plugin_main_file ), $matches_license );
384
385 if ( $has_license ) {
386 $plugin_license = $this->get_normalized_license( $matches_license[1] );
387 }
388
389 // Check different license types.
390 if ( ! empty( $plugin_license ) && ! empty( $license ) && $license !== $plugin_license ) {
391 $this->add_result_error_for_file(
392 $result,
393 __( '<strong>Your plugin has a different license declared in the readme file and plugin header.</strong><br>Please update your readme with a valid GPL license identifier.', 'plugin-check' ),
394 'license_mismatch',
395 $readme_file,
396 0,
397 0,
398 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#declared-license-mismatched',
399 9
400 );
401 }
402 }
403
404 /**
405 * Checks the readme file stable tag.
406 *
407 * @since 1.0.0
408 *
409 * @param Check_Result $result The Check Result to amend.
410 * @param string $readme_file Readme file.
411 * @param Parser $parser The Parser object.
412 */
413 private function check_stable_tag( Check_Result $result, string $readme_file, Parser $parser ) {
414 $stable_tag = $parser->stable_tag;
415
416 if ( empty( $stable_tag ) ) {
417 $this->add_result_error_for_file(
418 $result,
419 sprintf(
420 /* translators: 1: readme header tag, 2: plugin header tag */
421 __( '<strong>Invalid or missing %1$s.</strong><br>Your %1$s is meant to be the stable version of your plugin and it needs to be exactly the same with the %2$s in your main plugin file\'s header. Any mismatch can prevent users from downloading the correct plugin files from WordPress.org.', 'plugin-check' ),
422 'Stable Tag',
423 'Version'
424 ),
425 'no_stable_tag',
426 $readme_file,
427 0,
428 0,
429 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#incorrect-stable-tag',
430 9
431 );
432
433 return;
434 }
435
436 if ( 'trunk' === $stable_tag ) {
437 $this->add_result_error_for_file(
438 $result,
439 sprintf(
440 /* translators: 1: readme header tag, 2: example tag, 3: plugin header tag */
441 __( '<strong>Incorrect %1$s.</strong><br>It\'s recommended not to use "%2$s". Your %1$s is meant to be the stable version of your plugin and it needs to be exactly the same with the %3$s in your main plugin file\'s header. Any mismatch can prevent users from downloading the correct plugin files from WordPress.org.', 'plugin-check' ),
442 'Stable Tag',
443 'Stable Tag: trunk',
444 'Version'
445 ),
446 'trunk_stable_tag',
447 $readme_file,
448 0,
449 0,
450 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#incorrect-stable-tag',
451 9
452 );
453
454 return;
455 }
456
457 // Check the readme file Stable tag against the plugin's main file version.
458 $plugin_data = get_plugin_data( $result->plugin()->main_file() );
459
460 if (
461 ! empty( $plugin_data['Version'] ) &&
462 $stable_tag !== $plugin_data['Version']
463 ) {
464 $this->add_result_error_for_file(
465 $result,
466 sprintf(
467 /* translators: 1: readme header tag, 2: versions comparison, 3: plugin header tag */
468 __( '<strong>Mismatched %1$s: %2$s.</strong><br>Your %1$s is meant to be the stable version of your plugin and it needs to be exactly the same with the %3$s in your main plugin file\'s header. Any mismatch can prevent users from downloading the correct plugin files from WordPress.org.', 'plugin-check' ),
469 'Stable Tag',
470 esc_html( $stable_tag ) . ' != ' . esc_html( $plugin_data['Version'] ),
471 'Version'
472 ),
473 'stable_tag_mismatch',
474 $readme_file,
475 0,
476 0,
477 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#incorrect-stable-tag',
478 9
479 );
480 }
481 }
482
483 /**
484 * Checks the readme file upgrade notice.
485 *
486 * @since 1.0.2
487 *
488 * @param Check_Result $result The Check Result to amend.
489 * @param string $readme_file Readme file.
490 * @param Parser $parser The Parser object.
491 */
492 private function check_upgrade_notice( Check_Result $result, string $readme_file, Parser $parser ) {
493 $notices = $parser->upgrade_notice;
494
495 $maximum_characters = 300;
496
497 // Bail if no upgrade notices.
498 if ( 0 === count( $notices ) ) {
499 return;
500 }
501
502 foreach ( $notices as $version => $notice ) {
503 if ( strlen( $notice ) > $maximum_characters ) {
504 if ( empty( $version ) ) {
505 /* translators: %d: maximum limit. */
506 $message = sprintf( _n( 'The upgrade notice exceeds the limit of %d character.', 'The upgrade notice exceeds the limit of %d characters.', $maximum_characters, 'plugin-check' ), $maximum_characters );
507 } else {
508 /* translators: 1: version, 2: maximum limit. */
509 $message = sprintf( _n( 'The upgrade notice for "%1$s" exceeds the limit of %2$d character.', 'The upgrade notice for "%1$s" exceeds the limit of %2$d characters.', $maximum_characters, 'plugin-check' ), $version, $maximum_characters );
510 }
511
512 $this->add_result_warning_for_file( $result, $message, 'upgrade_notice_limit', $readme_file );
513 }
514 }
515 }
516
517 /**
518 * Checks the readme file warnings.
519 *
520 * @since 1.0.0
521 *
522 * @param Check_Result $result The Check Result to amend.
523 * @param string $readme_file Readme file.
524 * @param Parser $parser The Parser object.
525 *
526 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
527 */
528 private function check_for_warnings( Check_Result $result, string $readme_file, Parser $parser ) {
529 $warnings = $parser->warnings ? $parser->warnings : array();
530
531 // This should be ERROR rather than WARNING. So ignoring here to handle separately.
532 unset( $warnings['invalid_plugin_name_header'] );
533
534 // We handle license check in our own way.
535 unset( $warnings['license_missing'] );
536 unset( $warnings['invalid_license'] );
537 unset( $warnings['unknown_license'] );
538
539 $warning_keys = array_keys( $warnings );
540
541 $latest_wordpress_version = (float) $this->get_wordpress_stable_version();
542
543 $warning_details = array(
544 'contributor_ignored' => array(
545 'message' => sprintf(
546 /* translators: %s: plugin header tag */
547 __( 'One or more contributors listed were ignored. The "%s" field should only contain WordPress.org usernames. Remember that usernames are case-sensitive.', 'plugin-check' ),
548 'Contributors'
549 ),
550 ),
551 'requires_php_header_ignored' => array(
552 'message' => sprintf(
553 /* translators: 1: plugin header tag; 2: Example version 5.2.4. 3: Example version 7.0. */
554 __( 'The "%1$s" field was ignored. This field should only contain a PHP version such as "%2$s" or "%3$s".', 'plugin-check' ),
555 'Requires PHP',
556 '5.2.4',
557 '7.0'
558 ),
559 ),
560 'tested_header_ignored' => array(
561 'message' => sprintf(
562 /* translators: 1: plugin header tag; 2: Example version 5.0. 3: Example version 5.1. */
563 __( 'The "%1$s" field was ignored. This field should only contain a valid WordPress version such as "%2$s" or "%3$s".', 'plugin-check' ),
564 'Tested up to',
565 number_format( $latest_wordpress_version, 1 ),
566 number_format( $latest_wordpress_version + 0.1, 1 )
567 ),
568 'severity' => 7,
569 ),
570 'requires_header_ignored' => array(
571 'message' => sprintf(
572 /* translators: 1: plugin header tag; 2: Example version 5.0. 3: Example version 4.9. */
573 __( 'The "%1$s" field was ignored. This field should only contain a valid WordPress version such as "%2$s" or "%3$s".', 'plugin-check' ),
574 'Requires at least',
575 number_format( $latest_wordpress_version, 1 ),
576 number_format( $latest_wordpress_version - 0.1, 1 )
577 ),
578 ),
579 'too_many_tags' => array(
580 'message' => sprintf(
581 /* translators: %d: maximum tags limit */
582 __( 'One or more tags were ignored. Please limit your plugin to %d tags.', 'plugin-check' ),
583 5
584 ),
585 ),
586 'ignored_tags' => array(
587 'message' => sprintf(
588 /* translators: %s: list of tags not supported */
589 __( 'One or more tags were ignored. The following tags are not permitted: %s', 'plugin-check' ),
590 '"' . implode( '", "', $parser->ignore_tags ) . '"'
591 ),
592 ),
593 'no_short_description_present' => array(
594 'message' => sprintf(
595 /* translators: %s: section title */
596 __( 'The "%s" section is missing. An excerpt was generated from your main plugin description.', 'plugin-check' ),
597 'Short Description'
598 ),
599 ),
600 'trimmed_short_description' => array(
601 'message' => sprintf(
602 /* translators: 1: section title; 2: maximum limit */
603 _n( 'The "%1$s" section is too long and was truncated. A maximum of %2$d character is supported.', 'The "%1$s" section is too long and was truncated. A maximum of %2$d characters is supported.', 150, 'plugin-check' ),
604 'Short Description',
605 150
606 ),
607 'severity' => 6,
608 ),
609 );
610
611 if ( ! empty( $parser->sections ) ) {
612 foreach ( array_keys( $parser->sections ) as $section ) {
613 $max_length = $parser->maximum_field_lengths['section'];
614
615 if ( isset( $parser->maximum_field_lengths[ 'section-' . $section ] ) ) {
616 $max_length = $parser->maximum_field_lengths[ 'section-' . $section ];
617 }
618
619 $section_title = str_replace( '_', ' ', $section );
620
621 $section_title = ( 'faq' === $section ) ? strtoupper( $section_title ) : ucwords( $section_title );
622
623 $warning_details[ 'trimmed_section_' . $section ] = array(
624 'message' => sprintf(
625 /* translators: 1: section title; 2: maximum limit */
626 _n( 'The "%1$s" section is too long and was truncated. A maximum of %2$d character is supported.', 'The "%1$s" section is too long and was truncated. A maximum of %2$d characters is supported.', $max_length, 'plugin-check' ),
627 $section_title,
628 $max_length
629 ),
630 'severity' => 6,
631 );
632 }
633 }
634
635 $ignored_warnings = $this->get_ignored_warnings( $parser );
636
637 $warning_keys = array_diff( $warning_keys, $ignored_warnings );
638
639 if ( ! empty( $warning_keys ) ) {
640 foreach ( $warning_keys as $warning ) {
641 $warning_message = isset( $warning_details[ $warning ]['message'] ) ? $warning_details[ $warning ]['message'] : sprintf(
642 /* translators: %s: warning code */
643 __( 'Readme parser warning detected: %s', 'plugin-check' ),
644 esc_html( $warning )
645 );
646
647 $this->add_result_warning_for_file(
648 $result,
649 $warning_message,
650 'readme_parser_warnings_' . $warning,
651 $readme_file,
652 0,
653 0,
654 '',
655 isset( $warning_details[ $warning ]['severity'] ) ? $warning_details[ $warning ]['severity'] : 5
656 );
657 }
658 }
659 }
660
661 /**
662 * Checks the readme file for donate link.
663 *
664 * @since 1.3.0
665 *
666 * @param Check_Result $result The Check Result to amend.
667 * @param string $readme_file Readme file.
668 * @param Parser $parser The Parser object.
669 */
670 private function check_for_donate_link( Check_Result $result, string $readme_file, Parser $parser ) {
671 $donate_link = $parser->donate_link;
672
673 // Bail if empty donate link.
674 if ( empty( $donate_link ) ) {
675 return;
676 }
677
678 if ( ! ( filter_var( $donate_link, FILTER_VALIDATE_URL ) === $donate_link && str_starts_with( $donate_link, 'http' ) ) ) {
679 $this->add_result_warning_for_file(
680 $result,
681 sprintf(
682 /* translators: %s: plugin header field */
683 __( 'The "%s" header in the readme file must be a valid URL.', 'plugin-check' ),
684 'Donate link'
685 ),
686 'readme_invalid_donate_link',
687 $readme_file,
688 0,
689 0,
690 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
691 6
692 );
693 }
694 }
695
696 /**
697 * Checks the readme file for contributors.
698 *
699 * @since 1.2.0
700 *
701 * @param Check_Result $result The Check Result to amend.
702 * @param string $readme_file Readme file.
703 *
704 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
705 */
706 private function check_for_contributors( Check_Result $result, string $readme_file ) {
707 $regex = '/Contributors\s?:(?:\*\*|\s)?(.*?)\R/';
708
709 $matches = array();
710
711 self::file_preg_match( $regex, array( $readme_file ), $matches );
712
713 // Bail if no "Contributors" found.
714 if ( empty( $matches ) ) {
715 return;
716 }
717
718 $usernames = explode( ',', $matches[1] );
719
720 $usernames = array_unique( array_map( 'trim', $usernames ) );
721
722 $valid = true;
723
724 foreach ( $usernames as $username ) {
725 if ( 1 !== preg_match( '/^[a-z0-9_.\-@ ]+$/i', $username ) ) {
726 $valid = false;
727 break;
728 }
729 }
730
731 if ( ! $valid ) {
732 $this->add_result_warning_for_file(
733 $result,
734 sprintf(
735 /* translators: %s: plugin header field */
736 __( 'The "%s" header in the readme file must be a comma-separated list of WordPress.org-formatted usernames.', 'plugin-check' ),
737 'Contributors'
738 ),
739 'readme_invalid_contributors',
740 $readme_file,
741 0,
742 0,
743 '',
744 6
745 );
746
747 return;
748 }
749
750 $restricted_contributors = $this->get_restricted_contributors();
751
752 $disallowed_contributors = array_keys(
753 array_filter(
754 $restricted_contributors,
755 function ( $value ) {
756 return 'error' === strtolower( $value );
757 }
758 )
759 );
760
761 if ( ! empty( $disallowed_contributors ) ) {
762 $disallowed_usernames = array_intersect( $usernames, $disallowed_contributors );
763
764 if ( ! empty( $disallowed_usernames ) ) {
765 $this->add_result_error_for_file(
766 $result,
767 sprintf(
768 /* translators: 1: plugin header field, 2: usernames */
769 __( 'The "%1$s" header in the readme file contains restricted username(s). Found: %2$s', 'plugin-check' ),
770 'Contributors',
771 '"' . implode( '", "', $disallowed_usernames ) . '"'
772 ),
773 'readme_restricted_contributors',
774 $readme_file,
775 0,
776 0,
777 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
778 7
779 );
780 }
781 }
782
783 $reserved_contributors = array_keys(
784 array_filter(
785 $restricted_contributors,
786 function ( $value ) {
787 return 'warning' === strtolower( $value );
788 }
789 )
790 );
791
792 if ( ! empty( $reserved_contributors ) ) {
793 $reserved_usernames = array_intersect( $usernames, $reserved_contributors );
794
795 if ( ! empty( $reserved_usernames ) ) {
796 $this->add_result_warning_for_file(
797 $result,
798 sprintf(
799 /* translators: 1: plugin header field, 2: usernames */
800 __( 'The "%1$s" header in the readme file contains reserved username(s). Found: %2$s', 'plugin-check' ),
801 'Contributors',
802 '"' . implode( '", "', $reserved_usernames ) . '"'
803 ),
804 'readme_reserved_contributors',
805 $readme_file,
806 0,
807 0,
808 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
809 6
810 );
811 }
812 }
813 }
814
815 /**
816 * Checks the readme file for requires headers.
817 *
818 * @since 1.5.0
819 *
820 * @param Check_Result $result The Check Result to amend.
821 * @param string $readme_file Readme file.
822 * @param Parser $parser The Parser object.
823 */
824 private function check_requires_headers( Check_Result $result, string $readme_file, Parser $parser ) {
825 $ignored_warnings = $this->get_ignored_warnings( $parser );
826
827 $found_warnings = $parser->warnings ? $parser->warnings : array();
828
829 $current_warnings = array_diff( array_keys( $found_warnings ), $ignored_warnings );
830
831 $requires = array(
832 'requires_header_ignored' => array(
833 'label' => 'Requires at least',
834 'key' => 'requires',
835 'header_field' => 'RequiresWP',
836 ),
837 'requires_php_header_ignored' => array(
838 'label' => 'Requires PHP',
839 'key' => 'requires_php',
840 'header_field' => 'RequiresPHP',
841 ),
842 );
843
844 // Find potential requires keys to check.
845 $potential_requires = array_diff( array_keys( $requires ), $current_warnings );
846
847 // Bail if not found.
848 if ( empty( $potential_requires ) ) {
849 return;
850 }
851
852 $plugin_data = get_plugin_data( $result->plugin()->main_file(), false, false );
853
854 foreach ( $potential_requires as $require ) {
855 $readme_value = $parser->{$requires[ $require ]['key']};
856 $plugin_value = $plugin_data[ $requires[ $require ]['header_field'] ];
857
858 if ( ! empty( $readme_value ) && ! empty( $plugin_value ) && $readme_value !== $plugin_value ) {
859 $this->add_result_error_for_file(
860 $result,
861 sprintf(
862 /* translators: 1: readme header tag, 2: versions comparison */
863 __( '<strong>Mismatched %1$s: %2$s.</strong><br>"%1$s" needs to be exactly the same with that in your main plugin file\'s header.', 'plugin-check' ),
864 esc_html( $requires[ $require ]['label'] ),
865 esc_html( $readme_value ) . ' != ' . esc_html( $plugin_value )
866 ),
867 'readme_mismatched_header_' . $requires[ $require ]['key'],
868 $readme_file,
869 0,
870 0,
871 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information'
872 );
873 }
874 }
875 }
876
877 /**
878 * Returns ignored warnings.
879 *
880 * @since 1.0.2
881 *
882 * @param Parser $parser The Parser object.
883 * @return array Ignored warnings.
884 */
885 private function get_ignored_warnings( Parser $parser ) {
886 $ignored_warnings = array(
887 'contributor_ignored',
888 );
889
890 /**
891 * Filter the list of ignored readme parser warnings.
892 *
893 * @since 1.0.2
894 *
895 * @param array $ignored_warnings Array of ignored warning keys.
896 * @param Parser $parser The Parser object.
897 */
898 $ignored_warnings = (array) apply_filters( 'wp_plugin_check_ignored_readme_warnings', $ignored_warnings, $parser );
899
900 return $ignored_warnings;
901 }
902
903 /**
904 * Returns restricted contributors.
905 *
906 * @since 1.4.0
907 *
908 * @return array Restricted contributors.
909 */
910 private function get_restricted_contributors() {
911 $restricted_contributors = array(
912 'username' => 'error',
913 'your-name' => 'error',
914 'your-username' => 'error',
915 'your-wordpress-username' => 'error',
916 'your_wordpress_username' => 'error',
917 'yourusername' => 'error',
918 'yourwordpressusername' => 'error',
919 'wordpressdotorg' => 'warning',
920 );
921
922 /**
923 * Filter the list of restricted contributors.
924 *
925 * @since 1.4.0
926 *
927 * @param array $restricted_contributors Array of restricted contributors with error type.
928 */
929 $restricted_contributors = (array) apply_filters( 'wp_plugin_check_restricted_contributors', $restricted_contributors );
930
931 return $restricted_contributors;
932 }
933
934 /**
935 * Gets the description for the check.
936 *
937 * Every check must have a short description explaining what the check does.
938 *
939 * @since 1.1.0
940 *
941 * @return string Description.
942 */
943 public function get_description(): string {
944 return sprintf(
945 /* translators: %s: readme.txt */
946 __( 'Checks adherence to the %s requirements.', 'plugin-check' ),
947 '<code>readme.txt</code>'
948 );
949 }
950
951 /**
952 * Gets the documentation URL for the check.
953 *
954 * Every check must have a URL with further information about the check.
955 *
956 * @since 1.1.0
957 *
958 * @return string The documentation URL.
959 */
960 public function get_documentation_url(): string {
961 return __( 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/', 'plugin-check' );
962 }
963 }
964