Skip to content

Commit 9d2f354

Browse files
authored
Merge pull request #998 from WordPress/997-improve-url-validation
Improve URL validation for plugin header fields
2 parents 48af985 + d1a2520 commit 9d2f354

6 files changed

Lines changed: 217 additions & 73 deletions

File tree

includes/Checker/Checks/Plugin_Repo/Plugin_Header_Fields_Check.php

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use WordPress\Plugin_Check\Traits\Amend_Check_Result;
1515
use WordPress\Plugin_Check\Traits\License_Utils;
1616
use WordPress\Plugin_Check\Traits\Stable_Check;
17+
use WordPress\Plugin_Check\Traits\URL_Utils;
1718
use WordPress\Plugin_Check\Traits\Version_Utils;
1819

1920
/**
@@ -26,6 +27,7 @@ class Plugin_Header_Fields_Check implements Static_Check {
2627
use Amend_Check_Result;
2728
use License_Utils;
2829
use Stable_Check;
30+
use URL_Utils;
2931
use Version_Utils;
3032

3133
/**
@@ -135,22 +137,25 @@ public function run( Check_Result $result ) {
135137
'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields',
136138
7
137139
);
138-
} elseif ( preg_match( '/\/\/(example\.com|example\.net|example\.org)\//', $plugin_header['PluginURI'], $matches ) ) {
139-
$this->add_result_error_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-
7
153-
);
140+
} else {
141+
$matched_domain = $this->find_discouraged_domain( $plugin_header['PluginURI'] );
142+
if ( $matched_domain ) {
143+
$this->add_result_error_for_file(
144+
$result,
145+
sprintf(
146+
/* translators: 1: plugin header field, 2: domain */
147+
__( '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' ),
148+
esc_html( $labels['PluginURI'] ),
149+
esc_html( $matched_domain )
150+
),
151+
'plugin_header_invalid_plugin_uri_domain',
152+
$plugin_main_file,
153+
0,
154+
0,
155+
'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields',
156+
7
157+
);
158+
}
154159
}
155160
}
156161

@@ -242,6 +247,25 @@ public function run( Check_Result $result ) {
242247
'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields',
243248
7
244249
);
250+
} else {
251+
$matched_domain = $this->find_discouraged_domain( $plugin_header['AuthorURI'] );
252+
if ( $matched_domain ) {
253+
$this->add_result_error_for_file(
254+
$result,
255+
sprintf(
256+
/* translators: 1: plugin header field, 2: domain */
257+
__( 'The "%1$s" header in the plugin file is not valid. Discouraged domain "%2$s" found. This is the author\'s website or profile on another website.', 'plugin-check' ),
258+
esc_html( $labels['AuthorURI'] ),
259+
esc_html( $matched_domain )
260+
),
261+
'plugin_header_invalid_author_uri_domain',
262+
$plugin_main_file,
263+
0,
264+
0,
265+
'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields',
266+
7
267+
);
268+
}
245269
}
246270
}
247271

@@ -494,28 +518,6 @@ public function run( Check_Result $result ) {
494518
}
495519
}
496520

