From e8a9a1ea7c019016606d3febd6d90b8a5af8219d Mon Sep 17 00:00:00 2001 From: mainulsunvi Date: Tue, 17 Feb 2026 13:42:06 +0600 Subject: [PATCH 1/6] added external link validation for admin menus --- .../External_Admin_Menu_Links_Check.php | 164 ++++++++++++++++++ includes/Checker/Default_Check_Repository.php | 1 + .../load.php | 90 ++++++++++ .../load.php | 140 +++++++++++++++ .../External_Admin_Menu_Links_Check_Tests.php | 93 ++++++++++ 5 files changed, 488 insertions(+) create mode 100644 includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php create mode 100644 tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php create mode 100644 tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-without-errors/load.php create mode 100644 tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php diff --git a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php new file mode 100644 index 000000000..d33758b0a --- /dev/null +++ b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php @@ -0,0 +1,164 @@ +look_for_external_menu_links( $result, $php_files ); + } + + /** + * Looks for external URLs in admin menu functions and amends the given result with an error if found. + * + * @since 1.8.0 + * + * @param Check_Result $result The check result to amend, including the plugin context to check. + * @param array $php_files List of absolute PHP file paths. + */ + protected function look_for_external_menu_links( Check_Result $result, array $php_files ) { + // Build regex pattern for all menu functions. + $functions_pattern = implode( '|', array_map( 'preg_quote', $this->menu_functions ) ); + + // Pattern to match menu function calls with external URLs in the 4th parameter. + // This regex matches: + // - Function name from the list + // - Opening parenthesis + // - First 3 parameters (non-greedy, can be strings with single/double quotes or variables) + // - 4th parameter containing http://, https://, or // at the start of a string + $pattern = '/\b(' . $functions_pattern . ')\s*\(\s*' . + // First parameter + '(?:[^,]+)\s*,\s*' . + // Second parameter + '(?:[^,]+)\s*,\s*' . + // Third parameter + '(?:[^,]+)\s*,\s*' . + // Fourth parameter - look for external URL (http://, https://, or //) + '[\'"](?:https?:)?\/\/[^\'"]+[\'"]/i'; + + $files = self::files_preg_match_all( $pattern, $php_files ); + + if ( ! empty( $files ) ) { + foreach ( $files as $file ) { + $this->add_result_error_for_file( + $result, + sprintf( + /* translators: %s: Comma-separated list of admin menu function names. */ + __( + 'External URL used in admin menu.
Plugins should not add external links directly to the WordPress admin menu. This disrupts the expected user experience and navigation patterns. Instead, create an admin page within WordPress that contains external links with clear descriptions, or add external links within the plugin\'s settings page or help section. Please review usage of: %s', + 'plugin-check' + ), + implode( ', ', $this->menu_functions ) + ), + 'external_admin_menu_link', + $file['file'], + $file['line'], + $file['column'], + 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#11-plugins-should-not-hijack-the-admin', + 9 + ); + } + } + } + + /** + * Gets the description for the check. + * + * Every check must have a short description explaining what the check does. + * + * @since 1.8.0 + * + * @return string Description. + */ + public function get_description(): string { + return __( 'Detects external URLs used in WordPress admin menu functions, which disrupts the expected user experience.', 'plugin-check' ); + } + + /** + * Gets the documentation URL for the check. + * + * Every check must have a URL with further information about the check. + * + * @since 1.8.0 + * + * @return string The documentation URL. + */ + public function get_documentation_url(): string { + return __( 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#11-plugins-should-not-hijack-the-admin', 'plugin-check' ); + } +} diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php index d4e53f940..0a8d03226 100644 --- a/includes/Checker/Default_Check_Repository.php +++ b/includes/Checker/Default_Check_Repository.php @@ -100,6 +100,7 @@ private function register_default_checks() { 'direct_db' => new Checks\Security\Direct_DB_Check(), 'minified_files' => new Checks\Plugin_Repo\Minified_Files_Check(), 'direct_file_access' => new Checks\Plugin_Repo\Direct_File_Access_Check(), + 'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(), ) ); diff --git a/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php new file mode 100644 index 000000000..a17937f9a --- /dev/null +++ b/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php @@ -0,0 +1,90 @@ +