497-
/**
498-
* Checks if URL is valid.
499-
*
500-
* @since 1.2.0
501-
*
502-
* @param string $url URL.
503-
* @return bool true if the URL is valid, otherwise false.
504-
*/
505-
private function is_valid_url( $url ) {
506-
if ( filter_var( $url, FILTER_VALIDATE_URL ) !== $url || ! str_starts_with( $url, 'http' ) ) {
507-
return false;
508-
}
509-
510-
// Detect duplicated protocol (e.g., "https://http://example.com/").
511-
$parsed_url = wp_parse_url( $url );
512-
if ( isset( $parsed_url['scheme'] ) && str_contains( substr( $url, strlen( $parsed_url['scheme'] ) + 3 ), '://' ) ) {
513-
return false;
514-
}
515-
516-
return true;
517-
}
518-
519521
/**
520522
* Parses the plugin contents to retrieve plugin's metadata.
521523
*

includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use WordPress\Plugin_Check\Traits\Find_Readme;
1616
use WordPress\Plugin_Check\Traits\License_Utils;
1717
use WordPress\Plugin_Check\Traits\Stable_Check;
18+
use WordPress\Plugin_Check\Traits\URL_Utils;
1819
use WordPress\Plugin_Check\Traits\Version_Utils;
1920
use WordPressdotorg\Plugin_Directory\Readme\Parser as DotorgParser;
2021

@@ -31,6 +32,7 @@ class Plugin_Readme_Check extends Abstract_File_Check {
3132
use Find_Readme;
3233
use Stable_Check;
3334
use License_Utils;
35+
use URL_Utils;
3436
use Version_Utils;
3537

3638
/**
@@ -678,7 +680,7 @@ private function check_for_donate_link( Check_Result $result, string $readme_fil
678680
return;
679681
}
680682

681-
if ( ! ( filter_var( $donate_link, FILTER_VALIDATE_URL ) === $donate_link && str_starts_with( $donate_link, 'http' ) ) ) {
683+
if ( ! $this->is_valid_url( $donate_link ) ) {
682684
$this->add_result_warning_for_file(
683685
$result,
684686
sprintf(

includes/Traits/URL_Utils.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
/**
3+
* Trait WordPress\Plugin_Check\Traits\URL_Utils
4+
*
5+
* @package plugin-check
6+
*/
7+
8+
namespace WordPress\Plugin_Check\Traits;
9+
10+
/**
11+
* Trait for URL utilities.
12+
*
13+
* @since 1.6.0
14+
*/
15+
trait URL_Utils {
16+
17+
/**
18+
* Checks if URL is valid.
19+
*
20+
* @since 1.6.0
21+
*
22+
* @param string $url URL.
23+
* @return bool true if the URL is valid, otherwise false.
24+
*/
25+
protected function is_valid_url( string $url ): bool {
26+
if ( filter_var( $url, FILTER_VALIDATE_URL ) !== $url || ! str_starts_with( $url, 'http' ) ) {
27+
return false;
28+
}
29+
30+
// Detect duplicated protocol (e.g., "https://http://example.com/").
31+
$parsed_url = wp_parse_url( $url );
32+
33+
if ( isset( $parsed_url['scheme'] ) && str_contains( substr( $url, strlen( $parsed_url['scheme'] ) + 3 ), '://' ) ) {
34+
return false;
35+
}
36+
37+
return true;
38+
}
39+
40+
/**
41+
* Finds and returns the discouraged domain matched in the URL's host, or null if none.
42+
*
43+
* @since 1.6.0
44+
*
45+
* @param string $url The URL to check.
46+
* @return string|null The matched discouraged domain, or null if none matched.
47+
*/
48+
protected function find_discouraged_domain( string $url ) {
49+
$discouraged_domains = $this->get_discouraged_domains();
50+
51+
if ( empty( $discouraged_domains ) ) {
52+
return null;
53+
}
54+
55+
$parsed_url = wp_parse_url( $url );
56+
57+
if ( empty( $parsed_url['host'] ) ) {
58+
return null;
59+
}
60+
61+
$host = strtolower( rtrim( $parsed_url['host'], '.' ) );
62+
63+
foreach ( $discouraged_domains as $domain ) {
64+
$domain = strtolower( rtrim( $domain, '.' ) );
65+
if (
66+
$host === $domain ||
67+
( strlen( $host ) > strlen( $domain ) && substr( $host, -strlen( $domain ) - 1 ) === '.' . $domain )
68+
) {
69+
return $domain;
70+
}
71+
}
72+
73+
return null;
74+
}
75+
76+
/**
77+
* Checks if URL has discouraged domain.
78+
*
79+
* @since 1.6.0
80+
*
81+
* @param string $url The URL to check.
82+
* @return bool True if the URL has a discouraged domain, false otherwise.
83+
*/
84+
protected function has_discouraged_domain( $url ) {
85+
return null !== $this->find_discouraged_domain( $url );
86+
}
87+
88+
/**
89+
* Returns discouraged domains.
90+
*
91+
* @since 1.6.0
92+
*
93+
* @return array Discouraged domains.
94+
*/
95+
private function get_discouraged_domains() {
96+
$discouraged_domains = array(
97+
'example.com',
98+
'example.net',
99+
'example.org',
100+
'yourdomain.com',
101+
'yourwebsite.com',
102+
);
103+
104+
/**
105+
* Filter the list of discouraged domains.
106+
*
107+
* @since 1.6.0
108+
*
109+
* @param array $discouraged_domains Array of discouraged domains.
110+
*/
111+
$discouraged_domains = (array) apply_filters( 'wp_plugin_check_discouraged_domains', $discouraged_domains );
112+
113+
return $discouraged_domains;
114+
}
115+
}

tests/phpunit/testdata/plugins/test-plugin-header-fields-duplicated-protocol-with-errors/load.php

Lines changed: 0 additions & 17 deletions
This file was deleted.

tests/phpunit/tests/Checker/Checks/Plugin_Header_Fields_Check_Tests.php

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,21 +168,4 @@ public function test_run_without_errors_requires_at_least_latest_version() {
168168

169169
$this->assertEmpty( $errors );
170170
}
171-
172-
public function test_run_with_errors_duplicated_protocol_is_valid_url() {
173-
$check = new Plugin_Header_Fields_Check();
174-
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-header-fields-duplicated-protocol-with-errors/load.php' );
175-
$check_result = new Check_Result( $check_context );
176-
177-
$check->run( $check_result );
178-
179-
$errors = $check_result->get_errors();
180-
181-
$filtered_items = wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_invalid_author_uri' ) );
182-
$filtered_items = array_values( $filtered_items );
183-
184-
$this->assertCount( 1, $filtered_items );
185-
$this->assertStringContainsString( 'Author URI', $filtered_items[0]['message'] );
186-
$this->assertStringContainsString( 'is not valid', $filtered_items[0]['message'] );
187-
}
188171
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
/**
3+
* Tests for the URL_Utils trait.
4+
*
5+
* @package plugin-check
6+
*/
7+
8+
use WordPress\Plugin_Check\Traits\URL_Utils;
9+
10+
class URL_Utils_Tests extends WP_UnitTestCase {
11+
12+
use URL_Utils;
13+
14+
/**
15+
* @dataProvider data_url_items
16+
*/
17+
public function test_url_validation( $url, $is_valid ) {
18+
$result = $this->is_valid_url( $url );
19+
$this->assertSame( $is_valid, $result );
20+
}
21+
22+
public function data_url_items() {
23+
return array(
24+
array( 'http://example.com/', true ),
25+
array( 'https://www.example.com', true ),
26+
array( 'https://example.com/page.html', true ),
27+
array( 'https://http://example.com/', false ),
28+
array( 'ftp://example.com/file.txt', false ),
29+
);
30+
}
31+
32+
/**
33+
* @dataProvider data_discouraged_domain_urls
34+
*/
35+
public function test_has_discouraged_domain( $url, $expected ) {
36+
$result = $this->has_discouraged_domain( $url );
37+
$this->assertSame( $expected, $result );
38+
}
39+
40+
public function data_discouraged_domain_urls() {
41+
return array(
42+
array( 'http://example.com/', true ),
43+
array( 'https://example.com', true ),
44+
array( 'https://example.org', true ),
45+
array( 'http://example.net/page', true ),
46+
array( 'http://yourwebsite.com', true ),
47+
array( 'http://sub.example.com/', true ),
48+
array( 'http://www.example.com/', true ),
49+
array( 'https://www.example.com/', true ),
50+
array( 'https://www.example.org', true ),
51+
array( 'http://www.example.net/page', true ),
52+
array( 'http://www.yourwebsite.com', true ),
53+
array( 'http://notexample.com/', false ),
54+
array( 'http://example.co.uk/', false ),
55+
array( 'http://myexample.com/', false ),
56+
array( 'http://sub.notexample.com/', false ),
57+
);
58+
}
59+
}

0 commit comments

Comments
 (0)