My Plugin Settings

'; +} + +function my_plugin_options_page() { + echo '

My Plugin Options

'; +} + +function my_plugin_tools_page() { + echo '

My Plugin Tools

'; +} + +function my_theme_settings_page() { + echo '

Theme Settings

'; +} + +function my_plugin_manager_page() { + echo '

Plugin Manager

'; +} + +function my_user_settings_page() { + echo '

User Settings

'; +} + +function my_dashboard_info_page() { + echo '

Dashboard Info

'; +} + +function my_plugin_advanced_page() { + echo '

Advanced Settings

'; +} diff --git a/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php new file mode 100644 index 000000000..8bcbcaba4 --- /dev/null +++ b/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php @@ -0,0 +1,93 @@ +run( $check_result ); + + $errors = $check_result->get_errors(); + + $this->assertNotEmpty( $errors ); + $this->assertArrayHasKey( 'load.php', $errors ); + $this->assertGreaterThan( 0, $check_result->get_error_count() ); + + // Check that the error code is correct. + $found_external_menu_error = false; + foreach ( $errors['load.php'] as $line => $columns ) { + foreach ( $columns as $column => $messages ) { + foreach ( $messages as $message ) { + if ( 'external_admin_menu_link' === $message['code'] ) { + $found_external_menu_error = true; + break 3; + } + } + } + } + $this->assertTrue( $found_external_menu_error, 'Expected external_admin_menu_link error code not found.' ); + } + + /** + * Test that internal admin menu slugs do not trigger errors. + */ + public function test_no_errors_for_internal_admin_menu_links() { + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-external-admin-menu-links-without-errors/load.php' ); + $check_result = new Check_Result( $check_context ); + + $check = new External_Admin_Menu_Links_Check(); + $check->run( $check_result ); + + $errors = $check_result->get_errors(); + + $this->assertEmpty( $errors ); + $this->assertSame( 0, $check_result->get_error_count() ); + } + + /** + * Test that the check returns the correct categories. + */ + public function test_get_categories() { + $check = new External_Admin_Menu_Links_Check(); + $categories = $check->get_categories(); + + $this->assertContains( Check_Categories::CATEGORY_PLUGIN_REPO, $categories ); + } + + /** + * Test that the check has a description. + */ + public function test_get_description() { + $check = new External_Admin_Menu_Links_Check(); + $description = $check->get_description(); + + $this->assertNotEmpty( $description ); + $this->assertIsString( $description ); + } + + /** + * Test that the check has a documentation URL. + */ + public function test_get_documentation_url() { + $check = new External_Admin_Menu_Links_Check(); + $url = $check->get_documentation_url(); + + $this->assertNotEmpty( $url ); + $this->assertStringContainsString( 'https://', $url ); + } +} From 1376e9c9aac63dd4a841ada0dc051dada39b2650 Mon Sep 17 00:00:00 2001 From: mainulsunvi Date: Tue, 17 Feb 2026 13:54:25 +0600 Subject: [PATCH 2/6] resolved php linting error --- .../Checker/Checks/External_Admin_Menu_Links_Check_Tests.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php index 8bcbcaba4..9c709f6a2 100644 --- a/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php @@ -10,7 +10,7 @@ use WordPress\Plugin_Check\Checker\Check_Result; use WordPress\Plugin_Check\Checker\Checks\Plugin_Repo\External_Admin_Menu_Links_Check; -class External_Admin_Menu_Links_Check_Tests extends \WP_UnitTestCase { +class External_Admin_Menu_Links_Check_Tests extends WP_UnitTestCase { /** * Test that external URLs in admin menu functions are detected as errors. From ba14320dea9915b763d71e7bf6460c6482e62b82 Mon Sep 17 00:00:00 2001 From: mainulsunvi Date: Tue, 17 Feb 2026 14:00:40 +0600 Subject: [PATCH 3/6] Linting error: missing period at the end of a comment. --- .../Plugin_Repo/External_Admin_Menu_Links_Check.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php index d33758b0a..3bf0e51a1 100644 --- a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php @@ -100,15 +100,15 @@ protected function look_for_external_menu_links( Check_Result $result, array $ph // - Function name from the list // - Opening parenthesis // - First 3 parameters (non-greedy, can be strings with single/double quotes or variables) - // - 4th parameter containing http://, https://, or // at the start of a string + // - 4th parameter containing http://, https://, or // at the start of a string. $pattern = '/\b(' . $functions_pattern . ')\s*\(\s*' . - // First parameter + // First parameter. '(?:[^,]+)\s*,\s*' . - // Second parameter + // Second parameter. '(?:[^,]+)\s*,\s*' . - // Third parameter + // Third parameter. '(?:[^,]+)\s*,\s*' . - // Fourth parameter - look for external URL (http://, https://, or //) + // Fourth parameter - look for external URL (http://, https://, or //). '[\'"](?:https?:)?\/\/[^\'"]+[\'"]/i'; $files = self::files_preg_match_all( $pattern, $php_files ); From 534ae346f5fa421875e50aaaba7047c715d8c932 Mon Sep 17 00:00:00 2001 From: mainulsunvi Date: Tue, 17 Feb 2026 14:03:57 +0600 Subject: [PATCH 4/6] Linting error: whitespace error --- .../Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php index 3bf0e51a1..d73218b3a 100644 --- a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php @@ -102,7 +102,7 @@ protected function look_for_external_menu_links( Check_Result $result, array $ph // - First 3 parameters (non-greedy, can be strings with single/double quotes or variables) // - 4th parameter containing http://, https://, or // at the start of a string. $pattern = '/\b(' . $functions_pattern . ')\s*\(\s*' . - // First parameter. + // First parameter. '(?:[^,]+)\s*,\s*' . // Second parameter. '(?:[^,]+)\s*,\s*' . From cfec2fe071ce6e919bd789a43f69540946e659ea Mon Sep 17 00:00:00 2001 From: mainulsunvi Date: Tue, 17 Feb 2026 14:49:26 +0600 Subject: [PATCH 5/6] Refactor the code to verify only the top-level menu. --- .../External_Admin_Menu_Links_Check.php | 37 ++++------- .../load.php | 62 ++++--------------- .../load.php | 16 +++++ .../External_Admin_Menu_Links_Check_Tests.php | 4 +- 4 files changed, 42 insertions(+), 77 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php index d73218b3a..ee0448a11 100644 --- a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php @@ -29,28 +29,17 @@ class External_Admin_Menu_Links_Check extends Abstract_File_Check { use Stable_Check; /** - * List of admin menu functions to check. + * List of top-level admin menu functions to check. * - * The 4th parameter (index 3) is the menu slug in all these functions. - * add_submenu_page is intentionally excluded as it may legitimately link to - * support pages or external resources. + * Only add_menu_page is checked as it adds to the top-level admin menu. + * External URLs in submenus (add_submenu_page, add_options_page, etc.) are + * acceptable for documentation, support links, and other resources. * * @since 1.8.0 * @var array */ protected $menu_functions = array( 'add_menu_page', - 'add_options_page', - 'add_management_page', - 'add_theme_page', - 'add_plugins_page', - 'add_users_page', - 'add_dashboard_page', - 'add_posts_page', - 'add_media_page', - 'add_links_page', - 'add_pages_page', - 'add_comments_page', ); /** @@ -97,9 +86,9 @@ protected function look_for_external_menu_links( Check_Result $result, array $ph // Pattern to match menu function calls with external URLs in the 4th parameter. // This regex matches: - // - Function name from the list - // - Opening parenthesis - // - First 3 parameters (non-greedy, can be strings with single/double quotes or variables) + // - Function name from the list. + // - Opening parenthesis. + // - First 3 parameters (non-greedy, can be strings with single/double quotes or variables). // - 4th parameter containing http://, https://, or // at the start of a string. $pattern = '/\b(' . $functions_pattern . ')\s*\(\s*' . // First parameter. @@ -117,13 +106,9 @@ protected function look_for_external_menu_links( Check_Result $result, array $ph foreach ( $files as $file ) { $this->add_result_error_for_file( $result, - sprintf( - /* translators: %s: Comma-separated list of admin menu function names. */ - __( - 'External URL used in admin menu.
Plugins should not add external links directly to the WordPress admin menu. This disrupts the expected user experience and navigation patterns. Instead, create an admin page within WordPress that contains external links with clear descriptions, or add external links within the plugin\'s settings page or help section. Please review usage of: %s', - 'plugin-check' - ), - implode( ', ', $this->menu_functions ) + __( + 'External URL used in top-level admin menu.
Plugins should not add external links directly to the top-level WordPress admin menu using add_menu_page(). This disrupts the expected user experience and navigation patterns. Instead, create an admin page within WordPress that contains external links with clear descriptions, or add external links as submenu items using add_submenu_page().', + 'plugin-check' ), 'external_admin_menu_link', $file['file'], @@ -146,7 +131,7 @@ protected function look_for_external_menu_links( Check_Result $result, array $ph * @return string Description. */ public function get_description(): string { - return __( 'Detects external URLs used in WordPress admin menu functions, which disrupts the expected user experience.', 'plugin-check' ); + return __( 'Detects external URLs used in top-level WordPress admin menu, which disrupts the expected user experience.', 'plugin-check' ); } /** diff --git a/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php index a17937f9a..350102bb3 100644 --- a/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php +++ b/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php @@ -16,10 +16,11 @@ */ /** - * These are examples of problematic code that adds external links to admin menu. + * These are examples of problematic code that adds external links to + * the TOP-LEVEL admin menu using add_menu_page(). */ -// ❌ Adding external link to main menu with https. +// ❌ Adding external link to top-level menu with https. add_menu_page( 'External Resource', 'External Resource', @@ -30,55 +31,18 @@ 30 ); -// ❌ Adding external link to options page with http. -add_options_page( - 'Settings', - 'Settings', - 'manage_options', - 'http://example.com/settings' -); - -// ❌ Adding external link to management page. -add_management_page( - 'Tools', - 'Tools', - 'manage_options', - 'https://example.com/tools' -); - -// ❌ Adding external link to theme page. -add_theme_page( - 'Theme Options', - 'Theme Options', - 'manage_options', - 'https://example.com/theme-options' -); - -// ❌ Adding external link to plugins page. -add_plugins_page( - 'Plugin Settings', - 'Plugin Settings', - 'manage_options', - 'https://example.com/plugin-settings' -); - -// ❌ Adding external link to users page. -add_users_page( - 'User Import', - 'User Import', - 'manage_options', - 'https://example.com/user-import' -); - -// ❌ Adding external link to dashboard page. -add_dashboard_page( - 'Dashboard Widget', - 'Dashboard Widget', +// ❌ Adding external link to top-level menu with http. +add_menu_page( + 'HTTP External', + 'HTTP External', 'manage_options', - 'https://example.com/dashboard-widget' + 'http://example.com/http-page', + '', + 'dashicons-admin-site', + 31 ); -// ❌ Adding external link with protocol-relative URL. +// ❌ Adding external link with protocol-relative URL to top-level menu. add_menu_page( 'Protocol Relative', 'Protocol Relative', @@ -86,5 +50,5 @@ '//example.com/protocol-relative', '', 'dashicons-admin-site', - 31 + 32 ); diff --git a/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-without-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-without-errors/load.php index b8143c14a..87b55f8ba 100644 --- a/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-without-errors/load.php +++ b/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-without-errors/load.php @@ -94,6 +94,22 @@ '' ); +// ✅ External URLs in submenu functions are allowed for docs, support, etc. +add_options_page( + 'Plugin Documentation', + 'Documentation', + 'manage_options', + 'https://example.com/plugin-docs' +); + +// ✅ External URL in management page for support link. +add_management_page( + 'Support', + 'Get Support', + 'manage_options', + 'https://example.com/support' +); + // ✅ Adding submenu page with internal slug. add_submenu_page( 'my-plugin-settings', diff --git a/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php index 9c709f6a2..f1100f119 100644 --- a/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/External_Admin_Menu_Links_Check_Tests.php @@ -13,7 +13,7 @@ class External_Admin_Menu_Links_Check_Tests extends WP_UnitTestCase { /** - * Test that external URLs in admin menu functions are detected as errors. + * Test that external URLs in add_menu_page() (top-level menu) are detected as errors. */ public function test_detect_external_admin_menu_links_with_errors() { $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-external-admin-menu-links-with-errors/load.php' ); @@ -44,7 +44,7 @@ public function test_detect_external_admin_menu_links_with_errors() { } /** - * Test that internal admin menu slugs do not trigger errors. + * Test that internal slugs and external URLs in submenu functions do not trigger errors. */ public function test_no_errors_for_internal_admin_menu_links() { $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-external-admin-menu-links-without-errors/load.php' ); From fc25ca17ca5ddcd5e652646bfbde71910668bf96 Mon Sep 17 00:00:00 2001 From: mainulsunvi Date: Wed, 18 Feb 2026 11:44:53 +0600 Subject: [PATCH 6/6] review feedbacks fixed --- .../External_Admin_Menu_Links_Check.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php index ee0448a11..140ca007e 100644 --- a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php @@ -21,7 +21,7 @@ * as the menu slug parameter in WordPress admin menu functions. This practice violates * WordPress.org Plugin Directory Guideline #11 which prohibits hijacking the admin experience. * - * @since 1.8.0 + * @since 1.9.0 */ class External_Admin_Menu_Links_Check extends Abstract_File_Check { @@ -35,7 +35,7 @@ class External_Admin_Menu_Links_Check extends Abstract_File_Check { * External URLs in submenus (add_submenu_page, add_options_page, etc.) are * acceptable for documentation, support links, and other resources. * - * @since 1.8.0 + * @since 1.9.0 * @var array */ protected $menu_functions = array( @@ -47,7 +47,7 @@ class External_Admin_Menu_Links_Check extends Abstract_File_Check { * * Every check must have at least one category. * - * @since 1.8.0 + * @since 1.9.0 * * @return array The categories for the check. */ @@ -58,7 +58,7 @@ public function get_categories() { /** * Amends the given result by running the check on the given list of files. * - * @since 1.8.0 + * @since 1.9.0 * * @param Check_Result $result The check result to amend, including the plugin context to check. * @param array $files List of absolute file paths. @@ -75,7 +75,7 @@ protected function check_files( Check_Result $result, array $files ) { /** * Looks for external URLs in admin menu functions and amends the given result with an error if found. * - * @since 1.8.0 + * @since 1.9.0 * * @param Check_Result $result The check result to amend, including the plugin context to check. * @param array $php_files List of absolute PHP file paths. @@ -107,7 +107,7 @@ protected function look_for_external_menu_links( Check_Result $result, array $ph $this->add_result_error_for_file( $result, __( - 'External URL used in top-level admin menu.
Plugins should not add external links directly to the top-level WordPress admin menu using add_menu_page(). This disrupts the expected user experience and navigation patterns. Instead, create an admin page within WordPress that contains external links with clear descriptions, or add external links as submenu items using add_submenu_page().', + 'External URL used in top-level admin menu.
Plugins should not use add_menu_page() for external links as it disrupts the WordPress navigation experience.', 'plugin-check' ), 'external_admin_menu_link', @@ -115,7 +115,7 @@ protected function look_for_external_menu_links( Check_Result $result, array $ph $file['line'], $file['column'], 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#11-plugins-should-not-hijack-the-admin', - 9 + 7 ); } } @@ -126,7 +126,7 @@ protected function look_for_external_menu_links( Check_Result $result, array $ph * * Every check must have a short description explaining what the check does. * - * @since 1.8.0 + * @since 1.9.0 * * @return string Description. */ @@ -139,7 +139,7 @@ public function get_description(): string { * * Every check must have a URL with further information about the check. * - * @since 1.8.0 + * @since 1.9.0 * * @return string The documentation URL. */