From 18bda14e1e78eba29b2df570583a8b6320d26a58 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 16 Aug 2021 20:16:13 +0000 Subject: [PATCH 001/257] Code Modernization: Check the return type of `parse_url()` in `WP::parse_request()`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As per the PHP manual: > If the `component` parameter is omitted, an associative array is returned. > If the `component` parameter is specified, `parse_url()` returns a string (or an int, in the case of `PHP_URL_PORT`) instead of an array. If the requested component doesn't exist within the given URL, `null` will be returned. Reference: [https://www.php.net/manual/en/function.parse-url.php#refsect1-function.parse-url-returnvalues PHP Manual: parse_url(): Return Values] In this case, `parse_url()` is called with the `PHP_URL_PATH` as `$component`. This will return `null` in the majority of cases, as – exсept for subdirectory-based sites – `home_url()` returns a URL without the trailing slash, like `http://example.org`. The return value of `parse_url()` was subsequently passed to `trim()`, leading to a `trim(): Passing null to parameter #1 ($string) of type string is deprecated` notice on PHP 8.1. Fixed by adjusting the logic flow to: * Only pass the return value of `parse_url()` to follow-on functions if it makes sense, i.e. if it isn't `null`, nor an empty string. * Preventing calls to `preg_replace()` and `trim()` further down in the function logic flow, when `preg_replace()`/`trim()` would have nothing to do anyhow. Follow-up to [25617]. Props jrf, hellofromTonya, SergeyBiryukov. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51622 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp.php | 24 +++++++++----- tests/phpunit/tests/wp/parseRequest.php | 42 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 tests/phpunit/tests/wp/parseRequest.php diff --git a/src/wp-includes/class-wp.php b/src/wp-includes/class-wp.php index 8c7c48b3b24c3..770df083e70ea 100644 --- a/src/wp-includes/class-wp.php +++ b/src/wp-includes/class-wp.php @@ -170,8 +170,13 @@ public function parse_request( $extra_query_vars = '' ) { list( $req_uri ) = explode( '?', $_SERVER['REQUEST_URI'] ); $self = $_SERVER['PHP_SELF']; - $home_path = trim( parse_url( home_url(), PHP_URL_PATH ), '/' ); - $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) ); + + $home_path = parse_url( home_url(), PHP_URL_PATH ); + $home_path_regex = ''; + if ( is_string( $home_path ) && '' !== $home_path ) { + $home_path = trim( $home_path, '/' ); + $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) ); + } /* * Trim path info from the end and the leading home path from the front. @@ -180,14 +185,17 @@ public function parse_request( $extra_query_vars = '' ) { */ $req_uri = str_replace( $pathinfo, '', $req_uri ); $req_uri = trim( $req_uri, '/' ); - $req_uri = preg_replace( $home_path_regex, '', $req_uri ); - $req_uri = trim( $req_uri, '/' ); - $pathinfo = trim( $pathinfo, '/' ); - $pathinfo = preg_replace( $home_path_regex, '', $pathinfo ); $pathinfo = trim( $pathinfo, '/' ); $self = trim( $self, '/' ); - $self = preg_replace( $home_path_regex, '', $self ); - $self = trim( $self, '/' ); + + if ( ! empty( $home_path_regex ) ) { + $req_uri = preg_replace( $home_path_regex, '', $req_uri ); + $req_uri = trim( $req_uri, '/' ); + $pathinfo = preg_replace( $home_path_regex, '', $pathinfo ); + $pathinfo = trim( $pathinfo, '/' ); + $self = preg_replace( $home_path_regex, '', $self ); + $self = trim( $self, '/' ); + } // The requested permalink is in $pathinfo for path info requests and // $req_uri for other requests. diff --git a/tests/phpunit/tests/wp/parseRequest.php b/tests/phpunit/tests/wp/parseRequest.php new file mode 100644 index 0000000000000..2f022d7d2dd3a --- /dev/null +++ b/tests/phpunit/tests/wp/parseRequest.php @@ -0,0 +1,42 @@ +wp = new WP(); + } + + /** + * Test that PHP 8.1 "passing null to non-nullable" deprecation notice + * is not thrown when the home URL has no path/trailing slash (default setup). + * + * Note: This does not test the actual functioning of the parse_request() method. + * It just and only tests for/against the deprecation notice. + * + * @ticket 53635 + */ + public function test_no_deprecation_notice_when_home_url_has_no_path() { + // Make sure rewrite rules are not empty. + $this->set_permalink_structure( '/%year%/%monthnum%/%postname%/' ); + + // Make sure the test will function independently of whatever the test user set in wp-tests-config.php. + add_filter( + 'home_url', + static function ( $url ) { + return 'http://example.org'; + } + ); + + $this->wp->parse_request(); + $this->assertSame( '', $this->wp->request ); + } +} From 4d2762ed93d9d7a8801a05f764de6fe6ded9b87c Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 16 Aug 2021 21:33:54 +0000 Subject: [PATCH 002/257] Tests: Rename classes in `phpunit/tests/formatting/` per the naming conventions. https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/#naming-and-organization Follow-up to [47780], [48911], [49327], [50291], [50292], [50342], [50452], [50453], [50456], [50967], [50968], [50969], [51491], [51492], [51493]. See #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51623 602fd350-edb4-49c9-b593-d223f7449a82 --- .../formatting/{CapitalPDangit.php => capitalPDangit.php} | 0 tests/phpunit/tests/formatting/{CleanPre.php => cleanPre.php} | 0 .../{ConvertInvalidEntries.php => convertInvalidEntries.php} | 0 .../tests/formatting/{Smilies.php => convertSmilies.php} | 2 +- tests/phpunit/tests/formatting/{Emoji.php => emoji.php} | 0 tests/phpunit/tests/formatting/ent2ncr.php | 2 +- tests/phpunit/tests/formatting/{EscAttr.php => escAttr.php} | 0 tests/phpunit/tests/formatting/{EscHtml.php => escHtml.php} | 0 tests/phpunit/tests/formatting/{JSEscape.php => escJs.php} | 2 +- .../tests/formatting/{EscTextarea.php => escTextarea.php} | 0 tests/phpunit/tests/formatting/{EscUrl.php => escUrl.php} | 0 tests/phpunit/tests/formatting/{EscXml.php => escXml.php} | 0 .../{ExcerptRemoveBlocks.php => excerptRemoveBlocks.php} | 0 .../tests/formatting/{BlogInfo.php => getBloginfo.php} | 2 +- .../formatting/{GetUrlInContent.php => getUrlInContent.php} | 0 .../tests/formatting/{HumanTimeDiff.php => humanTimeDiff.php} | 0 tests/phpunit/tests/formatting/{IsEmail.php => isEmail.php} | 0 .../tests/formatting/{LikeEscape.php => likeEscape.php} | 0 .../formatting/{LinksAddTarget.php => linksAddTarget.php} | 0 .../tests/formatting/{MakeClickable.php => makeClickable.php} | 0 tests/phpunit/tests/formatting/{MapDeep.php => mapDeep.php} | 0 .../{NormalizeWhitespace.php => normalizeWhitespace.php} | 0 .../tests/formatting/{RemoveAccents.php => removeAccents.php} | 0 .../formatting/{SanitizeFileName.php => sanitizeFileName.php} | 0 .../formatting/{SanitizeMimeType.php => sanitizeMimeType.php} | 0 .../formatting/{SanitizeOrderby.php => sanitizeOrderby.php} | 0 .../tests/formatting/{SanitizePost.php => sanitizePost.php} | 0 .../{SanitizeTextField.php => sanitizeTextField.php} | 0 .../tests/formatting/{SanitizeTitle.php => sanitizeTitle.php} | 0 ...anitizeTitleWithDashes.php => sanitizeTitleWithDashes.php} | 0 .../{SanitizeTrackbackUrls.php => sanitizeTrackbackUrls.php} | 0 .../tests/formatting/{SanitizeUser.php => sanitizeUser.php} | 0 .../phpunit/tests/formatting/{SeemsUtf8.php => seemsUtf8.php} | 0 tests/phpunit/tests/formatting/{Slashit.php => slashit.php} | 0 .../formatting/{StripSlashesDeep.php => stripslashesDeep.php} | 2 +- .../tests/formatting/{URLShorten.php => urlShorten.php} | 4 ++-- .../tests/formatting/{UrlencodeDeep.php => urlencodeDeep.php} | 0 .../tests/formatting/{Utf8UriEncode.php => utf8UriEncode.php} | 0 tests/phpunit/tests/formatting/{Autop.php => wpAutop.php} | 2 +- .../tests/formatting/{WPBasename.php => wpBasename.php} | 2 +- .../tests/formatting/{HtmlExcerpt.php => wpHtmlExcerpt.php} | 2 +- .../tests/formatting/{WpHtmlSplit.php => wpHtmlSplit.php} | 2 +- .../tests/formatting/{WpHtmlEditPre.php => wpHtmleditPre.php} | 2 +- .../formatting/{isoDescrambler.php => wpIsoDescrambler.php} | 2 +- .../{WPMakeLinkRelative.php => wpMakeLinkRelative.php} | 2 +- .../tests/formatting/{WPRelNoFollow.php => wpRelNofollow.php} | 2 +- tests/phpunit/tests/formatting/{WPRelUgc.php => wpRelUgc.php} | 2 +- .../{WpReplaceInHtmlTags.php => wpReplaceInHtmlTags.php} | 2 +- .../tests/formatting/{WpRichEditPre.php => wpRicheditPre.php} | 2 +- tests/phpunit/tests/formatting/{WPSlash.php => wpSlash.php} | 2 +- .../formatting/{WPSpecialchars.php => wpSpecialchars.php} | 2 +- .../formatting/{WPStripAllTags.php => wpStripAllTags.php} | 2 +- .../{WPTargetedLinkRel.php => wpTargetedLinkRel.php} | 2 +- .../tests/formatting/{WPTexturize.php => wpTexturize.php} | 2 +- .../tests/formatting/{WpTrimExcerpt.php => wpTrimExcerpt.php} | 2 +- .../tests/formatting/{WPTrimWords.php => wpTrimWords.php} | 2 +- tests/phpunit/tests/formatting/{Zeroise.php => zeroise.php} | 0 57 files changed, 25 insertions(+), 25 deletions(-) rename tests/phpunit/tests/formatting/{CapitalPDangit.php => capitalPDangit.php} (100%) rename tests/phpunit/tests/formatting/{CleanPre.php => cleanPre.php} (100%) rename tests/phpunit/tests/formatting/{ConvertInvalidEntries.php => convertInvalidEntries.php} (100%) rename tests/phpunit/tests/formatting/{Smilies.php => convertSmilies.php} (99%) rename tests/phpunit/tests/formatting/{Emoji.php => emoji.php} (100%) rename tests/phpunit/tests/formatting/{EscAttr.php => escAttr.php} (100%) rename tests/phpunit/tests/formatting/{EscHtml.php => escHtml.php} (100%) rename tests/phpunit/tests/formatting/{JSEscape.php => escJs.php} (95%) rename tests/phpunit/tests/formatting/{EscTextarea.php => escTextarea.php} (100%) rename tests/phpunit/tests/formatting/{EscUrl.php => escUrl.php} (100%) rename tests/phpunit/tests/formatting/{EscXml.php => escXml.php} (100%) rename tests/phpunit/tests/formatting/{ExcerptRemoveBlocks.php => excerptRemoveBlocks.php} (100%) rename tests/phpunit/tests/formatting/{BlogInfo.php => getBloginfo.php} (96%) rename tests/phpunit/tests/formatting/{GetUrlInContent.php => getUrlInContent.php} (100%) rename tests/phpunit/tests/formatting/{HumanTimeDiff.php => humanTimeDiff.php} (100%) rename tests/phpunit/tests/formatting/{IsEmail.php => isEmail.php} (100%) rename tests/phpunit/tests/formatting/{LikeEscape.php => likeEscape.php} (100%) rename tests/phpunit/tests/formatting/{LinksAddTarget.php => linksAddTarget.php} (100%) rename tests/phpunit/tests/formatting/{MakeClickable.php => makeClickable.php} (100%) rename tests/phpunit/tests/formatting/{MapDeep.php => mapDeep.php} (100%) rename tests/phpunit/tests/formatting/{NormalizeWhitespace.php => normalizeWhitespace.php} (100%) rename tests/phpunit/tests/formatting/{RemoveAccents.php => removeAccents.php} (100%) rename tests/phpunit/tests/formatting/{SanitizeFileName.php => sanitizeFileName.php} (100%) rename tests/phpunit/tests/formatting/{SanitizeMimeType.php => sanitizeMimeType.php} (100%) rename tests/phpunit/tests/formatting/{SanitizeOrderby.php => sanitizeOrderby.php} (100%) rename tests/phpunit/tests/formatting/{SanitizePost.php => sanitizePost.php} (100%) rename tests/phpunit/tests/formatting/{SanitizeTextField.php => sanitizeTextField.php} (100%) rename tests/phpunit/tests/formatting/{SanitizeTitle.php => sanitizeTitle.php} (100%) rename tests/phpunit/tests/formatting/{SanitizeTitleWithDashes.php => sanitizeTitleWithDashes.php} (100%) rename tests/phpunit/tests/formatting/{SanitizeTrackbackUrls.php => sanitizeTrackbackUrls.php} (100%) rename tests/phpunit/tests/formatting/{SanitizeUser.php => sanitizeUser.php} (100%) rename tests/phpunit/tests/formatting/{SeemsUtf8.php => seemsUtf8.php} (100%) rename tests/phpunit/tests/formatting/{Slashit.php => slashit.php} (100%) rename tests/phpunit/tests/formatting/{StripSlashesDeep.php => stripslashesDeep.php} (96%) rename tests/phpunit/tests/formatting/{URLShorten.php => urlShorten.php} (91%) rename tests/phpunit/tests/formatting/{UrlencodeDeep.php => urlencodeDeep.php} (100%) rename tests/phpunit/tests/formatting/{Utf8UriEncode.php => utf8UriEncode.php} (100%) rename tests/phpunit/tests/formatting/{Autop.php => wpAutop.php} (99%) rename tests/phpunit/tests/formatting/{WPBasename.php => wpBasename.php} (91%) rename tests/phpunit/tests/formatting/{HtmlExcerpt.php => wpHtmlExcerpt.php} (90%) rename tests/phpunit/tests/formatting/{WpHtmlSplit.php => wpHtmlSplit.php} (94%) rename tests/phpunit/tests/formatting/{WpHtmlEditPre.php => wpHtmleditPre.php} (93%) rename tests/phpunit/tests/formatting/{isoDescrambler.php => wpIsoDescrambler.php} (82%) rename tests/phpunit/tests/formatting/{WPMakeLinkRelative.php => wpMakeLinkRelative.php} (95%) rename tests/phpunit/tests/formatting/{WPRelNoFollow.php => wpRelNofollow.php} (97%) rename tests/phpunit/tests/formatting/{WPRelUgc.php => wpRelUgc.php} (97%) rename tests/phpunit/tests/formatting/{WpReplaceInHtmlTags.php => wpReplaceInHtmlTags.php} (90%) rename tests/phpunit/tests/formatting/{WpRichEditPre.php => wpRicheditPre.php} (94%) rename tests/phpunit/tests/formatting/{WPSlash.php => wpSlash.php} (97%) rename tests/phpunit/tests/formatting/{WPSpecialchars.php => wpSpecialchars.php} (98%) rename tests/phpunit/tests/formatting/{WPStripAllTags.php => wpStripAllTags.php} (93%) rename tests/phpunit/tests/formatting/{WPTargetedLinkRel.php => wpTargetedLinkRel.php} (98%) rename tests/phpunit/tests/formatting/{WPTexturize.php => wpTexturize.php} (99%) rename tests/phpunit/tests/formatting/{WpTrimExcerpt.php => wpTrimExcerpt.php} (96%) rename tests/phpunit/tests/formatting/{WPTrimWords.php => wpTrimWords.php} (98%) rename tests/phpunit/tests/formatting/{Zeroise.php => zeroise.php} (100%) diff --git a/tests/phpunit/tests/formatting/CapitalPDangit.php b/tests/phpunit/tests/formatting/capitalPDangit.php similarity index 100% rename from tests/phpunit/tests/formatting/CapitalPDangit.php rename to tests/phpunit/tests/formatting/capitalPDangit.php diff --git a/tests/phpunit/tests/formatting/CleanPre.php b/tests/phpunit/tests/formatting/cleanPre.php similarity index 100% rename from tests/phpunit/tests/formatting/CleanPre.php rename to tests/phpunit/tests/formatting/cleanPre.php diff --git a/tests/phpunit/tests/formatting/ConvertInvalidEntries.php b/tests/phpunit/tests/formatting/convertInvalidEntries.php similarity index 100% rename from tests/phpunit/tests/formatting/ConvertInvalidEntries.php rename to tests/phpunit/tests/formatting/convertInvalidEntries.php diff --git a/tests/phpunit/tests/formatting/Smilies.php b/tests/phpunit/tests/formatting/convertSmilies.php similarity index 99% rename from tests/phpunit/tests/formatting/Smilies.php rename to tests/phpunit/tests/formatting/convertSmilies.php index f5394d4268e67..63d95aa07169b 100644 --- a/tests/phpunit/tests/formatting/Smilies.php +++ b/tests/phpunit/tests/formatting/convertSmilies.php @@ -4,7 +4,7 @@ * @group formatting * @group emoji */ -class Tests_Formatting_Smilies extends WP_UnitTestCase { +class Tests_Formatting_ConvertSmilies extends WP_UnitTestCase { /** * Basic Test Content DataProvider diff --git a/tests/phpunit/tests/formatting/Emoji.php b/tests/phpunit/tests/formatting/emoji.php similarity index 100% rename from tests/phpunit/tests/formatting/Emoji.php rename to tests/phpunit/tests/formatting/emoji.php diff --git a/tests/phpunit/tests/formatting/ent2ncr.php b/tests/phpunit/tests/formatting/ent2ncr.php index ba84e6d2e0692..64571580e4d94 100644 --- a/tests/phpunit/tests/formatting/ent2ncr.php +++ b/tests/phpunit/tests/formatting/ent2ncr.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_Ent2NCR extends WP_UnitTestCase { +class Tests_Formatting_Ent2ncr extends WP_UnitTestCase { /** * @dataProvider entities */ diff --git a/tests/phpunit/tests/formatting/EscAttr.php b/tests/phpunit/tests/formatting/escAttr.php similarity index 100% rename from tests/phpunit/tests/formatting/EscAttr.php rename to tests/phpunit/tests/formatting/escAttr.php diff --git a/tests/phpunit/tests/formatting/EscHtml.php b/tests/phpunit/tests/formatting/escHtml.php similarity index 100% rename from tests/phpunit/tests/formatting/EscHtml.php rename to tests/phpunit/tests/formatting/escHtml.php diff --git a/tests/phpunit/tests/formatting/JSEscape.php b/tests/phpunit/tests/formatting/escJs.php similarity index 95% rename from tests/phpunit/tests/formatting/JSEscape.php rename to tests/phpunit/tests/formatting/escJs.php index 4dcb06481940f..bd6407518b3bd 100644 --- a/tests/phpunit/tests/formatting/JSEscape.php +++ b/tests/phpunit/tests/formatting/escJs.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_JSEscape extends WP_UnitTestCase { +class Tests_Formatting_EscJs extends WP_UnitTestCase { function test_js_escape_simple() { $out = esc_js( 'foo bar baz();' ); $this->assertSame( 'foo bar baz();', $out ); diff --git a/tests/phpunit/tests/formatting/EscTextarea.php b/tests/phpunit/tests/formatting/escTextarea.php similarity index 100% rename from tests/phpunit/tests/formatting/EscTextarea.php rename to tests/phpunit/tests/formatting/escTextarea.php diff --git a/tests/phpunit/tests/formatting/EscUrl.php b/tests/phpunit/tests/formatting/escUrl.php similarity index 100% rename from tests/phpunit/tests/formatting/EscUrl.php rename to tests/phpunit/tests/formatting/escUrl.php diff --git a/tests/phpunit/tests/formatting/EscXml.php b/tests/phpunit/tests/formatting/escXml.php similarity index 100% rename from tests/phpunit/tests/formatting/EscXml.php rename to tests/phpunit/tests/formatting/escXml.php diff --git a/tests/phpunit/tests/formatting/ExcerptRemoveBlocks.php b/tests/phpunit/tests/formatting/excerptRemoveBlocks.php similarity index 100% rename from tests/phpunit/tests/formatting/ExcerptRemoveBlocks.php rename to tests/phpunit/tests/formatting/excerptRemoveBlocks.php diff --git a/tests/phpunit/tests/formatting/BlogInfo.php b/tests/phpunit/tests/formatting/getBloginfo.php similarity index 96% rename from tests/phpunit/tests/formatting/BlogInfo.php rename to tests/phpunit/tests/formatting/getBloginfo.php index 37ce3450facae..b1f87a0d16e9a 100644 --- a/tests/phpunit/tests/formatting/BlogInfo.php +++ b/tests/phpunit/tests/formatting/getBloginfo.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_BlogInfo extends WP_UnitTestCase { +class Tests_Formatting_GetBloginfo extends WP_UnitTestCase { /** * @dataProvider locales diff --git a/tests/phpunit/tests/formatting/GetUrlInContent.php b/tests/phpunit/tests/formatting/getUrlInContent.php similarity index 100% rename from tests/phpunit/tests/formatting/GetUrlInContent.php rename to tests/phpunit/tests/formatting/getUrlInContent.php diff --git a/tests/phpunit/tests/formatting/HumanTimeDiff.php b/tests/phpunit/tests/formatting/humanTimeDiff.php similarity index 100% rename from tests/phpunit/tests/formatting/HumanTimeDiff.php rename to tests/phpunit/tests/formatting/humanTimeDiff.php diff --git a/tests/phpunit/tests/formatting/IsEmail.php b/tests/phpunit/tests/formatting/isEmail.php similarity index 100% rename from tests/phpunit/tests/formatting/IsEmail.php rename to tests/phpunit/tests/formatting/isEmail.php diff --git a/tests/phpunit/tests/formatting/LikeEscape.php b/tests/phpunit/tests/formatting/likeEscape.php similarity index 100% rename from tests/phpunit/tests/formatting/LikeEscape.php rename to tests/phpunit/tests/formatting/likeEscape.php diff --git a/tests/phpunit/tests/formatting/LinksAddTarget.php b/tests/phpunit/tests/formatting/linksAddTarget.php similarity index 100% rename from tests/phpunit/tests/formatting/LinksAddTarget.php rename to tests/phpunit/tests/formatting/linksAddTarget.php diff --git a/tests/phpunit/tests/formatting/MakeClickable.php b/tests/phpunit/tests/formatting/makeClickable.php similarity index 100% rename from tests/phpunit/tests/formatting/MakeClickable.php rename to tests/phpunit/tests/formatting/makeClickable.php diff --git a/tests/phpunit/tests/formatting/MapDeep.php b/tests/phpunit/tests/formatting/mapDeep.php similarity index 100% rename from tests/phpunit/tests/formatting/MapDeep.php rename to tests/phpunit/tests/formatting/mapDeep.php diff --git a/tests/phpunit/tests/formatting/NormalizeWhitespace.php b/tests/phpunit/tests/formatting/normalizeWhitespace.php similarity index 100% rename from tests/phpunit/tests/formatting/NormalizeWhitespace.php rename to tests/phpunit/tests/formatting/normalizeWhitespace.php diff --git a/tests/phpunit/tests/formatting/RemoveAccents.php b/tests/phpunit/tests/formatting/removeAccents.php similarity index 100% rename from tests/phpunit/tests/formatting/RemoveAccents.php rename to tests/phpunit/tests/formatting/removeAccents.php diff --git a/tests/phpunit/tests/formatting/SanitizeFileName.php b/tests/phpunit/tests/formatting/sanitizeFileName.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizeFileName.php rename to tests/phpunit/tests/formatting/sanitizeFileName.php diff --git a/tests/phpunit/tests/formatting/SanitizeMimeType.php b/tests/phpunit/tests/formatting/sanitizeMimeType.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizeMimeType.php rename to tests/phpunit/tests/formatting/sanitizeMimeType.php diff --git a/tests/phpunit/tests/formatting/SanitizeOrderby.php b/tests/phpunit/tests/formatting/sanitizeOrderby.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizeOrderby.php rename to tests/phpunit/tests/formatting/sanitizeOrderby.php diff --git a/tests/phpunit/tests/formatting/SanitizePost.php b/tests/phpunit/tests/formatting/sanitizePost.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizePost.php rename to tests/phpunit/tests/formatting/sanitizePost.php diff --git a/tests/phpunit/tests/formatting/SanitizeTextField.php b/tests/phpunit/tests/formatting/sanitizeTextField.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizeTextField.php rename to tests/phpunit/tests/formatting/sanitizeTextField.php diff --git a/tests/phpunit/tests/formatting/SanitizeTitle.php b/tests/phpunit/tests/formatting/sanitizeTitle.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizeTitle.php rename to tests/phpunit/tests/formatting/sanitizeTitle.php diff --git a/tests/phpunit/tests/formatting/SanitizeTitleWithDashes.php b/tests/phpunit/tests/formatting/sanitizeTitleWithDashes.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizeTitleWithDashes.php rename to tests/phpunit/tests/formatting/sanitizeTitleWithDashes.php diff --git a/tests/phpunit/tests/formatting/SanitizeTrackbackUrls.php b/tests/phpunit/tests/formatting/sanitizeTrackbackUrls.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizeTrackbackUrls.php rename to tests/phpunit/tests/formatting/sanitizeTrackbackUrls.php diff --git a/tests/phpunit/tests/formatting/SanitizeUser.php b/tests/phpunit/tests/formatting/sanitizeUser.php similarity index 100% rename from tests/phpunit/tests/formatting/SanitizeUser.php rename to tests/phpunit/tests/formatting/sanitizeUser.php diff --git a/tests/phpunit/tests/formatting/SeemsUtf8.php b/tests/phpunit/tests/formatting/seemsUtf8.php similarity index 100% rename from tests/phpunit/tests/formatting/SeemsUtf8.php rename to tests/phpunit/tests/formatting/seemsUtf8.php diff --git a/tests/phpunit/tests/formatting/Slashit.php b/tests/phpunit/tests/formatting/slashit.php similarity index 100% rename from tests/phpunit/tests/formatting/Slashit.php rename to tests/phpunit/tests/formatting/slashit.php diff --git a/tests/phpunit/tests/formatting/StripSlashesDeep.php b/tests/phpunit/tests/formatting/stripslashesDeep.php similarity index 96% rename from tests/phpunit/tests/formatting/StripSlashesDeep.php rename to tests/phpunit/tests/formatting/stripslashesDeep.php index a401a99f792c3..a475f9402afdb 100644 --- a/tests/phpunit/tests/formatting/StripSlashesDeep.php +++ b/tests/phpunit/tests/formatting/stripslashesDeep.php @@ -4,7 +4,7 @@ * @group formatting * @group slashes */ -class Tests_Formatting_StripSlashesDeep extends WP_UnitTestCase { +class Tests_Formatting_StripslashesDeep extends WP_UnitTestCase { /** * @ticket 18026 */ diff --git a/tests/phpunit/tests/formatting/URLShorten.php b/tests/phpunit/tests/formatting/urlShorten.php similarity index 91% rename from tests/phpunit/tests/formatting/URLShorten.php rename to tests/phpunit/tests/formatting/urlShorten.php index a1f1942809871..94e2728e6b5ae 100644 --- a/tests/phpunit/tests/formatting/URLShorten.php +++ b/tests/phpunit/tests/formatting/urlShorten.php @@ -3,8 +3,8 @@ /** * @group formatting */ -class Tests_Formatting_URLShorten extends WP_UnitTestCase { - function test_shorten_url() { +class Tests_Formatting_UrlShorten extends WP_UnitTestCase { + function test_url_shorten() { $tests = array( 'wordpress\.org/about/philosophy' => 'wordpress\.org/about/philosophy', // No longer strips slashes. 'wordpress.org/about/philosophy' => 'wordpress.org/about/philosophy', diff --git a/tests/phpunit/tests/formatting/UrlencodeDeep.php b/tests/phpunit/tests/formatting/urlencodeDeep.php similarity index 100% rename from tests/phpunit/tests/formatting/UrlencodeDeep.php rename to tests/phpunit/tests/formatting/urlencodeDeep.php diff --git a/tests/phpunit/tests/formatting/Utf8UriEncode.php b/tests/phpunit/tests/formatting/utf8UriEncode.php similarity index 100% rename from tests/phpunit/tests/formatting/Utf8UriEncode.php rename to tests/phpunit/tests/formatting/utf8UriEncode.php diff --git a/tests/phpunit/tests/formatting/Autop.php b/tests/phpunit/tests/formatting/wpAutop.php similarity index 99% rename from tests/phpunit/tests/formatting/Autop.php rename to tests/phpunit/tests/formatting/wpAutop.php index 8a77c6879ead6..ccb6721073534 100644 --- a/tests/phpunit/tests/formatting/Autop.php +++ b/tests/phpunit/tests/formatting/wpAutop.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_Autop extends WP_UnitTestCase { +class Tests_Formatting_wpAutop extends WP_UnitTestCase { /** * @ticket 11008 diff --git a/tests/phpunit/tests/formatting/WPBasename.php b/tests/phpunit/tests/formatting/wpBasename.php similarity index 91% rename from tests/phpunit/tests/formatting/WPBasename.php rename to tests/phpunit/tests/formatting/wpBasename.php index a664e399c00b4..341a4c8a66e4f 100644 --- a/tests/phpunit/tests/formatting/WPBasename.php +++ b/tests/phpunit/tests/formatting/wpBasename.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_WP_Basename extends WP_UnitTestCase { +class Tests_Formatting_wpBasename extends WP_UnitTestCase { function test_wp_basename_unix() { $this->assertSame( diff --git a/tests/phpunit/tests/formatting/HtmlExcerpt.php b/tests/phpunit/tests/formatting/wpHtmlExcerpt.php similarity index 90% rename from tests/phpunit/tests/formatting/HtmlExcerpt.php rename to tests/phpunit/tests/formatting/wpHtmlExcerpt.php index 36315fc541880..bbe89106b3697 100644 --- a/tests/phpunit/tests/formatting/HtmlExcerpt.php +++ b/tests/phpunit/tests/formatting/wpHtmlExcerpt.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_HtmlExcerpt extends WP_UnitTestCase { +class Tests_Formatting_wpHtmlExcerpt extends WP_UnitTestCase { function test_simple() { $this->assertSame( 'Baba', wp_html_excerpt( 'Baba told me not to come', 4 ) ); } diff --git a/tests/phpunit/tests/formatting/WpHtmlSplit.php b/tests/phpunit/tests/formatting/wpHtmlSplit.php similarity index 94% rename from tests/phpunit/tests/formatting/WpHtmlSplit.php rename to tests/phpunit/tests/formatting/wpHtmlSplit.php index a9baa7cb8c1a8..50ebdd70511c8 100644 --- a/tests/phpunit/tests/formatting/WpHtmlSplit.php +++ b/tests/phpunit/tests/formatting/wpHtmlSplit.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_WpHtmlSplit extends WP_UnitTestCase { +class Tests_Formatting_wpHtmlSplit extends WP_UnitTestCase { /** * Basic functionality goes here. diff --git a/tests/phpunit/tests/formatting/WpHtmlEditPre.php b/tests/phpunit/tests/formatting/wpHtmleditPre.php similarity index 93% rename from tests/phpunit/tests/formatting/WpHtmlEditPre.php rename to tests/phpunit/tests/formatting/wpHtmleditPre.php index fceabbe8db3b7..f00e917f2ce3e 100644 --- a/tests/phpunit/tests/formatting/WpHtmlEditPre.php +++ b/tests/phpunit/tests/formatting/wpHtmleditPre.php @@ -4,7 +4,7 @@ * @group formatting * @expectedDeprecated wp_htmledit_pre */ -class Tests_Formatting_WpHtmlEditPre extends WP_UnitTestCase { +class Tests_Formatting_wpHtmleditPre extends WP_UnitTestCase { function _charset_iso_8859_1() { return 'iso-8859-1'; diff --git a/tests/phpunit/tests/formatting/isoDescrambler.php b/tests/phpunit/tests/formatting/wpIsoDescrambler.php similarity index 82% rename from tests/phpunit/tests/formatting/isoDescrambler.php rename to tests/phpunit/tests/formatting/wpIsoDescrambler.php index c1133c4bcf896..bb2be63514f71 100644 --- a/tests/phpunit/tests/formatting/isoDescrambler.php +++ b/tests/phpunit/tests/formatting/wpIsoDescrambler.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Test_WP_ISO_Descrambler extends WP_UnitTestCase { +class Tests_Formatting_wpIsoDescrambler extends WP_UnitTestCase { /* * Decodes text in RFC2047 "Q"-encoding, e.g. * =?iso-8859-1?q?this=20is=20some=20text?= diff --git a/tests/phpunit/tests/formatting/WPMakeLinkRelative.php b/tests/phpunit/tests/formatting/wpMakeLinkRelative.php similarity index 95% rename from tests/phpunit/tests/formatting/WPMakeLinkRelative.php rename to tests/phpunit/tests/formatting/wpMakeLinkRelative.php index da6099302b056..2592c2a2d0ac2 100644 --- a/tests/phpunit/tests/formatting/WPMakeLinkRelative.php +++ b/tests/phpunit/tests/formatting/wpMakeLinkRelative.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_WPMakeLinkRelative extends WP_UnitTestCase { +class Tests_Formatting_wpMakeLinkRelative extends WP_UnitTestCase { public function test_wp_make_link_relative_with_http_scheme() { $link = 'http://example.com/this-is-a-test-http-url/'; diff --git a/tests/phpunit/tests/formatting/WPRelNoFollow.php b/tests/phpunit/tests/formatting/wpRelNofollow.php similarity index 97% rename from tests/phpunit/tests/formatting/WPRelNoFollow.php rename to tests/phpunit/tests/formatting/wpRelNofollow.php index 10385cade78bd..f66da5db5ed6c 100644 --- a/tests/phpunit/tests/formatting/WPRelNoFollow.php +++ b/tests/phpunit/tests/formatting/wpRelNofollow.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Rel_No_Follow extends WP_UnitTestCase { +class Tests_Formatting_wpRelNofollow extends WP_UnitTestCase { /** * @ticket 9959 diff --git a/tests/phpunit/tests/formatting/WPRelUgc.php b/tests/phpunit/tests/formatting/wpRelUgc.php similarity index 97% rename from tests/phpunit/tests/formatting/WPRelUgc.php rename to tests/phpunit/tests/formatting/wpRelUgc.php index 85470155a1d67..ea0d4dda65adc 100644 --- a/tests/phpunit/tests/formatting/WPRelUgc.php +++ b/tests/phpunit/tests/formatting/wpRelUgc.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Rel_Ugc extends WP_UnitTestCase { +class Tests_Formatting_wpRelUgc extends WP_UnitTestCase { /** * @ticket 48022 diff --git a/tests/phpunit/tests/formatting/WpReplaceInHtmlTags.php b/tests/phpunit/tests/formatting/wpReplaceInHtmlTags.php similarity index 90% rename from tests/phpunit/tests/formatting/WpReplaceInHtmlTags.php rename to tests/phpunit/tests/formatting/wpReplaceInHtmlTags.php index 764a0cc178441..70b6c94e734cc 100644 --- a/tests/phpunit/tests/formatting/WpReplaceInHtmlTags.php +++ b/tests/phpunit/tests/formatting/wpReplaceInHtmlTags.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_WpReplaceInTags extends WP_UnitTestCase { +class Tests_Formatting_wpReplaceInHtmlTags extends WP_UnitTestCase { /** * Check for expected behavior of new function wp_replace_in_html_tags(). * diff --git a/tests/phpunit/tests/formatting/WpRichEditPre.php b/tests/phpunit/tests/formatting/wpRicheditPre.php similarity index 94% rename from tests/phpunit/tests/formatting/WpRichEditPre.php rename to tests/phpunit/tests/formatting/wpRicheditPre.php index 79ed8b1bc6d18..553e42049c850 100644 --- a/tests/phpunit/tests/formatting/WpRichEditPre.php +++ b/tests/phpunit/tests/formatting/wpRicheditPre.php @@ -4,7 +4,7 @@ * @group formatting * @expectedDeprecated wp_richedit_pre */ -class Tests_Formatting_WpRichEditPre extends WP_UnitTestCase { +class Tests_Formatting_wpRicheditPre extends WP_UnitTestCase { function _charset_iso_8859_1() { return 'iso-8859-1'; diff --git a/tests/phpunit/tests/formatting/WPSlash.php b/tests/phpunit/tests/formatting/wpSlash.php similarity index 97% rename from tests/phpunit/tests/formatting/WPSlash.php rename to tests/phpunit/tests/formatting/wpSlash.php index 4b0d206c06866..bc31a93987149 100644 --- a/tests/phpunit/tests/formatting/WPSlash.php +++ b/tests/phpunit/tests/formatting/wpSlash.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_WPSlash extends WP_UnitTestCase { +class Tests_Formatting_wpSlash extends WP_UnitTestCase { /** * @ticket 42195 diff --git a/tests/phpunit/tests/formatting/WPSpecialchars.php b/tests/phpunit/tests/formatting/wpSpecialchars.php similarity index 98% rename from tests/phpunit/tests/formatting/WPSpecialchars.php rename to tests/phpunit/tests/formatting/wpSpecialchars.php index 7cc131da04ea5..59169cd574ec3 100644 --- a/tests/phpunit/tests/formatting/WPSpecialchars.php +++ b/tests/phpunit/tests/formatting/wpSpecialchars.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_WPSpecialchars extends WP_UnitTestCase { +class Tests_Formatting_wpSpecialchars extends WP_UnitTestCase { function test_wp_specialchars_basics() { $html = '&<hello world>'; $this->assertSame( $html, _wp_specialchars( $html ) ); diff --git a/tests/phpunit/tests/formatting/WPStripAllTags.php b/tests/phpunit/tests/formatting/wpStripAllTags.php similarity index 93% rename from tests/phpunit/tests/formatting/WPStripAllTags.php rename to tests/phpunit/tests/formatting/wpStripAllTags.php index 8656af26957df..cbeb3214a7202 100644 --- a/tests/phpunit/tests/formatting/WPStripAllTags.php +++ b/tests/phpunit/tests/formatting/wpStripAllTags.php @@ -4,7 +4,7 @@ * * @group formatting */ -class Tests_Formatting_WPStripAllTags extends WP_UnitTestCase { +class Tests_Formatting_wpStripAllTags extends WP_UnitTestCase { function test_wp_strip_all_tags() { diff --git a/tests/phpunit/tests/formatting/WPTargetedLinkRel.php b/tests/phpunit/tests/formatting/wpTargetedLinkRel.php similarity index 98% rename from tests/phpunit/tests/formatting/WPTargetedLinkRel.php rename to tests/phpunit/tests/formatting/wpTargetedLinkRel.php index 932100c1cdae2..b58f4188b58cb 100644 --- a/tests/phpunit/tests/formatting/WPTargetedLinkRel.php +++ b/tests/phpunit/tests/formatting/wpTargetedLinkRel.php @@ -4,7 +4,7 @@ * @group formatting * @ticket 43187 */ -class Tests_Targeted_Link_Rel extends WP_UnitTestCase { +class Tests_Formatting_wpTargetedLinkRel extends WP_UnitTestCase { public function test_add_to_links_with_target_blank() { $content = '

Links: No rel

'; diff --git a/tests/phpunit/tests/formatting/WPTexturize.php b/tests/phpunit/tests/formatting/wpTexturize.php similarity index 99% rename from tests/phpunit/tests/formatting/WPTexturize.php rename to tests/phpunit/tests/formatting/wpTexturize.php index fda1fa9730c57..bfad5c707ad13 100644 --- a/tests/phpunit/tests/formatting/WPTexturize.php +++ b/tests/phpunit/tests/formatting/wpTexturize.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_WPTexturize extends WP_UnitTestCase { +class Tests_Formatting_wpTexturize extends WP_UnitTestCase { function test_dashes() { $this->assertSame( 'Hey — boo?', wptexturize( 'Hey -- boo?' ) ); $this->assertSame( 'Hey — boo?', wptexturize( 'Hey -- boo?' ) ); diff --git a/tests/phpunit/tests/formatting/WpTrimExcerpt.php b/tests/phpunit/tests/formatting/wpTrimExcerpt.php similarity index 96% rename from tests/phpunit/tests/formatting/WpTrimExcerpt.php rename to tests/phpunit/tests/formatting/wpTrimExcerpt.php index 01353c8e22ac9..f05e21fb2a613 100644 --- a/tests/phpunit/tests/formatting/WpTrimExcerpt.php +++ b/tests/phpunit/tests/formatting/wpTrimExcerpt.php @@ -4,7 +4,7 @@ * @group formatting * @covers ::wp_trim_excerpt */ -class Tests_Formatting_WpTrimExcerpt extends WP_UnitTestCase { +class Tests_Formatting_wpTrimExcerpt extends WP_UnitTestCase { /** * @ticket 25349 */ diff --git a/tests/phpunit/tests/formatting/WPTrimWords.php b/tests/phpunit/tests/formatting/wpTrimWords.php similarity index 98% rename from tests/phpunit/tests/formatting/WPTrimWords.php rename to tests/phpunit/tests/formatting/wpTrimWords.php index b7ec436df0f1b..3f18d782a6137 100644 --- a/tests/phpunit/tests/formatting/WPTrimWords.php +++ b/tests/phpunit/tests/formatting/wpTrimWords.php @@ -3,7 +3,7 @@ /** * @group formatting */ -class Tests_Formatting_WPTrimWords extends WP_UnitTestCase { +class Tests_Formatting_wpTrimWords extends WP_UnitTestCase { /** * Long Dummy Text. diff --git a/tests/phpunit/tests/formatting/Zeroise.php b/tests/phpunit/tests/formatting/zeroise.php similarity index 100% rename from tests/phpunit/tests/formatting/Zeroise.php rename to tests/phpunit/tests/formatting/zeroise.php From d93f76dca87687d72951bc0be18bd56f4b23bb4d Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 16 Aug 2021 22:16:32 +0000 Subject: [PATCH 003/257] Code Modernization: Correct handling of `null` in `wp_parse_str()`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes `parse_str(): Passing null to parameter #1 ($string) of type string is deprecated` notices on PHP 8.1, without change in behaviour. Impact: 311 of the pre-existing tests are affected by this issue. The PHP native `parse_str()` function expects a string, however, based on the failing tests, it is clear there are functions in WordPress which passes a non-string – including `null` – value to the `wp_parse_str()` function, which would subsequently pass it onto the PHP native function without further input validation. Most notable offender is the `wp_parse_args()` function which special cases arrays and objects, but passes everything else off to `wp_parse_str()`. Several ways to fix this issue have been explored, including checking the received value with `is_string()` or `is_scalar()` before passing it off to the PHP native `parse_str()` function. In the end it was decided against these in favor of a string cast as: * `is_string()` would significantly change the behavior for anything non-string. * `is_scalar()` up to a point as well, as it does not take objects with a `__toString()` method into account. Executing a string cast on the received value before passing it on maintains the pre-existing behavior while still preventing the deprecation notice coming from PHP 8.1. Reference: [https://www.php.net/manual/en/function.parse-str.php PHP Manual: parse_str()] Follow-up to [5709]. Props jrf, hellofromTonya, lucatume, SergeyBiryukov. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51624 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/formatting.php | 2 +- tests/phpunit/tests/formatting/wpParseStr.php | 144 ++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 tests/phpunit/tests/formatting/wpParseStr.php diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index edfdd1e416755..b80c40cdda601 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -4964,7 +4964,7 @@ function map_deep( $value, $callback ) { * @param array $array Variables will be stored in this array. */ function wp_parse_str( $string, &$array ) { - parse_str( $string, $array ); + parse_str( (string) $string, $array ); /** * Filters the array of variables derived from a parsed string. diff --git a/tests/phpunit/tests/formatting/wpParseStr.php b/tests/phpunit/tests/formatting/wpParseStr.php new file mode 100644 index 0000000000000..b8c3d063d2423 --- /dev/null +++ b/tests/phpunit/tests/formatting/wpParseStr.php @@ -0,0 +1,144 @@ +assertSame( $expected, $output ); + } + + /** + * Data Provider. + * + * @return array + */ + public function data_wp_parse_str() { + return array( + 'null' => array( + 'input' => null, + 'expected' => array(), + ), + 'boolean false' => array( + 'input' => false, + 'expected' => array(), + ), + 'boolean true' => array( + 'input' => true, + 'expected' => array( + 1 => '', + ), + ), + 'integer 0' => array( + 'input' => 0, + 'expected' => array( + 0 => '', + ), + ), + 'integer 456' => array( + 'input' => 456, + 'expected' => array( + 456 => '', + ), + ), + 'float 12.53' => array( + 'input' => 12.53, + 'expected' => array( + '12_53' => '', + ), + ), + 'plain string' => array( + 'input' => 'foobar', + 'expected' => array( + 'foobar' => '', + ), + ), + 'query string' => array( + 'input' => 'x=5&_baba=dudu&', + 'expected' => array( + 'x' => '5', + '_baba' => 'dudu', + ), + ), + 'stringable object' => array( + 'input' => new Fixture_Formatting_wpParseStr(), + 'expected' => array( + 'foobar' => '', + ), + ), + ); + } + + /** + * Tests that the result array only contains the result of the string parsing + * when provided with different types of input for the `$output` parameter. + * + * @dataProvider data_wp_parse_str_result_array_is_always_overwritten + * + * @param array|null $output Value for the `$output` parameter. + * @param array $expected Expected function output. + */ + public function test_wp_parse_str_result_array_is_always_overwritten( $output, $expected ) { + wp_parse_str( 'key=25&thing=text', $output ); + $this->assertSame( $expected, $output ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_parse_str_result_array_is_always_overwritten() { + // Standard value for expected output. + $expected = array( + 'key' => '25', + 'thing' => 'text', + ); + + return array( + 'output null' => array( + 'output' => null, + 'expected' => $expected, + ), + 'output empty array' => array( + 'output' => array(), + 'expected' => $expected, + ), + 'output non empty array, no conflicting keys' => array( + 'output' => array( + 'foo' => 'bar', + ), + 'expected' => $expected, + ), + 'output non empty array, conflicting keys' => array( + 'output' => array( + 'key' => 'value', + ), + 'expected' => $expected, + ), + ); + } +} + +/** + * Fixture for use in the tests. + */ +class Fixture_Formatting_wpParseStr { + public function __toString() { + return 'foobar'; + } +} From d7c22d29cfb6c813bf2318006f2cf399bccf1a2d Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 16 Aug 2021 22:51:47 +0000 Subject: [PATCH 004/257] Code Modernization: Check the input type in `validate_file()`. This fixes a `preg_match_all(): Passing null to parameter #2 ($subject) of type string is deprecated` notice on PHP 8.1. The behavior for `null` and `string` input is covered by the existing `Tests_Functions::test_validate_file()` test. Effect: Errors down by 238, assertions up by 1920, failures down by 1. Props jrf, hellofromTonya, SergeyBiryukov. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51625 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 33344f31532d3..7b01568ad42c0 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5709,6 +5709,10 @@ function iis7_supports_permalinks() { * @return int 0 means nothing is wrong, greater than 0 means something was wrong. */ function validate_file( $file, $allowed_files = array() ) { + if ( ! is_scalar( $file ) || '' === $file ) { + return 0; + } + // `../` on its own is not allowed: if ( '../' === $file ) { return 1; From 5f5895bf61dfe62890e296ad7acd81aad9bde88b Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 17 Aug 2021 00:14:20 +0000 Subject: [PATCH 005/257] Code Modernization: Check the return type of `parse_url()` in `download_url()`. As per the PHP manual: > If the `component` parameter is omitted, an associative array is returned. > If the `component` parameter is specified, `parse_url()` returns a string (or an int, in the case of `PHP_URL_PORT`) instead of an array. If the requested component doesn't exist within the given URL, `null` will be returned. Reference: [https://www.php.net/manual/en/function.parse-url.php#refsect1-function.parse-url-returnvalues PHP Manual: parse_url(): Return Values] This commit adds three unit tests for `download_url()`: * The first test is "girl-scouting" to make sure that the code up to the point where the error is expected is tested. * The second test exposed a PHP 8.1 `basename(): Passing null to parameter #1 ($path) of type string is deprecated` error due to the call to `parse_url()` returning `null` when the component requested does not exist in the passed URL. * The output of the call to `parse_url()` stored in the `$url_path` variable is used in more places in the function logic. The third test exposes a second PHP 8.1 deprecation notice, this time for `substr(): Passing null to parameter #1 ($string) of type string is deprecated`. This commit also removes duplicate `parse_url()` calls. Neither `$url` nor `$url_filename` are changed between when they are first received/defined and when they are re-used, so there is no need to repeat the function calls. Follow-up to [51606], [51622]. Props jrf, hellofromTonya, SergeyBiryukov. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51626 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/file.php | 11 ++-- tests/phpunit/tests/admin/includesFile.php | 74 ++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 052c235e15724..8c4797d11398d 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -1126,7 +1126,11 @@ function download_url( $url, $timeout = 300, $signature_verification = false ) { return new WP_Error( 'http_no_url', __( 'Invalid URL Provided.' ) ); } - $url_filename = basename( parse_url( $url, PHP_URL_PATH ) ); + $url_path = parse_url( $url, PHP_URL_PATH ); + $url_filename = ''; + if ( is_string( $url_path ) && '' !== $url_path ) { + $url_filename = basename( $url_path ); + } $tmpfname = wp_tempnam( $url_filename ); if ( ! $tmpfname ) { @@ -1212,9 +1216,8 @@ function download_url( $url, $timeout = 300, $signature_verification = false ) { // WordPress.org stores signatures at $package_url.sig. $signature_url = false; - $url_path = parse_url( $url, PHP_URL_PATH ); - if ( '.zip' === substr( $url_path, -4 ) || '.tar.gz' === substr( $url_path, -7 ) ) { + if ( is_string( $url_path ) && ( '.zip' === substr( $url_path, -4 ) || '.tar.gz' === substr( $url_path, -7 ) ) ) { $signature_url = str_replace( $url_path, $url_path . '.sig', $url ); } @@ -1243,7 +1246,7 @@ function download_url( $url, $timeout = 300, $signature_verification = false ) { } // Perform the checks. - $signature_verification = verify_file_signature( $tmpfname, $signature, basename( parse_url( $url, PHP_URL_PATH ) ) ); + $signature_verification = verify_file_signature( $tmpfname, $signature, $url_filename ); } if ( is_wp_error( $signature_verification ) ) { diff --git a/tests/phpunit/tests/admin/includesFile.php b/tests/phpunit/tests/admin/includesFile.php index a3af5da2d1c9c..5c58cbfcd4023 100644 --- a/tests/phpunit/tests/admin/includesFile.php +++ b/tests/phpunit/tests/admin/includesFile.php @@ -77,4 +77,78 @@ public function _fake_download_url_non_200_response_code( $response, $args, $url public function __return_5() { return 5; } + + /** + * Verify that a WP_Error object is returned when invalid input is passed as the `$url` parameter. + * + * @covers ::download_url + * @dataProvider data_download_url_empty_url + * + * @param mixed $url Input URL. + */ + public function test_download_url_empty_url( $url ) { + $error = download_url( $url ); + $this->assertWPError( $error ); + $this->assertSame( 'http_no_url', $error->get_error_code() ); + $this->assertSame( 'Invalid URL Provided.', $error->get_error_message() ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_download_url_empty_url() { + return array( + 'null' => array( null ), + 'false' => array( false ), + 'integer 0' => array( 0 ), + 'empty string' => array( '' ), + 'string 0' => array( '0' ), + ); + } + + /** + * Test that PHP 8.1 "passing null to non-nullable" deprecation notice + * is not thrown when the `$url` does not have a path component. + * + * @ticket 53635 + * @covers ::download_url + */ + public function test_download_url_no_warning_for_url_without_path() { + $result = download_url( 'https://example.com' ); + + $this->assertIsString( $result ); + $this->assertNotEmpty( $result ); // File path will be generated, but will never be empty. + } + + /** + * Test that PHP 8.1 "passing null to non-nullable" deprecation notice + * is not thrown when the `$url` does not have a path component, + * and signature verification via a local file is requested. + * + * @ticket 53635 + * @covers ::download_url + */ + public function test_download_url_no_warning_for_url_without_path_with_signature_verification() { + add_filter( + 'wp_signature_hosts', + static function( $urls ) { + $urls[] = 'example.com'; + return $urls; + } + ); + $error = download_url( 'https://example.com', 300, true ); + + /* + * Note: This test is not testing the signature verification itself. + * There is no signature available for the domain used in the test, + * which is why an error is expected and that's fine. + * The point of the test is to verify that the call to `verify_file_signature()` + * is actually reached and that no PHP deprecation notice is thrown + * before this point. + */ + $this->assertWPError( $error ); + $this->assertSame( 'signature_verification_no_signature', $error->get_error_code() ); + } } From 6d574d7cabfaf6cc9797d2119ba1a10d9cd78fa3 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 17 Aug 2021 00:27:38 +0000 Subject: [PATCH 006/257] Tests: Use a better return type check for `parse_url()` in `do_enclose()` tests. Since the `pathinfo()` function accepts a string, checking for that specifically is more consistent with similar checks elsewhere in core. Follow-up to [51606], [51622], [51626]. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51627 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/functions/doEnclose.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/functions/doEnclose.php b/tests/phpunit/tests/functions/doEnclose.php index 30a8e73dac0c2..5c8a42089fba7 100644 --- a/tests/phpunit/tests/functions/doEnclose.php +++ b/tests/phpunit/tests/functions/doEnclose.php @@ -280,7 +280,7 @@ public function fake_http_request( $false, $arguments, $url ) { $path = parse_url( $url, PHP_URL_PATH ); - if ( null !== $path ) { + if ( is_string( $path ) ) { $extension = pathinfo( $path, PATHINFO_EXTENSION ); if ( isset( $fake_headers[ $extension ] ) ) { return $fake_headers[ $extension ]; From 16b448416bcfc7ea9757ade3bfc9603a6b886a11 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 17 Aug 2021 20:01:15 +0000 Subject: [PATCH 007/257] Tests: Move loading the `PO` class to `set_up_before_class()`. This ensures that the class is loaded once before the first test of the test case class is run, and `require_once()` is not unnecessarily called for each test method individually. Follow-up to [1106/tests]. See #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51628 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/pomo/po.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/pomo/po.php b/tests/phpunit/tests/pomo/po.php index c79b37033f77a..9a37b4dd4eabd 100644 --- a/tests/phpunit/tests/pomo/po.php +++ b/tests/phpunit/tests/pomo/po.php @@ -4,9 +4,16 @@ * @group pomo */ class Tests_POMO_PO extends WP_UnitTestCase { - function set_up() { - parent::set_up(); + + public static function set_up_before_class() { + parent::set_up_before_class(); + require_once ABSPATH . '/wp-includes/pomo/po.php'; + } + + public function set_up() { + parent::set_up(); + // Not so random wordpress.pot string -- multiple lines. $this->mail = 'Your new WordPress blog has been successfully set up at: From 0f51a986022931dbe8b945ef5d2bb0814c314af3 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 17 Aug 2021 21:55:22 +0000 Subject: [PATCH 008/257] Code Modernization: Check the return type of `parse_url()` on Plugin/Theme Editor screens. As per the PHP manual: > If the `component` parameter is omitted, an associative array is returned. > If the `component` parameter is specified, `parse_url()` returns a string (or an int, in the case of `PHP_URL_PORT`) instead of an array. If the requested component doesn't exist within the given URL, `null` will be returned. Reference: [https://www.php.net/manual/en/function.parse-url.php#refsect1-function.parse-url-returnvalues PHP Manual: parse_url(): Return Values] While it is probably unlikely that someone would have a direct link to the plugin/theme editor on their home page or even on someone else's homepage, it is entirely possible for the referrer URL to not have a "path" component. In PHP 8.1, this would lead to a `basename(): Passing null to parameter #1 ($string) of type string is deprecated` notice. Changing the logic around and adding validation for the return type value of `parse_url()` prevents that. Follow-up to [51606], [51622], [51626]. Props jrf, hellofromTonya, SergeyBiryukov. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51629 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/plugin-editor.php | 10 ++++++---- src/wp-admin/theme-editor.php | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/wp-admin/plugin-editor.php b/src/wp-admin/plugin-editor.php index 36dce7795d9d5..800e468f3f9f8 100644 --- a/src/wp-admin/plugin-editor.php +++ b/src/wp-admin/plugin-editor.php @@ -312,10 +312,12 @@ $excluded_referer_basenames = array( 'plugin-editor.php', 'wp-login.php' ); - if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) { - $return_url = $referer; - } else { - $return_url = admin_url( '/' ); + $return_url = admin_url( '/' ); + if ( $referer ) { + $referer_path = parse_url( $referer, PHP_URL_PATH ); + if ( is_string( $referer_path ) && ! in_array( basename( $referer_path ), $excluded_referer_basenames, true ) ) { + $return_url = $referer; + } } ?>
diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 572a2d82c4631..46926df87ce3b 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -343,10 +343,12 @@ $excluded_referer_basenames = array( 'theme-editor.php', 'wp-login.php' ); - if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) { - $return_url = $referer; - } else { - $return_url = admin_url( '/' ); + $return_url = admin_url( '/' ); + if ( $referer ) { + $referer_path = parse_url( $referer, PHP_URL_PATH ); + if ( is_string( $referer_path ) && ! in_array( basename( $referer_path ), $excluded_referer_basenames, true ) ) { + $return_url = $referer; + } } ?>
From 6ec2594d74fb25140917d8caf41c259498fa43c4 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 17 Aug 2021 22:07:55 +0000 Subject: [PATCH 009/257] Code Modernization: Check the return type of `parse_url()` in `ms_cookie_constants()`. As per the PHP manual: > If the `component` parameter is omitted, an associative array is returned. > If the `component` parameter is specified, `parse_url()` returns a string (or an int, in the case of `PHP_URL_PORT`) instead of an array. If the requested component doesn't exist within the given URL, `null` will be returned. Reference: [https://www.php.net/manual/en/function.parse-url.php#refsect1-function.parse-url-returnvalues PHP Manual: parse_url(): Return Values] It is entirely possible for the `siteurl` option to not have a "path" component. In PHP 8.1, this would lead to a `trim(): Passing null to parameter #1 ($string) of type string is deprecated` notice. Changing the logic around and adding validation for the return type value of `parse_url()` prevents that. As this function is declaring global constants, adding tests for this change is not really an option without potentially affecting other tests. Follow-up to [51606], [51622], [51626], [51629]. Props jrf, hellofromTonya, SergeyBiryukov. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51630 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/ms-default-constants.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/ms-default-constants.php b/src/wp-includes/ms-default-constants.php index 7d794e32e6523..016ed437b7146 100644 --- a/src/wp-includes/ms-default-constants.php +++ b/src/wp-includes/ms-default-constants.php @@ -68,7 +68,8 @@ function ms_cookie_constants() { * @since 2.6.0 */ if ( ! defined( 'ADMIN_COOKIE_PATH' ) ) { - if ( ! is_subdomain_install() || trim( parse_url( get_option( 'siteurl' ), PHP_URL_PATH ), '/' ) ) { + $site_path = parse_url( get_option( 'siteurl' ), PHP_URL_PATH ); + if ( ! is_subdomain_install() || is_string( $site_path ) && trim( $site_path, '/' ) ) { define( 'ADMIN_COOKIE_PATH', SITECOOKIEPATH ); } else { define( 'ADMIN_COOKIE_PATH', SITECOOKIEPATH . 'wp-admin' ); From acfe40f335fd763ebcea6873cd65e6ea110ecf53 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 18 Aug 2021 05:37:01 +0000 Subject: [PATCH 010/257] Media: Fix layout of media library modal on narrow screens. Reduces the right margin of the media library modal on small and medium width screens to remove excess white-space. On very narrow screens this was preventing the media icons from displaying. Props andraganescu, desrosj, joedolson, moch11, mukesh27, sabernhardt, SergeyBiryukov, zieladam. Fixes #53679. git-svn-id: https://develop.svn.wordpress.org/trunk@51631 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/css/media-views.css | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/css/media-views.css b/src/wp-includes/css/media-views.css index 784903db6d745..b3dca00ea1d47 100644 --- a/src/wp-includes/css/media-views.css +++ b/src/wp-includes/css/media-views.css @@ -2617,7 +2617,9 @@ .attachments-browser .attachments, .attachments-browser .uploader-inline, - .attachments-browser .media-toolbar { + .attachments-browser .media-toolbar, + .attachments-browser .attachments-wrapper, + .attachments-browser.has-load-more .attachments-wrapper { right: 262px; } @@ -2884,10 +2886,15 @@ .attachments-browser .attachments, .attachments-browser .uploader-inline, - .attachments-browser .media-toolbar { + .attachments-browser .media-toolbar, + .media-frame-content .attachments-browser .attachments-wrapper { right: 0; } + .attachments-browser .attachments-wrapper { + padding-top: 12px; + } + .image-details .media-frame-title { display: block; top: 0; From cd616cf406d77a5eb653aa97cc5b6e2dc3e669a6 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 18 Aug 2021 05:59:28 +0000 Subject: [PATCH 011/257] Media: Increase number of media items displayed per page. Increase the number of media items displayed per page from 40 to 80 to improve the experience for users navigating the library on sites with a large quantity of media in the library. Props AlGala, antpb, hellofromTonya, joedolson, SergeyBiryukov, wb1234. Fixes #53827. git-svn-id: https://develop.svn.wordpress.org/trunk@51632 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/media/models/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/media/models/query.js b/src/js/media/models/query.js index 1d56fed624694..b3f62018f5cd4 100644 --- a/src/js/media/models/query.js +++ b/src/js/media/models/query.js @@ -171,7 +171,7 @@ Query = Attachments.extend(/** @lends wp.media.model.Query.prototype */{ * @readonly */ defaultArgs: { - posts_per_page: 40 + posts_per_page: 80 }, /** * @readonly From 27a2a804a33a3d414c33d7d543743455325e22fb Mon Sep 17 00:00:00 2001 From: SergeyBiryukov Date: Wed, 18 Aug 2021 13:22:02 +0000 Subject: [PATCH 012/257] Code Modernization: Only set `auto_detect_line_endings` in PHP < 8.1. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since PHP 8.1, the `auto_detect_line_endings` setting is deprecated: > The `auto_detect_line_endings` ini setting modifies the behavior of `file()` and `fgets()` to support an isolated `\r` (as opposed to `\n` or `\r\n`) as a newline character. These newlines were used by “Classic” Mac OS, a system which has been discontinued in 2001, nearly two decades ago. Interoperability with such systems is no longer relevant. Reference: [https://wiki.php.net/rfc/deprecations_php_8_1#auto_detect_line_endings_ini_setting PHP RFC: Deprecations for PHP 8.1: auto_detect_line_endings ini setting] > The `auto_detect_line_endings` ini setting has been deprecated. If necessary, handle `\r` line breaks manually instead. Reference: [https://github.com/php/php-src/blob/1cf4fb739f7a4fa8404a4c0958f13d04eae519d4/UPGRADING#L456-L457 PHP 8.1 Upgrade Notes]. This commit fixes the warning when running tests for the `PO` class: {{{ Deprecated: auto_detect_line_endings is deprecated in /var/www/src/wp-includes/pomo/po.php on line 16 }}} Follow-up to [10584], [51628]. See #53635. git-svn-id: https://develop.svn.wordpress.org/trunk@51633 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/pomo/po.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/pomo/po.php b/src/wp-includes/pomo/po.php index 99bb6891b1040..22b94c11bf972 100644 --- a/src/wp-includes/pomo/po.php +++ b/src/wp-includes/pomo/po.php @@ -13,7 +13,10 @@ define( 'PO_MAX_LINE_LEN', 79 ); } -ini_set( 'auto_detect_line_endings', 1 ); +// This setting has been deprecated in PHP 8.1. +if ( PHP_VERSION_ID < 80100 ) { + ini_set( 'auto_detect_line_endings', 1 ); +} /** * Routines for working with PO files From 9aa15898070935370fc9ca3b7aaa1682ba63e818 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 18 Aug 2021 13:52:16 +0000 Subject: [PATCH 013/257] External Libraries: Upgrade PHPMailer to version 6.5.1. The latest release includes preliminary PHP 8.1 support, as well as some small bug fixes. Release notes: https://github.com/PHPMailer/PHPMailer/releases/tag/v6.5.1 For a full list of changes in this update, see the PHPMailer GitHub: https://github.com/PHPMailer/PHPMailer/compare/v6.5.0...v6.5.1 Follow-up to [50628], [50799], [51169]. Props jrf. Fixes #53953. git-svn-id: https://develop.svn.wordpress.org/trunk@51634 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/PHPMailer/Exception.php | 2 +- src/wp-includes/PHPMailer/PHPMailer.php | 135 +++++++++++++++++------- src/wp-includes/PHPMailer/SMTP.php | 2 +- 3 files changed, 98 insertions(+), 41 deletions(-) diff --git a/src/wp-includes/PHPMailer/Exception.php b/src/wp-includes/PHPMailer/Exception.php index a50a8991f7a1d..52eaf95158a37 100644 --- a/src/wp-includes/PHPMailer/Exception.php +++ b/src/wp-includes/PHPMailer/Exception.php @@ -35,6 +35,6 @@ class Exception extends \Exception */ public function errorMessage() { - return '' . htmlspecialchars($this->getMessage()) . "
\n"; + return '' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "
\n"; } } diff --git a/src/wp-includes/PHPMailer/PHPMailer.php b/src/wp-includes/PHPMailer/PHPMailer.php index 5618251950c95..5b6dcfad6da95 100644 --- a/src/wp-includes/PHPMailer/PHPMailer.php +++ b/src/wp-includes/PHPMailer/PHPMailer.php @@ -103,14 +103,14 @@ class PHPMailer * * @var string */ - public $From = 'root@localhost'; + public $From = ''; /** * The From name of the message. * * @var string */ - public $FromName = 'Root User'; + public $FromName = ''; /** * The envelope sender of the message. @@ -689,7 +689,7 @@ class PHPMailer protected $boundary = []; /** - * The array of available languages. + * The array of available text strings for the current language. * * @var array */ @@ -750,7 +750,7 @@ class PHPMailer * * @var string */ - const VERSION = '6.5.0'; + const VERSION = '6.5.1'; /** * Error severity: message only, continue processing. @@ -858,7 +858,7 @@ public function __destruct() private function mailPassthru($to, $subject, $body, $header, $params) { //Check overloading of mail function to avoid double-encoding - if (ini_get('mbstring.func_overload') & 1) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated + if (ini_get('mbstring.func_overload') & 1) { $subject = $this->secureHeader($subject); } else { $subject = $this->encodeHeader($this->secureHeader($subject)); @@ -1188,25 +1188,33 @@ protected function addAnAddress($kind, $address, $name = '') * * @return array */ - public static function parseAddresses($addrstr, $useimap = true) + public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591) { $addresses = []; if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { //Use this built-in parser if it's available $list = imap_rfc822_parse_adrlist($addrstr, ''); + // Clear any potential IMAP errors to get rid of notices being thrown at end of script. + imap_errors(); foreach ($list as $address) { if ( - ('.SYNTAX-ERROR.' !== $address->host) && static::validateAddress( - $address->mailbox . '@' . $address->host - ) + '.SYNTAX-ERROR.' !== $address->host && + static::validateAddress($address->mailbox . '@' . $address->host) ) { //Decode the name part if it's present and encoded if ( property_exists($address, 'personal') && - extension_loaded('mbstring') && - preg_match('/^=\?.*\?=$/', $address->personal) + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + defined('MB_CASE_UPPER') && + preg_match('/^=\?.*\?=$/s', $address->personal) ) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $address->personal = str_replace('_', '=20', $address->personal); + //Decode the name $address->personal = mb_decode_mimeheader($address->personal); + mb_internal_encoding($origCharset); } $addresses[] = [ @@ -1234,9 +1242,16 @@ public static function parseAddresses($addrstr, $useimap = true) $email = trim(str_replace('>', '', $email)); $name = trim($name); if (static::validateAddress($email)) { + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled //If this name is encoded, decode it - if (preg_match('/^=\?.*\?=$/', $name)) { + if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $name = str_replace('_', '=20', $name); + //Decode the name $name = mb_decode_mimeheader($name); + mb_internal_encoding($origCharset); } $addresses[] = [ //Remove any surrounding quotes and spaces from the name @@ -1439,11 +1454,9 @@ public function punyencodeAddress($address) $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_UTS46); } elseif (defined('INTL_IDNA_VARIANT_2003')) { //Fall back to this old, deprecated/removed encoding - // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003); } else { //Fall back to a default we don't know about - // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet $punycode = idn_to_ascii($domain, $errorcode); } if (false !== $punycode) { @@ -1510,12 +1523,7 @@ public function preSend() && ini_get('mail.add_x_header') === '1' && stripos(PHP_OS, 'WIN') === 0 ) { - trigger_error( - 'Your version of PHP is affected by a bug that may result in corrupted messages.' . - ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . - ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', - E_USER_WARNING - ); + trigger_error($this->lang('buggy_php'), E_USER_WARNING); } try { @@ -1726,7 +1734,7 @@ protected function sendmailSend($header, $body) fwrite($mail, $header); fwrite($mail, $body); $result = pclose($mail); - $addrinfo = static::parseAddresses($toAddr); + $addrinfo = static::parseAddresses($toAddr, true, $this->charSet); $this->doCallback( ($result === 0), [[$addrinfo['address'], $addrinfo['name']]], @@ -1886,7 +1894,7 @@ protected function mailSend($header, $body) if ($this->SingleTo && count($toArr) > 1) { foreach ($toArr as $toAddr) { $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); - $addrinfo = static::parseAddresses($toAddr); + $addrinfo = static::parseAddresses($toAddr, true, $this->charSet); $this->doCallback( $result, [[$addrinfo['address'], $addrinfo['name']]], @@ -2183,14 +2191,15 @@ public function smtpClose() /** * Set the language for error messages. - * Returns false if it cannot load the language file. * The default language is English. * * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") + * Optionally, the language code can be enhanced with a 4-character + * script annotation and/or a 2-character country annotation. * @param string $lang_path Path to the language file directory, with trailing separator (slash).D * Do not set this from user input! * - * @return bool + * @return bool Returns true if the requested language was loaded, false otherwise. */ public function setLanguage($langcode = 'en', $lang_path = '') { @@ -2213,44 +2222,77 @@ public function setLanguage($langcode = 'en', $lang_path = '') //Define full set of translatable strings in English $PHPMAILER_LANG = [ 'authenticate' => 'SMTP Error: Could not authenticate.', + 'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' . + ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . + ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', 'data_not_accepted' => 'SMTP Error: data not accepted.', 'empty_message' => 'Message body empty', 'encoding' => 'Unknown encoding: ', 'execute' => 'Could not execute: ', + 'extension_missing' => 'Extension missing: ', 'file_access' => 'Could not access file: ', 'file_open' => 'File Error: Could not open file: ', 'from_failed' => 'The following From address failed: ', 'instantiate' => 'Could not instantiate mail function.', 'invalid_address' => 'Invalid address: ', + 'invalid_header' => 'Invalid header name or value', 'invalid_hostentry' => 'Invalid hostentry: ', 'invalid_host' => 'Invalid host: ', 'mailer_not_supported' => ' mailer is not supported.', 'provide_address' => 'You must provide at least one recipient email address.', 'recipients_failed' => 'SMTP Error: The following recipients failed: ', 'signing' => 'Signing Error: ', + 'smtp_code' => 'SMTP code: ', + 'smtp_code_ex' => 'Additional SMTP info: ', 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_detail' => 'Detail: ', 'smtp_error' => 'SMTP server error: ', 'variable_set' => 'Cannot set or reset variable: ', - 'extension_missing' => 'Extension missing: ', ]; if (empty($lang_path)) { //Calculate an absolute path so it can work if CWD is not here $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; } + //Validate $langcode - if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { + $foundlang = true; + $langcode = strtolower($langcode); + if ( + !preg_match('/^(?P[a-z]{2})(?P Date: Sat, 23 Oct 2021 12:36:15 +0000 Subject: [PATCH 225/257] Tests: Add `@ticket` references for `page_on_front` canonical tests. Follow-up to [669/tests], [849/tests], [36238], [47760]. See #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51928 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/canonical/pageOnFront.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/phpunit/tests/canonical/pageOnFront.php b/tests/phpunit/tests/canonical/pageOnFront.php index 8f6488a9ab2f2..64058ab4fe664 100644 --- a/tests/phpunit/tests/canonical/pageOnFront.php +++ b/tests/phpunit/tests/canonical/pageOnFront.php @@ -51,21 +51,21 @@ function data() { */ return array( // Check against an odd redirect. - array( '/page/2/', '/page/2/' ), - array( '/?page=2', '/page/2/' ), - array( '/page/1/', '/' ), - array( '/?page=1', '/' ), + array( '/page/2/', '/page/2/', 20385 ), + array( '/?page=2', '/page/2/', 35344 ), + array( '/page/1/', '/', 35344 ), + array( '/?page=1', '/', 35344 ), // The page designated as the front page should redirect to the front of the site. - array( '/front-page/', '/' ), + array( '/front-page/', '/', 20385 ), // The front page supports the pagination. - array( '/front-page/2/', '/page/2/' ), - array( '/front-page/?page=2', '/page/2/' ), + array( '/front-page/2/', '/page/2/', 35344 ), + array( '/front-page/?page=2', '/page/2/', 35344 ), // The posts page does not support the pagination. - array( '/blog-page/2/', '/blog-page/' ), - array( '/blog-page/?page=2', '/blog-page/' ), + array( '/blog-page/2/', '/blog-page/', 45337 ), + array( '/blog-page/?page=2', '/blog-page/', 45337 ), // The posts page supports regular pagination. - array( '/blog-page/?paged=2', '/blog-page/page/2/' ), + array( '/blog-page/?paged=2', '/blog-page/page/2/', 20385 ), ); } } From e6ba58cb68554f08cd7c1d6934fe1b77c5792e41 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 24 Oct 2021 19:21:18 +0000 Subject: [PATCH 226/257] Coding Standards: Rename the `$arrHeaders` variable to `$processed_headers` in `WP_Http_Streams::request()`. This fixes a `Variable "$arrHeaders" is not in valid snake_case format` WPCS warning. Follow-up to [8516], [51826]. See #53359. git-svn-id: https://develop.svn.wordpress.org/trunk@51929 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-http-streams.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/class-wp-http-streams.php b/src/wp-includes/class-wp-http-streams.php index d6cfc53359308..abf16dd44d2e8 100644 --- a/src/wp-includes/class-wp-http-streams.php +++ b/src/wp-includes/class-wp-http-streams.php @@ -316,14 +316,14 @@ public function request( $url, $args = array() ) { fclose( $handle ); - $arrHeaders = WP_Http::processHeaders( $process['headers'], $url ); + $processed_headers = WP_Http::processHeaders( $process['headers'], $url ); $response = array( - 'headers' => $arrHeaders['headers'], + 'headers' => $processed_headers['headers'], // Not yet processed. 'body' => null, - 'response' => $arrHeaders['response'], - 'cookies' => $arrHeaders['cookies'], + 'response' => $processed_headers['response'], + 'cookies' => $processed_headers['cookies'], 'filename' => $parsed_args['filename'], ); @@ -334,13 +334,13 @@ public function request( $url, $args = array() ) { } // If the body was chunk encoded, then decode it. - if ( ! empty( $process['body'] ) && isset( $arrHeaders['headers']['transfer-encoding'] ) - && 'chunked' === $arrHeaders['headers']['transfer-encoding'] + if ( ! empty( $process['body'] ) && isset( $processed_headers['headers']['transfer-encoding'] ) + && 'chunked' === $processed_headers['headers']['transfer-encoding'] ) { $process['body'] = WP_Http::chunkTransferDecode( $process['body'] ); } - if ( true === $parsed_args['decompress'] && true === WP_Http_Encoding::should_decode( $arrHeaders['headers'] ) ) { + if ( true === $parsed_args['decompress'] && true === WP_Http_Encoding::should_decode( $processed_headers['headers'] ) ) { $process['body'] = WP_Http_Encoding::decompress( $process['body'] ); } From 2238c302f9598b6f8f4ab0c27b175fd9ca7370fd Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 25 Oct 2021 00:22:41 +0000 Subject: [PATCH 227/257] Docs: Use sign-up & signup consistently in `wp-signup.php`. In the docblocks throughout `wp-signup.php` use sign up for verbs and sign-up for nouns. Props audrasjb, jeffpaul. Fixes #54041. See #53399. git-svn-id: https://develop.svn.wordpress.org/trunk@51930 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-signup.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/wp-signup.php b/src/wp-signup.php index e13d6e49e771f..375dad9517c56 100644 --- a/src/wp-signup.php +++ b/src/wp-signup.php @@ -43,14 +43,14 @@ function do_signup_header() { $wp_query->is_404 = false; /** - * Fires before the Site Signup page is loaded. + * Fires before the Site Sign-up page is loaded. * * @since 4.4.0 */ do_action( 'before_signup_header' ); /** - * Prints styles for front-end Multisite signup pages. + * Prints styles for front-end Multisite Sign-up pages. * * @since MU (3.0.0) */ @@ -79,7 +79,7 @@ function wpmu_signup_stylesheet() { get_header( 'wp-signup' ); /** - * Fires before the site sign-up form. + * Fires before the site Sign-up form. * * @since 3.0.0 */ @@ -89,7 +89,7 @@ function wpmu_signup_stylesheet() {
-

+

Date: Mon, 25 Oct 2021 16:18:26 +0000 Subject: [PATCH 228/257] Coding Standards: Rename `$theHeaders` variable to `$processed_headers` in `WP_Http_Curl::request()`. This fixes a `Variable "$theHeaders" is not in valid snake_case format` WPCS warning. Follow-up to [8516], [8520], [51826], [51929]. See #53359. git-svn-id: https://develop.svn.wordpress.org/trunk@51931 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-http-curl.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-http-curl.php b/src/wp-includes/class-wp-http-curl.php index 64ff7efb834ab..c4505f2e6198e 100644 --- a/src/wp-includes/class-wp-http-curl.php +++ b/src/wp-includes/class-wp-http-curl.php @@ -256,7 +256,8 @@ public function request( $url, $args = array() ) { } curl_exec( $handle ); - $theHeaders = WP_Http::processHeaders( $this->headers, $url ); + + $processed_headers = WP_Http::processHeaders( $this->headers, $url ); $theBody = $this->body; $bytes_written_total = $this->bytes_written_total; @@ -267,7 +268,7 @@ public function request( $url, $args = array() ) { $curl_error = curl_errno( $handle ); // If an error occurred, or, no response. - if ( $curl_error || ( 0 == strlen( $theBody ) && empty( $theHeaders['headers'] ) ) ) { + if ( $curl_error || ( 0 == strlen( $theBody ) && empty( $processed_headers['headers'] ) ) ) { if ( CURLE_WRITE_ERROR /* 23 */ == $curl_error ) { if ( ! $this->max_body_length || $this->max_body_length != $bytes_written_total ) { if ( $parsed_args['stream'] ) { @@ -299,10 +300,10 @@ public function request( $url, $args = array() ) { } $response = array( - 'headers' => $theHeaders['headers'], + 'headers' => $processed_headers['headers'], 'body' => null, - 'response' => $theHeaders['response'], - 'cookies' => $theHeaders['cookies'], + 'response' => $processed_headers['response'], + 'cookies' => $processed_headers['cookies'], 'filename' => $parsed_args['filename'], ); @@ -312,7 +313,9 @@ public function request( $url, $args = array() ) { return $redirect_response; } - if ( true === $parsed_args['decompress'] && true === WP_Http_Encoding::should_decode( $theHeaders['headers'] ) ) { + if ( true === $parsed_args['decompress'] + && true === WP_Http_Encoding::should_decode( $processed_headers['headers'] ) + ) { $theBody = WP_Http_Encoding::decompress( $theBody ); } From 22bb0fee98a8eea57ff62933d889eb89f303356f Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Mon, 25 Oct 2021 19:28:04 +0000 Subject: [PATCH 229/257] Build/Test Tools: Restore Slack notifications for older branches. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In [51921], the GitHub Actions workflows were updated to utilize the Slack notifications workflow as a callable one instead of on the `workflow_run` event. This eliminated the need for an additional “Slack Notifications” workflow run for every completed workflow, but only when other workflows are updated as well. This resulted in notifications from older branches breaking, as the changes in [51921] were not backported. Instead of backporting the needed changes now (the Slack workflow is still being polished), this commit partially restores the `workflow_run` event for older branches so that notifications will resume. See #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51934 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/slack-notifications.yml | 40 ++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/slack-notifications.yml b/.github/workflows/slack-notifications.yml index ce8696608ffbe..aa944450851f0 100644 --- a/.github/workflows/slack-notifications.yml +++ b/.github/workflows/slack-notifications.yml @@ -6,6 +6,22 @@ name: Slack Notifications on: + workflow_run: + workflows: + - Code Coverage Report + - Coding Standards + - End-to-end Tests + - JavaScript Tests + - PHP Compatibility + - PHPUnit Tests + - Test NPM + - Test old branches + types: + - completed + branches: + - '[3-4].[0-9]' + - '5.[0-8]' + workflow_call: secrets: SLACK_GHA_SUCCESS_WEBHOOK: @@ -21,6 +37,9 @@ on: description: 'The Slack webhook URL for a failed build.' required: true +env: + CURRENT_BRANCH: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name }} + jobs: # Gathers the details needed for Slack notifications. # @@ -28,8 +47,11 @@ jobs: # submit data to Slack webhook URLs configured to post messages. # # Performs the following steps: + # - Retrieves the workflow ID (if necessary). + # - Retrieves the workflow URL (if necessary). # - Retrieves the previous workflow run and stores its conclusion. # - Sets the previous conclusion as an output. + # - Prepares the commit message. # - Constructs and stores a message payload as an output. prepare: name: Prepare notifications @@ -42,6 +64,7 @@ jobs: steps: - name: Get the workflow ID id: current-workflow-id + if: ${{ github.event_name == 'push' }} uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 with: script: | @@ -55,6 +78,7 @@ jobs: - name: Get the workflow URL id: current-workflow-url uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 + if: ${{ github.event_name == 'push' }} with: script: | const workflow_run = await github.rest.actions.getWorkflowRun({ @@ -72,8 +96,8 @@ jobs: const previous_runs = await github.rest.actions.listWorkflowRuns({ owner: '${{ github.repository_owner }}', repo: 'wordpress-develop', - workflow_id: ${{ steps.current-workflow-id.outputs.result }}, - branch: '${{ github.ref_name }}', + workflow_id: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.workflow_id || steps.current-workflow-id.outputs.result }}, + branch: '${{ env.CURRENT_BRANCH }}', per_page: 1, page: 2, }); @@ -87,21 +111,21 @@ jobs: id: commit-message run: | COMMIT_MESSAGE=$(cat <<'EOF' | awk 'NR==1' | sed 's/`/\\`/g' | sed 's/\"/\\\\"/g' - ${{ github.event.head_commit.message }} + ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_commit.message || github.event.head_commit.message }} EOF ) echo "::set-output name=commit_message_escaped::${COMMIT_MESSAGE}" - name: Construct payload and store as an output id: create-payload - run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.workflow }}\",\"ref_name\":\"${{ github.ref_name }}\",\"run_url\":\"${{ steps.current-workflow-url.outputs.result }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" + run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.workflow }}\",\"ref_name\":\"${{ env.CURRENT_BRANCH }}\",\"run_url\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.html_url || steps.current-workflow-url.outputs.result }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" # Posts notifications when a workflow fails. failure: name: Failure notifications runs-on: ubuntu-latest needs: [ prepare ] - if: ${{ failure() }} + if: ${{ github.event_name == 'push' && failure() || github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' }} steps: - name: Post failure notifications to Slack @@ -116,7 +140,7 @@ jobs: name: Fixed notifications runs-on: ubuntu-latest needs: [ prepare ] - if: ${{ needs.prepare.outputs.previous_conclusion == 'failure' && success() }} + if: ${{ needs.prepare.outputs.previous_conclusion == 'failure' && ( github.event_name == 'push' && success() || github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' ) }} steps: - name: Post failure notifications to Slack @@ -131,7 +155,7 @@ jobs: name: Success notifications runs-on: ubuntu-latest needs: [ prepare ] - if: ${{ success() }} + if: ${{ github.event_name == 'push' && success() || github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' }} steps: - name: Post success notifications to Slack @@ -146,7 +170,7 @@ jobs: name: Cancelled notifications runs-on: ubuntu-latest needs: [ prepare ] - if: ${{ cancelled() }} + if: ${{ github.event_name == 'push' && cancelled() || github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'cancelled' }} steps: - name: Post cancelled notifications to Slack From e1eb18d5dfcfb7d815b0c783a5bc636939a90889 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Mon, 25 Oct 2021 20:26:45 +0000 Subject: [PATCH 230/257] Build/Test Tools: Use the correct workflow name in notifications on `workflow_run`. When a workflow is triggered through a `workflow_run` event, the context is not the original workflow. The details about the original workflow are passed through the `github.event` context. This also moves the conditional check controlling whether the Slack workflow is run into the calling workflows to prevent them from running for pull requests. Follow up to [51921-51922,51924-51925,51934]. See #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51937 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/coding-standards.yml | 2 +- .github/workflows/end-to-end-tests.yml | 2 +- .github/workflows/javascript-tests.yml | 2 +- .github/workflows/php-compatibility.yml | 2 +- .github/workflows/phpunit-tests.yml | 2 +- .github/workflows/slack-notifications.yml | 2 +- .github/workflows/test-coverage.yml | 2 +- .github/workflows/test-npm.yml | 2 +- .github/workflows/test-old-branches.yml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 03e6277cd6417..164642e1ad785 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -151,7 +151,7 @@ jobs: name: Slack Notifications uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master needs: [ phpcs, jshint ] - if: ${{ always() }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index fc41233eda5ec..16ad0c0f99fbb 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -121,7 +121,7 @@ jobs: name: Slack Notifications uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master needs: [ e2e-tests ] - if: ${{ always() }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/javascript-tests.yml b/.github/workflows/javascript-tests.yml index 073f5bbd498ef..a13a3b095e0c7 100644 --- a/.github/workflows/javascript-tests.yml +++ b/.github/workflows/javascript-tests.yml @@ -90,7 +90,7 @@ jobs: name: Slack Notifications uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master needs: [ test-js ] - if: ${{ always() }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/php-compatibility.yml b/.github/workflows/php-compatibility.yml index 30b2f5f00119b..2121ddeb143d9 100644 --- a/.github/workflows/php-compatibility.yml +++ b/.github/workflows/php-compatibility.yml @@ -92,7 +92,7 @@ jobs: name: Slack Notifications uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master needs: [ php-compatibility ] - if: ${{ always() }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 22d98f3f4ac5b..ef328a5761f2a 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -247,7 +247,7 @@ jobs: name: Slack Notifications uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master needs: [ test-php ] - if: ${{ always() }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/slack-notifications.yml b/.github/workflows/slack-notifications.yml index aa944450851f0..b75bc5b01d28f 100644 --- a/.github/workflows/slack-notifications.yml +++ b/.github/workflows/slack-notifications.yml @@ -118,7 +118,7 @@ jobs: - name: Construct payload and store as an output id: create-payload - run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.workflow }}\",\"ref_name\":\"${{ env.CURRENT_BRANCH }}\",\"run_url\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.html_url || steps.current-workflow-url.outputs.result }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" + run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.name || github.workflow }}\",\"ref_name\":\"${{ env.CURRENT_BRANCH }}\",\"run_url\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.html_url || steps.current-workflow-url.outputs.result }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" # Posts notifications when a workflow fails. failure: diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 7d3cdbcd3da67..cbf18067e9c00 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -173,7 +173,7 @@ jobs: name: Slack Notifications uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master needs: [ test-coverage-report ] - if: ${{ always() }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/test-npm.yml b/.github/workflows/test-npm.yml index d2af984d25e47..207882513aec9 100644 --- a/.github/workflows/test-npm.yml +++ b/.github/workflows/test-npm.yml @@ -159,7 +159,7 @@ jobs: name: Slack Notifications uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master needs: [ test-npm, test-npm-macos ] - if: ${{ always() }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/test-old-branches.yml b/.github/workflows/test-old-branches.yml index 5d19130b26eab..7051b727d964b 100644 --- a/.github/workflows/test-old-branches.yml +++ b/.github/workflows/test-old-branches.yml @@ -74,7 +74,7 @@ jobs: name: Slack Notifications uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master needs: [ dispatch-workflows-for-old-branches ] - if: ${{ always() }} + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} From 98bf67e02b0a4b4aa3848f4e8266c4fd66cdaad0 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 26 Oct 2021 02:02:50 +0000 Subject: [PATCH 231/257] Tests: Some test improvements for `clean_dirsize_cache()` tests: * Move the directory being tested to the `data` directory, for consistency with other test data. * Set the `svn:eol-style` property to `native`, for consistency with other files. * Correct the test class name in `dummy.txt`. Follow-up to [51246], [51910], [51911]. See #52241, #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51938 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/data/functions/dummy.txt | 1 + .../tests/functions/cleanDirsizeCache.php | 141 +++++++++++++++++- .../tests/functions/fixtures/dummy.txt | 1 - 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 tests/phpunit/data/functions/dummy.txt delete mode 100644 tests/phpunit/tests/functions/fixtures/dummy.txt diff --git a/tests/phpunit/data/functions/dummy.txt b/tests/phpunit/data/functions/dummy.txt new file mode 100644 index 0000000000000..7614ea709c87a --- /dev/null +++ b/tests/phpunit/data/functions/dummy.txt @@ -0,0 +1 @@ +This is a dummy text file which is only used by the `Tests_Functions_CleanDirsizeCache::test_recurse_dirsize_without_transient()` test. diff --git a/tests/phpunit/tests/functions/cleanDirsizeCache.php b/tests/phpunit/tests/functions/cleanDirsizeCache.php index d600bdca542eb..27aacda62b9b9 100644 --- a/tests/phpunit/tests/functions/cleanDirsizeCache.php +++ b/tests/phpunit/tests/functions/cleanDirsizeCache.php @@ -1 +1,140 @@ -expectNotice(); $this->expectNoticeMessage( $expected_message ); clean_dirsize_cache( $path ); } /** * Data provider. * * @return array */ public function data_clean_dirsize_cache_with_invalid_inputs() { return array( 'null' => array( 'path' => null, 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received NULL.', ), 'bool false' => array( 'path' => false, 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received boolean.', ), 'empty string' => array( 'path' => '', 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received string.', ), 'array' => array( 'path' => array( '.', './second/path/' ), 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received array.', ), ); } /** * Test the handling of a non-path text string passed as the $path parameter. * * @ticket 52241 * * @covers ::clean_dirsize_cache * * @dataProvider data_clean_dirsize_cache_with_non_path_string * * @param string $path Path input to use in the test. * @param int $expected_count Expected number of paths in the cache after cleaning. */ public function test_clean_dirsize_cache_with_non_path_string( $path, $expected_count ) { // Set the dirsize cache to our mock. set_transient( 'dirsize_cache', $this->mock_dirsize_cache_with_non_path_string() ); clean_dirsize_cache( $path ); $cache = get_transient( 'dirsize_cache' ); $this->assertIsArray( $cache ); $this->assertCount( $expected_count, $cache ); } /** * Data provider. * * @return array */ public function data_clean_dirsize_cache_with_non_path_string() { return array( 'single dot' => array( 'path' => '.', 'expected_count' => 1, ), 'non-path' => array( 'path' => 'string', 'expected_count' => 1, ), 'non-existant string, but non-path' => array( 'path' => 'doesnotexist', 'expected_count' => 2, ), ); } private function mock_dirsize_cache_with_non_path_string() { return array( '.' => array( 'size' => 50 ), 'string' => array( 'size' => 42 ), ); } /** * Test the behaviour of the function when the transient doesn't exist. * * @ticket 52241 * @ticket 53635 * * @covers ::recurse_dirsize */ public function test_recurse_dirsize_without_transient() { delete_transient( 'dirsize_cache' ); $size = recurse_dirsize( __DIR__ . '/fixtures' ); $this->assertGreaterThan( 10, $size ); } /** * Test the behaviour of the function when the transient does exist, but is not an array. * * In particular, this tests that no PHP TypeErrors are being thrown. * * @ticket 52241 * @ticket 53635 * * @covers ::recurse_dirsize */ public function test_recurse_dirsize_with_invalid_transient() { set_transient( 'dirsize_cache', 'this is not a valid transient for dirsize cache' ); $size = recurse_dirsize( __DIR__ . '/fixtures' ); $this->assertGreaterThan( 10, $size ); } } \ No newline at end of file +expectNotice(); + $this->expectNoticeMessage( $expected_message ); + + clean_dirsize_cache( $path ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_clean_dirsize_cache_with_invalid_inputs() { + return array( + 'null' => array( + 'path' => null, + 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received NULL.', + ), + 'bool false' => array( + 'path' => false, + 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received boolean.', + ), + 'empty string' => array( + 'path' => '', + 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received string.', + ), + 'array' => array( + 'path' => array( '.', './second/path/' ), + 'expected_message' => 'clean_dirsize_cache() only accepts a non-empty path string, received array.', + ), + ); + } + + /** + * Test the handling of a non-path text string passed as the $path parameter. + * + * @ticket 52241 + * + * @covers ::clean_dirsize_cache + * + * @dataProvider data_clean_dirsize_cache_with_non_path_string + * + * @param string $path Path input to use in the test. + * @param int $expected_count Expected number of paths in the cache after cleaning. + */ + public function test_clean_dirsize_cache_with_non_path_string( $path, $expected_count ) { + // Set the dirsize cache to our mock. + set_transient( 'dirsize_cache', $this->mock_dirsize_cache_with_non_path_string() ); + + clean_dirsize_cache( $path ); + + $cache = get_transient( 'dirsize_cache' ); + $this->assertIsArray( $cache ); + $this->assertCount( $expected_count, $cache ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_clean_dirsize_cache_with_non_path_string() { + return array( + 'single dot' => array( + 'path' => '.', + 'expected_count' => 1, + ), + 'non-path' => array( + 'path' => 'string', + 'expected_count' => 1, + ), + 'non-existant string, but non-path' => array( + 'path' => 'doesnotexist', + 'expected_count' => 2, + ), + ); + } + + private function mock_dirsize_cache_with_non_path_string() { + return array( + '.' => array( 'size' => 50 ), + 'string' => array( 'size' => 42 ), + ); + } + + /** + * Test the behaviour of the function when the transient doesn't exist. + * + * @ticket 52241 + * @ticket 53635 + * + * @covers ::recurse_dirsize + */ + public function test_recurse_dirsize_without_transient() { + delete_transient( 'dirsize_cache' ); + + $size = recurse_dirsize( DIR_TESTDATA . '/functions' ); + + $this->assertGreaterThan( 10, $size ); + } + + /** + * Test the behaviour of the function when the transient does exist, but is not an array. + * + * In particular, this tests that no PHP TypeErrors are being thrown. + * + * @ticket 52241 + * @ticket 53635 + * + * @covers ::recurse_dirsize + */ + public function test_recurse_dirsize_with_invalid_transient() { + set_transient( 'dirsize_cache', 'this is not a valid transient for dirsize cache' ); + + $size = recurse_dirsize( DIR_TESTDATA . '/functions' ); + + $this->assertGreaterThan( 10, $size ); + } +} diff --git a/tests/phpunit/tests/functions/fixtures/dummy.txt b/tests/phpunit/tests/functions/fixtures/dummy.txt deleted file mode 100644 index 7cc0f577eecaa..0000000000000 --- a/tests/phpunit/tests/functions/fixtures/dummy.txt +++ /dev/null @@ -1 +0,0 @@ -This is a dummy text file which is only used by the `Tests_Multisite_CleanDirsizeCache::test_recurse_dirsize_without_transient()` test. \ No newline at end of file From 9b6c18b75658d27fb9d8098f2291b7e1b4eaef34 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Wed, 27 Oct 2021 14:58:24 +0000 Subject: [PATCH 232/257] Admin/HTTP API: add suggested filename support to `download_url()`. This change allows for external clients to supply a suggested filename via a `Content-Disposition` response header. This filename is processed through `sanitize_file_name()` to ensure it is allowable (on the server, MIME's, etc...) and `validate_file()` to prevent directory traversal. If the suggested filename fails the above processing/checks, that suggestion is discarded and the standard temporary filename (generated by WordPress) is used. If no `Content-Disposition` header is found in the response headers, the standard temporary filename continues to be used as per normal. Included in this change are 6 additional PHPUnit tests with 9 assertions. These tests confirm that valid filename values are correctly saved, and invalid filename values are correctly rejected. Props cklosows, costdev, dd32, johnjamesjacoby, ocean90, psrpinto. Fixes #38231. git-svn-id: https://develop.svn.wordpress.org/trunk@51939 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/file.php | 24 +++ tests/phpunit/tests/admin/includesFile.php | 171 +++++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 50345c63315a0..8a28c63adca5f 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -1112,6 +1112,7 @@ function wp_handle_sideload( &$file, $overrides = false, $time = null ) { * * @since 2.5.0 * @since 5.2.0 Signature Verification with SoftFail was added. + * @since 5.9.0 Support for Content-Disposition filename was added. * * @param string $url The URL of the file to download. * @param int $timeout The timeout for the request to download the file. @@ -1182,6 +1183,29 @@ function download_url( $url, $timeout = 300, $signature_verification = false ) { return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ), $data ); } + $content_disposition = wp_remote_retrieve_header( $response, 'content-disposition' ); + + if ( $content_disposition ) { + $content_disposition = strtolower( $content_disposition ); + + if ( 0 === strpos( $content_disposition, 'attachment; filename=' ) ) { + $tmpfname_disposition = sanitize_file_name( substr( $content_disposition, 21 ) ); + } else { + $tmpfname_disposition = ''; + } + + // Potential file name must be valid string + if ( $tmpfname_disposition && is_string( $tmpfname_disposition ) && ( 0 === validate_file( $tmpfname_disposition ) ) ) { + if ( rename( $tmpfname, $tmpfname_disposition ) ) { + $tmpfname = $tmpfname_disposition; + } + + if ( ( $tmpfname !== $tmpfname_disposition ) && file_exists( $tmpfname_disposition ) ) { + unlink( $tmpfname_disposition ); + } + } + } + $content_md5 = wp_remote_retrieve_header( $response, 'content-md5' ); if ( $content_md5 ) { diff --git a/tests/phpunit/tests/admin/includesFile.php b/tests/phpunit/tests/admin/includesFile.php index 59299409550fd..db17f834b55ea 100644 --- a/tests/phpunit/tests/admin/includesFile.php +++ b/tests/phpunit/tests/admin/includesFile.php @@ -78,6 +78,177 @@ public function __return_5() { return 5; } + /** + * @ticket 38231 + * @dataProvider data_download_url_should_respect_filename_from_content_disposition_header + * + * @covers ::download_url + * + * @param $filter A callback containing a fake Content-Disposition header. + */ + public function test_download_url_should_respect_filename_from_content_disposition_header( $filter ) { + add_filter( 'pre_http_request', array( $this, $filter ), 10, 3 ); + + $filename = download_url( 'url_with_content_disposition_header' ); + $this->assertStringContainsString( 'filename-from-content-disposition-header', $filename ); + $this->assertFileExists( $filename ); + $this->unlink( $filename ); + + remove_filter( 'pre_http_request', array( $this, $filter ) ); + } + + /** + * Data provider for test_download_url_should_respect_filename_from_content_disposition_header. + * + * @return array + */ + public function data_download_url_should_respect_filename_from_content_disposition_header() { + return array( + 'valid parameters' => array( 'filter_content_disposition_header_with_filename' ), + 'path traversal' => array( 'filter_content_disposition_header_with_filename_with_path_traversal' ), + 'no quotes' => array( 'filter_content_disposition_header_with_filename_without_quotes' ), + ); + } + + /** + * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header. + * + * @since 5.9.0 + * + * @return array + */ + public function filter_content_disposition_header_with_filename( $response, $args, $url ) { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-disposition' => 'attachment; filename="filename-from-content-disposition-header.txt"', + ), + ); + } + + /** + * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header. + * + * @since 5.9.0 + * + * @return array + */ + public function filter_content_disposition_header_with_filename_with_path_traversal( $response, $args, $url ) { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-disposition' => 'attachment; filename="../../filename-from-content-disposition-header.txt"', + ), + ); + } + + /** + * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header. + * + * @since 5.9.0 + * + * @return array + */ + public function filter_content_disposition_header_with_filename_without_quotes( $response, $args, $url ) { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-disposition' => 'attachment; filename=filename-from-content-disposition-header.txt', + ), + ); + } + + /** + * @ticket 38231 + * @dataProvider data_download_url_should_reject_filename_from_invalid_content_disposition_header + * + * @covers ::download_url + * + * @param $filter A callback containing a fake Content-Disposition header. + */ + public function test_download_url_should_reject_filename_from_invalid_content_disposition_header( $filter ) { + add_filter( 'pre_http_request', array( $this, $filter ), 10, 3 ); + + $filename = download_url( 'url_with_content_disposition_header' ); + $this->assertStringContainsString( 'url_with_content_disposition_header', $filename ); + $this->unlink( $filename ); + + remove_filter( 'pre_http_request', array( $this, $filter ) ); + } + + /** + * Data provider for test_download_url_should_reject_filename_from_invalid_content_disposition_header. + * + * @return array + */ + public function data_download_url_should_reject_filename_from_invalid_content_disposition_header() { + return array( + 'no context' => array( 'filter_content_disposition_header_with_filename_without_context' ), + 'inline context' => array( 'filter_content_disposition_header_with_filename_with_inline_context' ), + 'form-data context' => array( 'filter_content_disposition_header_with_filename_with_form_data_context' ), + ); + } + + /** + * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header. + * + * @since 5.9.0 + * + * @return array + */ + public function filter_content_disposition_header_with_filename_without_context( $response, $args, $url ) { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-disposition' => 'filename="filename-from-content-disposition-header.txt"', + ), + ); + } + + /** + * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header. + * + * @since 5.9.0 + * + * @return array + */ + public function filter_content_disposition_header_with_filename_with_inline_context( $response, $args, $url ) { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-disposition' => 'inline; filename="filename-from-content-disposition-header.txt"', + ), + ); + } + + /** + * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header. + * + * @since 5.9.0 + * + * @return array + */ + public function filter_content_disposition_header_with_filename_with_form_data_context( $response, $args, $url ) { + return array( + 'response' => array( + 'code' => 200, + ), + 'headers' => array( + 'content-disposition' => 'form-data; name="file"; filename="filename-from-content-disposition-header.txt"', + ), + ); + } + /** * Verify that a WP_Error object is returned when invalid input is passed as the `$url` parameter. * From 28a1ec5f5920dd54481d31f1d3fb2f21901084e4 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 27 Oct 2021 15:02:04 +0000 Subject: [PATCH 233/257] Coding Standards: Rename the `$process` variable to `$processed_response` for clarity in `WP_Http_Streams::request()`. Includes minor code layout fixes for better readability. Follow-up to [8516], [51826], [51929], [51931]. See #53359. git-svn-id: https://develop.svn.wordpress.org/trunk@51940 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-http-streams.php | 95 +++++++++++++++++------ 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/src/wp-includes/class-wp-http-streams.php b/src/wp-includes/class-wp-http-streams.php index abf16dd44d2e8..1014460376bda 100644 --- a/src/wp-includes/class-wp-http-streams.php +++ b/src/wp-includes/class-wp-http-streams.php @@ -92,6 +92,7 @@ public function request( $url, $args = array() ) { $is_local = isset( $parsed_args['local'] ) && $parsed_args['local']; $ssl_verify = isset( $parsed_args['sslverify'] ) && $parsed_args['sslverify']; + if ( $is_local ) { /** * Filters whether SSL should be verified for local HTTP API requests. @@ -141,10 +142,24 @@ public function request( $url, $args = array() ) { if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - $handle = @stream_socket_client( 'tcp://' . $proxy->host() . ':' . $proxy->port(), $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context ); + $handle = @stream_socket_client( + 'tcp://' . $proxy->host() . ':' . $proxy->port(), + $connection_error, + $connection_error_str, + $connect_timeout, + STREAM_CLIENT_CONNECT, + $context + ); } else { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - $handle = @stream_socket_client( $connect_host . ':' . $parsed_url['port'], $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context ); + $handle = @stream_socket_client( + $connect_host . ':' . $parsed_url['port'], + $connection_error, + $connection_error_str, + $connect_timeout, + STREAM_CLIENT_CONNECT, + $context + ); } if ( $secure_transport ) { @@ -152,9 +167,23 @@ public function request( $url, $args = array() ) { } } else { if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { - $handle = stream_socket_client( 'tcp://' . $proxy->host() . ':' . $proxy->port(), $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context ); + $handle = stream_socket_client( + 'tcp://' . $proxy->host() . ':' . $proxy->port(), + $connection_error, + $connection_error_str, + $connect_timeout, + STREAM_CLIENT_CONNECT, + $context + ); } else { - $handle = stream_socket_client( $connect_host . ':' . $parsed_url['port'], $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context ); + $handle = stream_socket_client( + $connect_host . ':' . $parsed_url['port'], + $connection_error, + $connection_error_str, + $connect_timeout, + STREAM_CLIENT_CONNECT, + $context + ); } } @@ -185,9 +214,9 @@ public function request( $url, $args = array() ) { $strHeaders = strtoupper( $parsed_args['method'] ) . ' ' . $requestPath . ' HTTP/' . $parsed_args['httpversion'] . "\r\n"; $include_port_in_host_header = ( - ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) || - ( 'http' === $parsed_url['scheme'] && 80 != $parsed_url['port'] ) || - ( 'https' === $parsed_url['scheme'] && 443 != $parsed_url['port'] ) + ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) + || ( 'http' === $parsed_url['scheme'] && 80 != $parsed_url['port'] ) + || ( 'https' === $parsed_url['scheme'] && 443 != $parsed_url['port'] ) ); if ( $include_port_in_host_header ) { @@ -238,6 +267,7 @@ public function request( $url, $args = array() ) { $bodyStarted = false; $keep_reading = true; $block_size = 4096; + if ( isset( $parsed_args['limit_response_size'] ) ) { $block_size = min( $block_size, $parsed_args['limit_response_size'] ); } @@ -249,6 +279,7 @@ public function request( $url, $args = array() ) { } else { $stream_handle = fopen( $parsed_args['filename'], 'w+' ); } + if ( ! $stream_handle ) { return new WP_Error( 'http_request_failed', @@ -262,22 +293,25 @@ public function request( $url, $args = array() ) { } $bytes_written = 0; + while ( ! feof( $handle ) && $keep_reading ) { $block = fread( $handle, $block_size ); if ( ! $bodyStarted ) { $strResponse .= $block; if ( strpos( $strResponse, "\r\n\r\n" ) ) { - $process = WP_Http::processResponse( $strResponse ); - $bodyStarted = true; - $block = $process['body']; + $processed_response = WP_Http::processResponse( $strResponse ); + $bodyStarted = true; + $block = $processed_response['body']; unset( $strResponse ); - $process['body'] = ''; + $processed_response['body'] = ''; } } $this_block_size = strlen( $block ); - if ( isset( $parsed_args['limit_response_size'] ) && ( $bytes_written + $this_block_size ) > $parsed_args['limit_response_size'] ) { + if ( isset( $parsed_args['limit_response_size'] ) + && ( $bytes_written + $this_block_size ) > $parsed_args['limit_response_size'] + ) { $this_block_size = ( $parsed_args['limit_response_size'] - $bytes_written ); $block = substr( $block, 0, $this_block_size ); } @@ -292,31 +326,41 @@ public function request( $url, $args = array() ) { $bytes_written += $bytes_written_to_file; - $keep_reading = ! isset( $parsed_args['limit_response_size'] ) || $bytes_written < $parsed_args['limit_response_size']; + $keep_reading = ( + ! isset( $parsed_args['limit_response_size'] ) + || $bytes_written < $parsed_args['limit_response_size'] + ); } fclose( $stream_handle ); } else { $header_length = 0; + while ( ! feof( $handle ) && $keep_reading ) { $block = fread( $handle, $block_size ); $strResponse .= $block; + if ( ! $bodyStarted && strpos( $strResponse, "\r\n\r\n" ) ) { $header_length = strpos( $strResponse, "\r\n\r\n" ) + 4; $bodyStarted = true; } - $keep_reading = ( ! $bodyStarted || ! isset( $parsed_args['limit_response_size'] ) || strlen( $strResponse ) < ( $header_length + $parsed_args['limit_response_size'] ) ); + + $keep_reading = ( + ! $bodyStarted + || ! isset( $parsed_args['limit_response_size'] ) + || strlen( $strResponse ) < ( $header_length + $parsed_args['limit_response_size'] ) + ); } - $process = WP_Http::processResponse( $strResponse ); + $processed_response = WP_Http::processResponse( $strResponse ); unset( $strResponse ); } fclose( $handle ); - $processed_headers = WP_Http::processHeaders( $process['headers'], $url ); + $processed_headers = WP_Http::processHeaders( $processed_response['headers'], $url ); $response = array( 'headers' => $processed_headers['headers'], @@ -334,21 +378,26 @@ public function request( $url, $args = array() ) { } // If the body was chunk encoded, then decode it. - if ( ! empty( $process['body'] ) && isset( $processed_headers['headers']['transfer-encoding'] ) + if ( ! empty( $processed_response['body'] ) + && isset( $processed_headers['headers']['transfer-encoding'] ) && 'chunked' === $processed_headers['headers']['transfer-encoding'] ) { - $process['body'] = WP_Http::chunkTransferDecode( $process['body'] ); + $processed_response['body'] = WP_Http::chunkTransferDecode( $processed_response['body'] ); } - if ( true === $parsed_args['decompress'] && true === WP_Http_Encoding::should_decode( $processed_headers['headers'] ) ) { - $process['body'] = WP_Http_Encoding::decompress( $process['body'] ); + if ( true === $parsed_args['decompress'] + && true === WP_Http_Encoding::should_decode( $processed_headers['headers'] ) + ) { + $processed_response['body'] = WP_Http_Encoding::decompress( $processed_response['body'] ); } - if ( isset( $parsed_args['limit_response_size'] ) && strlen( $process['body'] ) > $parsed_args['limit_response_size'] ) { - $process['body'] = substr( $process['body'], 0, $parsed_args['limit_response_size'] ); + if ( isset( $parsed_args['limit_response_size'] ) + && strlen( $processed_response['body'] ) > $parsed_args['limit_response_size'] + ) { + $processed_response['body'] = substr( $processed_response['body'], 0, $parsed_args['limit_response_size'] ); } - $response['body'] = $process['body']; + $response['body'] = $processed_response['body']; return $response; } From 489260a45ae44b7a7ad6282040542079cb756782 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 27 Oct 2021 17:08:15 +0000 Subject: [PATCH 234/257] Script Loader: Correct the number of arguments passed to the closure in `enqueue_block_styles_assets()`. This avoids an `Uncaught ArgumentCountError: Too few arguments to function {closure}(), 1 passed` PHP fatal error when registering a block style with the `should_load_separate_core_block_assets` filter enabled. Follow-up to [51471]. Props aristath, shimon246, jrf, gziolo. Fixes #54323. git-svn-id: https://develop.svn.wordpress.org/trunk@51941 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/script-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 71c56192ee6c1..f90390e5c37d8 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2448,7 +2448,7 @@ function enqueue_block_styles_assets() { if ( wp_should_load_separate_core_block_assets() ) { add_filter( 'render_block', - function( $html, $block ) use ( $style_properties ) { + function( $html ) use ( $style_properties ) { wp_enqueue_style( $style_properties['style_handle'] ); return $html; } From c043d409006957852791facfe5a16c7a6ea1017d Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 27 Oct 2021 18:20:58 +0000 Subject: [PATCH 235/257] Docs: Document the usage of some globals in `wp-includes/script-loader.php`. Follow-up to [44114], [44262], [49080], [50761], [51471]. See #53399. git-svn-id: https://develop.svn.wordpress.org/trunk@51942 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/script-loader.php | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index f90390e5c37d8..fa757b4fed1ed 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -39,11 +39,16 @@ * * @since 5.0.0 * + * @global string $tinymce_version + * @global bool $concatenate_scripts + * @global bool $compress_scripts + * * @param WP_Scripts $scripts WP_Scripts object. * @param bool $force_uncompressed Whether to forcibly prevent gzip compression. Default false. */ function wp_register_tinymce_scripts( $scripts, $force_uncompressed = false ) { global $tinymce_version, $concatenate_scripts, $compress_scripts; + $suffix = wp_scripts_get_suffix(); $dev_suffix = wp_scripts_get_suffix( 'dev' ); @@ -72,6 +77,8 @@ function wp_register_tinymce_scripts( $scripts, $force_uncompressed = false ) { * * @since 5.0.0 * + * @global WP_Locale $wp_locale WordPress date and time locale object. + * * @param WP_Scripts $scripts WP_Scripts object. */ function wp_default_packages_vendor( $scripts ) { @@ -277,6 +284,8 @@ function wp_default_packages_scripts( $scripts ) { * * @since 5.0.0 * + * @global WP_Locale $wp_locale WordPress date and time locale object. + * * @param WP_Scripts $scripts WP_Scripts object. */ function wp_default_packages_inline_scripts( $scripts ) { @@ -1379,9 +1388,13 @@ function wp_default_scripts( $scripts ) { * * @since 2.6.0 * + * @global array $editor_styles + * * @param WP_Styles $styles */ function wp_default_styles( $styles ) { + global $editor_styles; + // Include an unmodified $wp_version. require ABSPATH . WPINC . '/version.php'; @@ -1534,7 +1547,6 @@ function wp_default_styles( $styles ) { $wp_edit_blocks_dependencies[] = 'wp-editor-classic-layout-styles'; } - global $editor_styles; if ( ! is_array( $editor_styles ) || count( $editor_styles ) === 0 ) { // Include opinionated block styles if no $editor_styles are declared, so the editor never appears broken. $wp_edit_blocks_dependencies[] = 'wp-block-library-theme'; @@ -2032,16 +2044,17 @@ function _print_scripts() { * @return array */ function wp_print_head_scripts() { + global $wp_scripts; + if ( ! did_action( 'wp_print_scripts' ) ) { /** This action is documented in wp-includes/functions.wp-scripts.php */ do_action( 'wp_print_scripts' ); } - global $wp_scripts; - if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { return array(); // No need to run if nothing is queued. } + return print_head_scripts(); } @@ -2243,8 +2256,6 @@ function script_concat_settings() { * the editor and the front-end. * * @since 5.0.0 - * - * @global WP_Screen $current_screen WordPress current screen object. */ function wp_common_block_scripts_and_styles() { if ( is_admin() && ! wp_should_load_block_editor_scripts_and_styles() ) { @@ -2333,6 +2344,8 @@ function wp_enqueue_global_styles() { * * @since 5.6.0 * + * @global WP_Screen $current_screen WordPress current screen object. + * * @return bool Whether scripts and styles should be enqueued. */ function wp_should_load_block_editor_scripts_and_styles() { @@ -2436,8 +2449,12 @@ function wp_enqueue_registered_block_scripts_and_styles() { * Function responsible for enqueuing the styles required for block styles functionality on the editor and on the frontend. * * @since 5.3.0 + * + * @global WP_Styles $wp_styles */ function enqueue_block_styles_assets() { + global $wp_styles; + $block_styles = WP_Block_Styles_Registry::get_instance()->get_all_registered(); foreach ( $block_styles as $block_name => $styles ) { @@ -2465,7 +2482,7 @@ function( $html ) use ( $style_properties ) { // If the site loads separate styles per-block, check if the block has a stylesheet registered. if ( wp_should_load_separate_core_block_assets() ) { $block_stylesheet_handle = generate_block_asset_handle( $block_name, 'style' ); - global $wp_styles; + if ( isset( $wp_styles->registered[ $block_stylesheet_handle ] ) ) { $handle = $block_stylesheet_handle; } From 0cf6d3e48d18b4e8cb932fa02bb82bf5de14a48c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 27 Oct 2021 18:42:13 +0000 Subject: [PATCH 236/257] Role/Capability: Add support for capability queries in `WP_User_Query`. Similar to the existing `role`/`role__in`/`role__not_in` query arguments, this adds support for three new query arguments in `WP_User_Query`: * `capability` * `capability__in` * `capability__not_in` These can be used to fetch users with (or without) a specific set of capabilities, for example to get all users with the capability to edit a certain post type. Under the hood, this will check all existing roles on the site and perform a `LIKE` query against the `capabilities` user meta field to find: * all users with a role that has this capability * all users with the capability being assigned directly Note: In WordPress, not all capabilities are stored in the database. Capabilities can also be modified using filters like `map_meta_cap`. These new query arguments do NOT work for such capabilities. The prime use case for capability queries is to get all "authors", i.e. users with the capability to edit a certain post type. Until now, `'who' => 'authors'` was used for this, which relies on user levels. However, user levels were deprecated a long time ago and thus never added to custom roles. This led to constant frustration due to users with custom roles missing from places like author dropdowns. This updates any usage of `'who' => 'authors'` in core to use capability queries instead. Subsequently, `'who' => 'authors'` queries are being **deprecated** in favor of these new query arguments. Also adds a new `capabilities` parameter (mapping to `capability__in` in `WP_User_Query`) to the REST API users controller. Also updates `twentyfourteen_list_authors()` in Twenty Fourteen to make use of this new functionality, adding a new `twentyfourteen_list_authors_query_args` filter to make it easier to override this behavior. Props scribu, lgladdly, boonebgorges, spacedmonkey, peterwilsoncc, SergeyBiryukov, swissspidy. Fixes #16841. git-svn-id: https://develop.svn.wordpress.org/trunk@51943 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-wp-posts-list-table.php | 2 +- src/wp-admin/includes/meta-boxes.php | 4 +- .../themes/twentyfourteen/functions.php | 23 +- src/wp-includes/class-wp-user-query.php | 134 ++++++++++ .../class-wp-rest-users-controller.php | 32 ++- src/wp-includes/user.php | 21 +- .../tests/rest-api/rest-users-controller.php | 114 ++++++--- tests/phpunit/tests/user/query.php | 241 ++++++++++++++++++ tests/phpunit/tests/xmlrpc/wp/getUsers.php | 3 + tests/qunit/fixtures/wp-api-generated.js | 8 + 10 files changed, 535 insertions(+), 47 deletions(-) diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index 19686959e20b6..213ca201d95f9 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1660,7 +1660,7 @@ public function inline_edit() { if ( current_user_can( $post_type_object->cap->edit_others_posts ) ) { $users_opt = array( 'hide_if_only_one_author' => false, - 'who' => 'authors', + 'capability' => array( $post_type_object->cap->edit_posts ), 'name' => 'post_author', 'class' => 'authors', 'multi' => 1, diff --git a/src/wp-admin/includes/meta-boxes.php b/src/wp-admin/includes/meta-boxes.php index 193877ce96de6..c14ae52ddc02b 100644 --- a/src/wp-admin/includes/meta-boxes.php +++ b/src/wp-admin/includes/meta-boxes.php @@ -903,12 +903,14 @@ function post_slug_meta_box( $post ) { */ function post_author_meta_box( $post ) { global $user_ID; + + $post_type_object = get_post_type_object( $post->post_type ); ?> 'authors', + 'capability' => array( $post_type_object->cap->edit_posts ), 'name' => 'post_author_override', 'selected' => empty( $post->ID ) ? $user_ID : $post->post_author, 'include_selected' => true, diff --git a/src/wp-content/themes/twentyfourteen/functions.php b/src/wp-content/themes/twentyfourteen/functions.php index f39296b1ba33f..892d6ac939aef 100644 --- a/src/wp-content/themes/twentyfourteen/functions.php +++ b/src/wp-content/themes/twentyfourteen/functions.php @@ -491,15 +491,24 @@ function twentyfourteen_the_attached_image() { * @since Twenty Fourteen 1.0 */ function twentyfourteen_list_authors() { - $contributor_ids = get_users( - array( - 'fields' => 'ID', - 'orderby' => 'post_count', - 'order' => 'DESC', - 'who' => 'authors', - ) + $args = array( + 'fields' => 'ID', + 'orderby' => 'post_count', + 'order' => 'DESC', + 'capability' => array( 'edit_posts' ), ); + /** + * Filters query arguments for listing authors. + * + * @since 3.3 + * + * @param array $args Query arguments. + */ + $args = apply_filters( 'twentyfourteen_list_authors_query_args', $args ); + + $contributor_ids = get_users( $args ); + foreach ( $contributor_ids as $contributor_id ) : $post_count = count_user_posts( $contributor_id ); diff --git a/src/wp-includes/class-wp-user-query.php b/src/wp-includes/class-wp-user-query.php index f760c252886f0..936ffe89bf708 100644 --- a/src/wp-includes/class-wp-user-query.php +++ b/src/wp-includes/class-wp-user-query.php @@ -93,6 +93,9 @@ public static function fill_query_vars( $args ) { 'role' => '', 'role__in' => array(), 'role__not_in' => array(), + 'capability' => '', + 'capability__in' => array(), + 'capability__not_in' => array(), 'meta_key' => '', 'meta_value' => '', 'meta_compare' => '', @@ -133,6 +136,7 @@ public static function fill_query_vars( $args ) { * querying for all users with using -1. * @since 4.7.0 Added 'nicename', 'nicename__in', 'nicename__not_in', 'login', 'login__in', * and 'login__not_in' parameters. + * @since 5.9.0 Added 'capability', 'capability__in', and 'capability__not_in' parameters. * * @global wpdb $wpdb WordPress database abstraction object. * @global int $blog_id @@ -148,6 +152,19 @@ public static function fill_query_vars( $args ) { * roles. Default empty array. * @type string[] $role__not_in An array of role names to exclude. Users matching one or more of these * roles will not be included in results. Default empty array. + * @type string $capability An array or a comma-separated list of capability names that users must match + * to be included in results. Note that this is an inclusive list: users + * must match *each* capability. + * Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}. + * Default empty. + * @type string[] $capability__in An array of capability names. Matched users must have at least one of these + * capabilities. + * Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}. + * Default empty array. + * @type string[] $capability__not_in An array of capability names to exclude. Users matching one or more of these + * capabilities will not be included in results. + * Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}. + * Default empty array. * @type string $meta_key User meta key. Default empty. * @type string $meta_value User meta value. Default empty. * @type string $meta_compare Comparison operator to test the `$meta_value`. Accepts '=', '!=', @@ -320,6 +337,17 @@ public function prepare_query( $query = array() ) { $this->meta_query->parse_query_vars( $qv ); if ( isset( $qv['who'] ) && 'authors' === $qv['who'] && $blog_id ) { + _deprecated_argument( + 'WP_User_Query', + '5.9.0', + sprintf( + /* translators: 1: who, 2: capability */ + __( '%1$s is deprecated. Use %2$s instead.' ), + 'who', + 'capability' + ) + ); + $who_query = array( 'key' => $wpdb->get_blog_prefix( $blog_id ) . 'user_level', 'value' => 0, @@ -343,6 +371,7 @@ public function prepare_query( $query = array() ) { $this->meta_query->parse_query_vars( $this->meta_query->queries ); } + // Roles. $roles = array(); if ( isset( $qv['role'] ) ) { if ( is_array( $qv['role'] ) ) { @@ -362,6 +391,111 @@ public function prepare_query( $query = array() ) { $role__not_in = (array) $qv['role__not_in']; } + // Capabilities. + $available_roles = array(); + + if ( ! empty( $qv['capability'] ) || ! empty( $qv['capability__in'] ) || ! empty( $qv['capability__not_in'] ) ) { + global $wp_roles; + + $wp_roles->for_site( $blog_id ); + $available_roles = $wp_roles->roles; + } + + $capabilities = array(); + if ( ! empty( $qv['capability'] ) ) { + if ( is_array( $qv['capability'] ) ) { + $capabilities = $qv['capability']; + } elseif ( is_string( $qv['capability'] ) ) { + $capabilities = array_map( 'trim', explode( ',', $qv['capability'] ) ); + } + } + + $capability__in = array(); + if ( ! empty( $qv['capability__in'] ) ) { + $capability__in = (array) $qv['capability__in']; + } + + $capability__not_in = array(); + if ( ! empty( $qv['capability__not_in'] ) ) { + $capability__not_in = (array) $qv['capability__not_in']; + } + + // Keep track of all capabilities and the roles they're added on. + $caps_with_roles = array(); + + foreach ( $available_roles as $role => $role_data ) { + $role_caps = array_keys( array_filter( $role_data['capabilities'] ) ); + + foreach ( $capabilities as $cap ) { + if ( in_array( $cap, $role_caps, true ) ) { + $caps_with_roles[ $cap ][] = $role; + break; + } + } + + foreach ( $capability__in as $cap ) { + if ( in_array( $cap, $role_caps, true ) ) { + $role__in[] = $role; + break; + } + } + + foreach ( $capability__not_in as $cap ) { + if ( in_array( $cap, $role_caps, true ) ) { + $role__not_in[] = $role; + break; + } + } + } + + $role__in = array_merge( $role__in, $capability__in ); + $role__not_in = array_merge( $role__not_in, $capability__not_in ); + + $roles = array_unique( $roles ); + $role__in = array_unique( $role__in ); + $role__not_in = array_unique( $role__not_in ); + + // Support querying by capabilities added directly to users. + if ( $blog_id && ! empty( $capabilities ) ) { + $capabilities_clauses = array( 'relation' => 'AND' ); + + foreach ( $capabilities as $cap ) { + $clause = array( 'relation' => 'OR' ); + + $clause[] = array( + 'key' => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities', + 'value' => '"' . $cap . '"', + 'compare' => 'LIKE', + ); + + if ( ! empty( $caps_with_roles[ $cap ] ) ) { + foreach ( $caps_with_roles[ $cap ] as $role ) { + $clause[] = array( + 'key' => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities', + 'value' => '"' . $role . '"', + 'compare' => 'LIKE', + ); + } + } + + $capabilities_clauses[] = $clause; + } + + $role_queries[] = $capabilities_clauses; + + if ( empty( $this->meta_query->queries ) ) { + $this->meta_query->queries[] = $capabilities_clauses; + } else { + // Append the cap query to the original queries and reparse the query. + $this->meta_query->queries = array( + 'relation' => 'AND', + array( $this->meta_query->queries, array( $capabilities_clauses ) ), + ); + } + + $this->meta_query->parse_query_vars( $this->meta_query->queries ); + } + if ( $blog_id && ( ! empty( $roles ) || ! empty( $role__in ) || ! empty( $role__not_in ) || is_multisite() ) ) { $role_queries = array(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php index b3cdfc2e31e93..e3e5d935d7a1a 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php @@ -198,6 +198,15 @@ public function get_items_permissions_check( $request ) { ); } + // Check if capabilities is specified in GET request and if user can list users. + if ( ! empty( $request['capabilities'] ) && ! current_user_can( 'list_users' ) ) { + return new WP_Error( + 'rest_user_cannot_view', + __( 'Sorry, you are not allowed to filter users by capability.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_forbidden_context', @@ -254,13 +263,14 @@ public function get_items( $request ) { * present in $registered will be set. */ $parameter_mappings = array( - 'exclude' => 'exclude', - 'include' => 'include', - 'order' => 'order', - 'per_page' => 'number', - 'search' => 'search', - 'roles' => 'role__in', - 'slug' => 'nicename__in', + 'exclude' => 'exclude', + 'include' => 'include', + 'order' => 'order', + 'per_page' => 'number', + 'search' => 'search', + 'roles' => 'role__in', + 'capabilities' => 'capability__in', + 'slug' => 'nicename__in', ); $prepared_args = array(); @@ -1554,6 +1564,14 @@ public function get_collection_params() { ), ); + $query_params['capabilities'] = array( + 'description' => __( 'Limit result set to users matching at least one specific capability provided. Accepts csv list or single capability.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ); + $query_params['who'] = array( 'description' => __( 'Limit result set to users who are considered authors.' ), 'type' => 'string', diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 55bd546568fe1..018c960c031b1 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -1320,13 +1320,32 @@ function wp_dropdown_users( $args = '' ) { 'role' => '', 'role__in' => array(), 'role__not_in' => array(), + 'capability' => '', + 'capability__in' => array(), + 'capability__not_in' => array(), ); $defaults['selected'] = is_author() ? get_query_var( 'author' ) : 0; $parsed_args = wp_parse_args( $args, $defaults ); - $query_args = wp_array_slice_assoc( $parsed_args, array( 'blog_id', 'include', 'exclude', 'orderby', 'order', 'who', 'role', 'role__in', 'role__not_in' ) ); + $query_args = wp_array_slice_assoc( + $parsed_args, + array( + 'blog_id', + 'include', + 'exclude', + 'orderby', + 'order', + 'who', + 'role', + 'role__in', + 'role__not_in', + 'capability', + 'capability__in', + 'capability__not_in', + ) + ); $fields = array( 'ID', 'user_login' ); diff --git a/tests/phpunit/tests/rest-api/rest-users-controller.php b/tests/phpunit/tests/rest-api/rest-users-controller.php index 1705cba2eb699..478148ed56229 100644 --- a/tests/phpunit/tests/rest-api/rest-users-controller.php +++ b/tests/phpunit/tests/rest-api/rest-users-controller.php @@ -15,6 +15,7 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { protected static $editor; protected static $draft_editor; protected static $subscriber; + protected static $author; protected static $authors = array(); protected static $posts = array(); @@ -55,6 +56,13 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'user_email' => 'subscriber@example.com', ) ); + self::$author = $factory->user->create( + array( + 'display_name' => 'author', + 'role' => 'author', + 'user_email' => 'author@example.com', + ) + ); foreach ( array( true, false ) as $show_in_rest ) { foreach ( array( true, false ) as $public ) { @@ -107,7 +115,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { } // Set up users for pagination tests. - for ( $i = 0; $i < self::$total_users - 10; $i++ ) { + for ( $i = 0; $i < self::$total_users - 11; $i++ ) { self::$user_ids[] = $factory->user->create( array( 'role' => 'contributor', @@ -121,6 +129,7 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$user ); self::delete_user( self::$editor ); self::delete_user( self::$draft_editor ); + self::delete_user( self::$author ); foreach ( self::$posts as $post ) { wp_delete_post( $post, true ); @@ -183,8 +192,7 @@ public function test_registered_query_params() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $keys = array_keys( $data['endpoints'][0]['args'] ); - sort( $keys ); - $this->assertSame( + $this->assertEqualSets( array( 'context', 'exclude', @@ -195,6 +203,7 @@ public function test_registered_query_params() { 'page', 'per_page', 'roles', + 'capabilities', 'search', 'slug', 'who', @@ -795,32 +804,19 @@ public function test_get_items_slug_csv_query() { public function test_get_items_roles() { wp_set_current_user( self::$user ); - $tango = $this->factory->user->create( - array( - 'display_name' => 'tango', - 'role' => 'subscriber', - ) - ); - $yolo = $this->factory->user->create( - array( - 'display_name' => 'yolo', - 'role' => 'author', - ) - ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'roles', 'author,subscriber' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 3, $data ); - $this->assertSame( $tango, $data[1]['id'] ); - $this->assertSame( $yolo, $data[2]['id'] ); + $this->assertCount( 2, $data ); + $this->assertSame( self::$author, $data[0]['id'] ); + $this->assertSame( self::$subscriber, $data[1]['id'] ); $request->set_param( 'roles', 'author' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertCount( 1, $data ); - $this->assertSame( $yolo, $data[0]['id'] ); + $this->assertSame( self::$author, $data[0]['id'] ); wp_set_current_user( 0 ); @@ -838,28 +834,86 @@ public function test_get_items_roles() { public function test_get_items_invalid_roles() { wp_set_current_user( self::$user ); - $lolz = $this->factory->user->create( - array( - 'display_name' => 'lolz', - 'role' => 'author', - ) - ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'roles', 'ilovesteak,author' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertCount( 1, $data ); - $this->assertSame( $lolz, $data[0]['id'] ); + $this->assertSame( self::$author, $data[0]['id'] ); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'roles', 'steakisgood' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 0, $data ); - $this->assertSame( array(), $data ); + $this->assertIsArray( $data ); + $this->assertEmpty( $data ); } + /** + * @ticket 16841 + */ + public function test_get_items_capabilities() { + wp_set_current_user( self::$user ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'capabilities', 'edit_posts' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertNotEmpty( $data ); + foreach ( $data as $user ) { + $this->assertTrue( user_can( $user['id'], 'edit_posts' ) ); + } + } + + /** + * @ticket 16841 + */ + public function test_get_items_capabilities_no_permission_no_user() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'capabilities', 'edit_posts' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 ); + } + + /** + * @ticket 16841 + */ + public function test_get_items_capabilities_no_permission_editor() { + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'capabilities', 'edit_posts' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_user_cannot_view', $response, 403 ); + } + + /** + * @ticket 16841 + */ + public function test_get_items_invalid_capabilities() { + wp_set_current_user( self::$user ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'roles', 'ilovesteak,author' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 1, $data ); + $this->assertSame( self::$author, $data[0]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'capabilities', 'steakisgood' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertEmpty( $data ); + } + + /** + * @expectedDeprecated WP_User_Query + */ public function test_get_items_who_author_query() { wp_set_current_user( self::$superadmin ); diff --git a/tests/phpunit/tests/user/query.php b/tests/phpunit/tests/user/query.php index f0a0827038625..170e5b24a2ff0 100644 --- a/tests/phpunit/tests/user/query.php +++ b/tests/phpunit/tests/user/query.php @@ -730,6 +730,7 @@ public function test_roles_and_caps_should_be_populated_for_explicit_value_of_di /** * @ticket 32019 * @group ms-required + * @expectedDeprecated WP_User_Query */ public function test_who_authors() { $b = self::factory()->blog->create(); @@ -755,6 +756,7 @@ public function test_who_authors() { /** * @ticket 32019 * @group ms-required + * @expectedDeprecated WP_User_Query */ public function test_who_authors_should_work_alongside_meta_query() { $b = self::factory()->blog->create(); @@ -789,6 +791,7 @@ public function test_who_authors_should_work_alongside_meta_query() { /** * @ticket 36724 * @group ms-required + * @expectedDeprecated WP_User_Query */ public function test_who_authors_should_work_alongside_meta_params() { $b = self::factory()->blog->create(); @@ -1725,4 +1728,242 @@ public static function filter_users_pre_query( $posts, $query ) { return array( 555 ); } + + /** + * @ticket 16841 + * @group ms-excluded + */ + public function test_get_single_capability_by_string() { + $wp_user_search = new WP_User_Query( array( 'capability' => 'install_plugins' ) ); + $users = $wp_user_search->get_results(); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + // User has the capability, but on Multisite they would also need to be a super admin. + // Hence using get_role_caps() instead of has_cap(). + $role_caps = $user->get_role_caps(); + $this->assertArrayHasKey( 'install_plugins', $role_caps ); + $this->assertTrue( $role_caps['install_plugins'] ); + } + } + + /** + * @ticket 16841 + * @group ms-required + */ + public function test_get_single_capability_by_string_multisite() { + $wp_user_search = new WP_User_Query( array( 'capability' => array( 'install_plugins' ) ) ); + $users = $wp_user_search->get_results(); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + $role_caps = $user->get_role_caps(); + $this->assertArrayHasKey( 'install_plugins', $role_caps ); + $this->assertTrue( $role_caps['install_plugins'] ); + // While the user can have the capability, on Multisite they also need to be a super admin. + if ( is_super_admin( $user->ID ) ) { + $this->assertTrue( $user->has_cap( 'install_plugins' ) ); + } else { + $this->assertFalse( $user->has_cap( 'install_plugins' ) ); + } + } + } + + /** + * @ticket 16841 + */ + public function test_get_single_capability_invalid() { + $wp_user_search = new WP_User_Query( array( 'capability' => 'foo_bar' ) ); + $users = $wp_user_search->get_results(); + + $this->assertEmpty( $users ); + } + + /** + * @ticket 16841 + */ + public function test_get_single_capability_by_array() { + $wp_user_search = new WP_User_Query( array( 'capability' => array( 'install_plugins' ) ) ); + $users = $wp_user_search->get_results(); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + // User has the capability, but on Multisite they would also need to be a super admin. + // Hence using get_role_caps() instead of has_cap(). + $role_caps = $user->get_role_caps(); + $this->assertArrayHasKey( 'install_plugins', $role_caps ); + $this->assertTrue( $role_caps['install_plugins'] ); + } + } + + /** + * @ticket 16841 + */ + public function test_get_single_capability_added_to_user() { + foreach ( self::$sub_ids as $subscriber ) { + $subscriber = get_user_by( 'ID', $subscriber ); + $subscriber->add_cap( 'custom_cap' ); + } + + $wp_user_search = new WP_User_Query( array( 'capability' => 'custom_cap' ) ); + $users = $wp_user_search->get_results(); + + $this->assertCount( 2, $users ); + $this->assertEqualSets( self::$sub_ids, wp_list_pluck( $users, 'ID' ) ); + + foreach ( $users as $user ) { + $this->assertTrue( $user->has_cap( 'custom_cap' ) ); + } + } + + /** + * @ticket 16841 + */ + public function test_get_multiple_capabilities_should_only_match_users_who_have_each_capability_test() { + wp_roles()->add_role( 'role_1', 'Role 1', array( 'role_1_cap' => true ) ); + wp_roles()->add_role( 'role_2', 'Role 2', array( 'role_2_cap' => true ) ); + + $subscriber1 = get_user_by( 'ID', self::$sub_ids[0] ); + $subscriber1->add_role( 'role_1' ); + + $subscriber2 = get_user_by( 'ID', self::$sub_ids[1] ); + $subscriber2->add_role( 'role_1' ); + $subscriber2->add_role( 'role_2' ); + + $wp_user_search = new WP_User_Query( array( 'capability' => array( 'role_1_cap', 'role_2_cap' ) ) ); + $users = $wp_user_search->get_results(); + + $this->assertCount( 1, $users ); + $this->assertSame( $users[0]->ID, $subscriber2->ID ); + foreach ( $users as $user ) { + $this->assertTrue( $user->has_cap( 'role_1_cap' ) ); + $this->assertTrue( $user->has_cap( 'role_2_cap' ) ); + } + } + + /** + * @ticket 16841 + */ + public function test_get_multiple_capabilities_should_only_match_users_who_have_each_capability_added_to_user() { + $admin1 = get_user_by( 'ID', self::$admin_ids[0] ); + $admin1->add_cap( 'custom_cap' ); + + $wp_user_search = new WP_User_Query( array( 'capability' => array( 'manage_options', 'custom_cap' ) ) ); + $users = $wp_user_search->get_results(); + + $this->assertCount( 1, $users ); + $this->assertSame( $users[0]->ID, $admin1->ID ); + $this->assertTrue( $users[0]->has_cap( 'custom_cap' ) ); + $this->assertTrue( $users[0]->has_cap( 'manage_options' ) ); + } + + /** + * @ticket 16841 + */ + public function test_get_multiple_capabilities_or() { + $wp_user_search = new WP_User_Query( array( 'capability__in' => array( 'publish_posts', 'edit_posts' ) ) ); + $users = $wp_user_search->get_results(); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + $this->assertTrue( $user->has_cap( 'publish_posts' ) || $user->has_cap( 'edit_posts' ) ); + } + } + + /** + * @ticket 16841 + */ + public function test_get_multiple_capabilities_or_added_to_user() { + $user = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $user->add_cap( 'custom_cap' ); + + $wp_user_search = new WP_User_Query( array( 'capability__in' => array( 'publish_posts', 'custom_cap' ) ) ); + $users = $wp_user_search->get_results(); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + $this->assertTrue( $user->has_cap( 'publish_posts' ) || $user->has_cap( 'custom_cap' ) ); + } + } + + /** + * @ticket 16841 + */ + public function test_capability_exclusion() { + $wp_user_search = new WP_User_Query( array( 'capability__not_in' => array( 'publish_posts', 'edit_posts' ) ) ); + $users = $wp_user_search->get_results(); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + $this->assertFalse( $user->has_cap( 'publish_posts' ) ); + $this->assertFalse( $user->has_cap( 'edit_posts' ) ); + } + } + + /** + * @ticket 16841 + */ + public function test_capability_exclusion_added_to_user() { + $user = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) ); + $user->add_cap( 'custom_cap' ); + + $wp_user_search = new WP_User_Query( array( 'capability__not_in' => array( 'publish_posts', 'custom_cap' ) ) ); + $users = $wp_user_search->get_results(); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + $this->assertFalse( $user->has_cap( 'publish_posts' ) ); + $this->assertFalse( $user->has_cap( 'custom_cap' ) ); + } + } + + /** + * @ticket 16841 + */ + public function test_capability__in_capability__not_in_combined() { + $wp_user_search = new WP_User_Query( + array( + 'capability__in' => array( 'read' ), + 'capability__not_in' => array( 'manage_options' ), + ) + ); + $users = $wp_user_search->get_results(); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + $this->assertTrue( $user->has_cap( 'read' ) ); + $this->assertFalse( $user->has_cap( 'manage_options' ) ); + } + } + + /** + * @ticket 16841 + * @group ms-required + */ + public function test_get_single_capability_multisite_blog_id() { + $blog_id = self::factory()->blog->create(); + + add_user_to_blog( $blog_id, self::$author_ids[0], 'subscriber' ); + add_user_to_blog( $blog_id, self::$author_ids[1], 'author' ); + add_user_to_blog( $blog_id, self::$author_ids[2], 'editor' ); + + $wp_user_search = new WP_User_Query( + array( + 'capability' => 'publish_posts', + 'blog_id' => $blog_id, + ) + ); + $users = $wp_user_search->get_results(); + + $found = wp_list_pluck( $wp_user_search->get_results(), 'ID' ); + + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { + $this->assertTrue( $user->has_cap( 'publish_posts' ) ); + } + + $this->assertNotContains( self::$author_ids[0], $found ); + $this->assertContains( self::$author_ids[1], $found ); + $this->assertContains( self::$author_ids[2], $found ); + } } diff --git a/tests/phpunit/tests/xmlrpc/wp/getUsers.php b/tests/phpunit/tests/xmlrpc/wp/getUsers.php index 627facea5ce35..4975b68381051 100644 --- a/tests/phpunit/tests/xmlrpc/wp/getUsers.php +++ b/tests/phpunit/tests/xmlrpc/wp/getUsers.php @@ -54,6 +54,9 @@ function test_invalid_role() { $this->assertSame( 403, $results->code ); } + /** + * @expectedDeprecated WP_User_Query + */ function test_role_filter() { $author_id = $this->make_user_by_role( 'author' ); $editor_id = $this->make_user_by_role( 'editor' ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 63dbf37f05c88..0d5b2230e0b47 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -5357,6 +5357,14 @@ mockedApiResponse.Schema = { }, "required": false }, + "capabilities": { + "description": "Limit result set to users matching at least one specific capability provided. Accepts csv list or single capability.", + "type": "array", + "items": { + "type": "string" + }, + "required": false + }, "who": { "description": "Limit result set to users who are considered authors.", "type": "string", From 39479eb86a4cdfac9d508f455d99de95662dd828 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Thu, 28 Oct 2021 14:09:07 +0000 Subject: [PATCH 237/257] Coding Standards: Correct alignment in `wp_enqueue_global_styles()`. This fixes an `Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space` WPCS warning. Follow-up to [50973], [51819]. See #53359. git-svn-id: https://develop.svn.wordpress.org/trunk@51944 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/script-loader.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index fa757b4fed1ed..7b66e1befa031 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2310,8 +2310,9 @@ function wp_enqueue_global_styles() { ! is_admin() ); - $stylesheet = null; + $stylesheet = null; $transient_name = 'global_styles_' . get_stylesheet(); + if ( $can_use_cache ) { $cache = get_transient( $transient_name ); if ( $cache ) { From 548829f2717de01fea971c740380e89a80fe7543 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Thu, 28 Oct 2021 16:06:20 +0000 Subject: [PATCH 238/257] Media: Close attachment details modal with esc key. The event that fired closing the attachment details modal also removed the keydown event listener, so subsequent modals could not be closed with the escape key. Props vondelphia, sourovroy, sabernhardt Fixes #53924. git-svn-id: https://develop.svn.wordpress.org/trunk@51945 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/media/views/modal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/media/views/modal.js b/src/js/media/views/modal.js index 42fc7c9f6056f..f28c92e7e6500 100644 --- a/src/js/media/views/modal.js +++ b/src/js/media/views/modal.js @@ -138,8 +138,8 @@ Modal = wp.media.View.extend(/** @lends wp.media.view.Modal.prototype */{ // Enable page scrolling. $( 'body' ).removeClass( 'modal-open' ); - // Hide modal and remove restricted media modal tab focus once it's closed. - this.$el.hide().off( 'keydown' ); + // Hide the modal element by adding display none. + this.$el.hide(); /* * Make visible again to assistive technologies all body children that From fe81c8fb358baf46d20ddc85bb0a03e6c5a97aef Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Thu, 28 Oct 2021 17:27:04 +0000 Subject: [PATCH 239/257] Administration: Hide mobile menu on focusout. Closes the admin menu on mobile devices when keyboard focus moves outside of the menu or menu toggle elements. Improves the usability of the menu on mobile by allowing closure anywhere outside the menu rather than only on the toggle. Props kaneva, costdev, sabernhardt Fixes #53587. git-svn-id: https://develop.svn.wordpress.org/trunk@51946 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/admin/common.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index 636237f0b4f2d..9c583f3d79efe 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1695,6 +1695,25 @@ $( function() { } } ); + // Close sidebar when focus moves outside of toggle and sidebar. + $( '#wp-admin-bar-menu-toggle, #adminmenumain' ).on( 'focusout', function() { + var focusIsInToggle, focusIsInSidebar; + + if ( ! $wpwrap.hasClass( 'wp-responsive-open' ) ) { + return; + } + + // A brief delay is required to allow focus to switch to another element. + setTimeout( function() { + focusIsInToggle = $.contains( $( '#wp-admin-bar-menu-toggle' )[0], $( ':focus' )[0] ); + focusIsInSidebar = $.contains( $( '#adminmenumain' )[0], $( ':focus' )[0] ); + + if ( ! focusIsInToggle && ! focusIsInSidebar ) { + $( '#wp-admin-bar-menu-toggle' ).trigger( 'click.wp-responsive' ); + } + }, 10 ); + } ); + // Add menu events. $adminmenu.on( 'click.wp-responsive', 'li.wp-has-submenu > a', function( event ) { if ( ! $adminmenu.data('wp-responsive') ) { From 965a38fc82d998ac921d42dc20f7614583688fc7 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Thu, 28 Oct 2021 17:49:08 +0000 Subject: [PATCH 240/257] Media: Remove deprecated click function in media uploader. Replace the call to jQuery's deprecated click handler. Props kapilpaul. Fixes #53261. git-svn-id: https://develop.svn.wordpress.org/trunk@51947 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/vendor/plupload/handlers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/vendor/plupload/handlers.js b/src/js/_enqueues/vendor/plupload/handlers.js index 2560495b7690d..fa602daf434ab 100644 --- a/src/js/_enqueues/vendor/plupload/handlers.js +++ b/src/js/_enqueues/vendor/plupload/handlers.js @@ -149,7 +149,7 @@ function prepareMediaItemInit( fileObj ) { jQuery( '.filename.original', item ).replaceWith( jQuery( '.filename.new', item ) ); // Bind Ajax to the new Delete button. - jQuery( 'a.delete', item ).click( function(){ + jQuery( 'a.delete', item ).on( 'click', function(){ // Tell the server to delete it. TODO: Handle exceptions. jQuery.ajax({ url: ajaxurl, @@ -167,7 +167,7 @@ function prepareMediaItemInit( fileObj ) { }); // Bind Ajax to the new Undo button. - jQuery( 'a.undo', item ).click( function(){ + jQuery( 'a.undo', item ).on( 'click', function(){ // Tell the server to untrash it. TODO: Handle exceptions. jQuery.ajax({ url: ajaxurl, From 59dc04981624986286ebfd66fb428017659987d9 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 29 Oct 2021 15:33:18 +0000 Subject: [PATCH 241/257] Site Health: Correct and improve the documentation for the `debug_information` hook. This corrects the structure of the documentation so it accurately reflects the array elements contained within. See #53399. git-svn-id: https://develop.svn.wordpress.org/trunk@51949 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-debug-data.php | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index b1e72314c5b26..4746546b968c6 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -1408,7 +1408,7 @@ public static function debug_data() { } /** - * Add or modify the debug information. + * Add to or modify the debug information shown on the Tools -> Site Health -> Info screen. * * Plugin or themes may wish to introduce their own debug information without creating additional admin pages * they can utilize this filter to introduce their own sections or add more data to existing sections. @@ -1417,35 +1417,43 @@ public static function debug_data() { * a prefix, both for consistency as well as avoiding key collisions. Note that the array keys are used as labels * for the copied data. * - * All strings are expected to be plain text except $description that can contain inline HTML tags (see below). + * All strings are expected to be plain text except `$description` that can contain inline HTML tags (see below). * * @since 5.2.0 * * @param array $args { * The debug information to be added to the core information page. * - * This is an associative multi-dimensional array, up to three levels deep. The topmost array holds the sections. - * Each section has a `$fields` associative array (see below), and each `$value` in `$fields` can be - * another associative array of name/value pairs when there is more structured data to display. + * This is an associative multi-dimensional array, up to three levels deep. The topmost array holds the sections, keyed by section ID. * - * @type string $label The title for this section of the debug output. - * @type string $description Optional. A description for your information section which may contain basic HTML - * markup, inline tags only as it is outputted in a paragraph. - * @type boolean $show_count Optional. If set to `true` the amount of fields will be included in the title for - * this section. - * @type boolean $private Optional. If set to `true` the section and all associated fields will be excluded - * from the copied data. - * @type array $fields { - * An associative array containing the data to be displayed. + * @type array ...$0 { + * Each section has a `$fields` associative array (see below), and each `$value` in `$fields` can be + * another associative array of name/value pairs when there is more structured data to display. * - * @type string $label The label for this piece of information. - * @type string $value The output that is displayed for this field. Text should be translated. Can be - * an associative array that is displayed as name/value pairs. - * @type string $debug Optional. The output that is used for this field when the user copies the data. - * It should be more concise and not translated. If not set, the content of `$value` is used. - * Note that the array keys are used as labels for the copied data. - * @type boolean $private Optional. If set to `true` the field will not be included in the copied data - * allowing you to show, for example, API keys here. + * @type string $label Required. The title for this section of the debug output. + * @type string $description Optional. A description for your information section which may contain basic HTML + * markup, inline tags only as it is outputted in a paragraph. + * @type bool $show_count Optional. If set to `true` the amount of fields will be included in the title for + * this section. + * @type bool $private Optional. If set to `true` the section and all associated fields will be excluded + * from the copied data. Default false. + * @type array $fields { + * Required. An associative array containing the fields to be displayed in the section, keyed by field ID. + * + * @type array ...$0 { + * An associative array containing the data to be displayed for the field. + * + * @type string $label Required. The label for this piece of information. + * @type mixed $value Required. The output that is displayed for this field. Text should be translated. Can be + * an associative array that is displayed as name/value pairs. + * Accepted types: `string|int|float|(string|int|float)[]`. + * @type string $debug Optional. The output that is used for this field when the user copies the data. + * It should be more concise and not translated. If not set, the content of `$value` is used. + * Note that the array keys are used as labels for the copied data. + * @type bool $private Optional. If set to `true` the field will not be included in the copied data + * allowing you to show, for example, API keys here. Default false. + * } + * } * } * } */ From c92c8df5d8968b5b0979f2340e1d188f98570a82 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 29 Oct 2021 16:50:29 +0000 Subject: [PATCH 242/257] Date/Time: Improve the docblocks for various date and time related functions. See #53399, #28992, #40653 git-svn-id: https://develop.svn.wordpress.org/trunk@51950 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 46 +++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 86a21a8e78e47..5101e142bfa51 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -10,9 +10,9 @@ /** * Convert given MySQL date string into a different format. * - * `$format` should be a PHP date format string. - * 'U' and 'G' formats will return a sum of timestamp with timezone offset. - * `$date` is expected to be local time in MySQL format (`Y-m-d H:i:s`). + * - `$format` should be a PHP date format string. + * - 'U' and 'G' formats will return an integer sum of timestamp with timezone offset. + * - `$date` is expected to be local time in MySQL format (`Y-m-d H:i:s`). * * Historically UTC time could be passed to the function to produce Unix timestamp. * @@ -24,7 +24,7 @@ * @param string $format Format of the date to return. * @param string $date Date string to convert. * @param bool $translate Whether the return date should be translated. Default true. - * @return string|int|false Formatted date string or sum of Unix timestamp and timezone offset. + * @return string|int|false Integer if `$format` is 'U' or 'G', string otherwise. * False on failure. */ function mysql2date( $format, $date, $translate = true ) { @@ -53,20 +53,21 @@ function mysql2date( $format, $date, $translate = true ) { /** * Retrieves the current time based on specified type. * - * The 'mysql' type will return the time in the format for MySQL DATETIME field. - * The 'timestamp' type will return the current timestamp or a sum of timestamp - * and timezone offset, depending on `$gmt`. - * Other strings will be interpreted as PHP date formats (e.g. 'Y-m-d'). + * - The 'mysql' type will return the time in the format for MySQL DATETIME field. + * - The 'timestamp' or 'U' types will return the current timestamp or a sum of timestamp + * and timezone offset, depending on `$gmt`. + * - Other strings will be interpreted as PHP date formats (e.g. 'Y-m-d'). * - * If $gmt is set to either '1' or 'true', then both types will use GMT time. - * if $gmt is false, the output is adjusted with the GMT offset in the WordPress option. + * If `$gmt` is a truthy value then both types will use GMT time, otherwise the + * output is adjusted with the GMT offset for the site. * * @since 1.0.0 + * @since 5.3.0 Now returns an integer if `$type` is 'U'. Previously a string was returned. * - * @param string $type Type of time to retrieve. Accepts 'mysql', 'timestamp', + * @param string $type Type of time to retrieve. Accepts 'mysql', 'timestamp', 'U', * or PHP date format string (e.g. 'Y-m-d'). * @param int|bool $gmt Optional. Whether to use GMT timezone. Default false. - * @return int|string Integer if $type is 'timestamp', string otherwise. + * @return int|string Integer if `$type` is 'timestamp' or 'U', string otherwise. */ function current_time( $type, $gmt = 0 ) { // Don't use non-GMT timestamp, unless you know the difference and really need to. @@ -85,7 +86,7 @@ function current_time( $type, $gmt = 0 ) { } /** - * Retrieves the current time as an object with the timezone from settings. + * Retrieves the current time as an object using the site's timezone. * * @since 5.3.0 * @@ -96,14 +97,23 @@ function current_datetime() { } /** - * Retrieves the timezone from site settings as a string. + * Retrieves the timezone of the site as a string. * - * Uses the `timezone_string` option to get a proper timezone if available, - * otherwise falls back to an offset. + * Uses the `timezone_string` option to get a proper timezone name if available, + * otherwise falls back to a manual UTC ± offset. + * + * Example return values: + * + * - 'Europe/Rome' + * - 'America/North_Dakota/New_Salem' + * - 'UTC' + * - '-06:30' + * - '+00:00' + * - '+08:45' * * @since 5.3.0 * - * @return string PHP timezone string or a ±HH:MM offset. + * @return string PHP timezone name or a ±HH:MM offset. */ function wp_timezone_string() { $timezone_string = get_option( 'timezone_string' ); @@ -125,7 +135,7 @@ function wp_timezone_string() { } /** - * Retrieves the timezone from site settings as a `DateTimeZone` object. + * Retrieves the timezone of the site as a `DateTimeZone` object. * * Timezone can be based on a PHP timezone string or a ±HH:MM offset. * From ba6b4c4cd962b18d013a0762b978f22420897c0f Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 29 Oct 2021 16:51:42 +0000 Subject: [PATCH 243/257] Coding Standards: Correct alignment in `WP_Site_Health::get_test_update_temp_backup_writable()`. This fixes an `Equals sign not aligned with surrounding assignments; expected 1 space but found 6 spaces` WPCS warning. Follow-up to [51815]. See #51857, #53359. git-svn-id: https://develop.svn.wordpress.org/trunk@51951 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-wp-site-health.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index c1431d2f85874..c45a2ccc63e08 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -1978,8 +1978,8 @@ public function get_test_update_temp_backup_writable() { $themes_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup/themes" ); if ( $plugins_dir_exists && ! $plugins_dir_is_writable && $themes_dir_exists && ! $themes_dir_is_writable ) { - $result['status'] = 'critical'; - $result['label'] = sprintf( + $result['status'] = 'critical'; + $result['label'] = sprintf( /* translators: %s: temp-backup */ __( 'Plugins and themes %s directories exist but are not writable' ), 'temp-backup' @@ -1994,8 +1994,8 @@ public function get_test_update_temp_backup_writable() { } if ( $plugins_dir_exists && ! $plugins_dir_is_writable ) { - $result['status'] = 'critical'; - $result['label'] = sprintf( + $result['status'] = 'critical'; + $result['label'] = sprintf( /* translators: %s: temp-backup */ __( 'Plugins %s directory exists but is not writable' ), 'temp-backup' @@ -2009,8 +2009,8 @@ public function get_test_update_temp_backup_writable() { } if ( $themes_dir_exists && ! $themes_dir_is_writable ) { - $result['status'] = 'critical'; - $result['label'] = sprintf( + $result['status'] = 'critical'; + $result['label'] = sprintf( /* translators: %s: temp-backup */ __( 'Themes %s directory exists but is not writable' ), 'temp-backup' @@ -2024,8 +2024,8 @@ public function get_test_update_temp_backup_writable() { } if ( ( ! $plugins_dir_exists || ! $themes_dir_exists ) && $backup_dir_exists && ! $backup_dir_is_writable ) { - $result['status'] = 'critical'; - $result['label'] = sprintf( + $result['status'] = 'critical'; + $result['label'] = sprintf( /* translators: %s: temp-backup */ __( 'The %s directory exists but is not writable' ), 'temp-backup' @@ -2039,8 +2039,8 @@ public function get_test_update_temp_backup_writable() { } if ( ! $backup_dir_exists && $upgrade_dir_exists && ! $upgrade_dir_is_writable ) { - $result['status'] = 'critical'; - $result['label'] = sprintf( + $result['status'] = 'critical'; + $result['label'] = sprintf( /* translators: %s: upgrade */ __( 'The %s directory exists but is not writable' ), 'upgrade' @@ -2054,8 +2054,8 @@ public function get_test_update_temp_backup_writable() { } if ( ! $upgrade_dir_exists && ! $wp_filesystem->is_writable( $wp_content ) ) { - $result['status'] = 'critical'; - $result['label'] = sprintf( + $result['status'] = 'critical'; + $result['label'] = sprintf( /* translators: %s: upgrade */ __( 'The %s directory cannot be created' ), 'upgrade' From 5b1da06715ede7a5f29b7a65bc12bdb4123a4ed2 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Fri, 29 Oct 2021 19:40:26 +0000 Subject: [PATCH 244/257] Build/Test Tools: Escape `$` within commit messages for `$variables. This ensures the variables are preserved in the Slack message. Props ocean90, desrosj. See #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51952 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/slack-notifications.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slack-notifications.yml b/.github/workflows/slack-notifications.yml index b75bc5b01d28f..021f10fb175dd 100644 --- a/.github/workflows/slack-notifications.yml +++ b/.github/workflows/slack-notifications.yml @@ -110,7 +110,7 @@ jobs: - name: Prepare commit message. id: commit-message run: | - COMMIT_MESSAGE=$(cat <<'EOF' | awk 'NR==1' | sed 's/`/\\`/g' | sed 's/\"/\\\\"/g' + COMMIT_MESSAGE=$(cat <<'EOF' | awk 'NR==1' | sed 's/`/\\`/g' | sed 's/\"/\\\\"/g' | sed 's/\$/\\$/g' ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_commit.message || github.event.head_commit.message }} EOF ) From f28310aedaed87a274b8510e8d80c7d732fdf41c Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Fri, 29 Oct 2021 19:59:05 +0000 Subject: [PATCH 245/257] Build/Test Tools: Adjust Slack notifications for `scheduled` and `workflow_dispatch` events. This makes the needed adjustments to fix Slack notifications for `scheduled` and `workflow_dispatch` events. The data needed to send notifications for these events are stored in different locations, or need to be accessed through API requests. Follow up to [51921], [51937]. See #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51953 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/slack-notifications.yml | 42 +++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/slack-notifications.yml b/.github/workflows/slack-notifications.yml index 021f10fb175dd..2f8a65c992cb2 100644 --- a/.github/workflows/slack-notifications.yml +++ b/.github/workflows/slack-notifications.yml @@ -64,38 +64,25 @@ jobs: steps: - name: Get the workflow ID id: current-workflow-id - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 with: script: | const workflow_run = await github.rest.actions.getWorkflowRun({ - owner: '${{ github.repository_owner }}', - repo: 'wordpress-develop', + owner: context.repo.owner, + repo: context.repo.repo, run_id: ${{ github.run_id }}, }); return workflow_run.data.workflow_id; - - name: Get the workflow URL - id: current-workflow-url - uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 - if: ${{ github.event_name == 'push' }} - with: - script: | - const workflow_run = await github.rest.actions.getWorkflowRun({ - owner: '${{ github.repository_owner }}', - repo: 'wordpress-develop', - run_id: ${{ github.run_id }}, - }); - return workflow_run.data.html_url; - - name: Get details about the previous workflow run id: previous-result uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 with: script: | const previous_runs = await github.rest.actions.listWorkflowRuns({ - owner: '${{ github.repository_owner }}', - repo: 'wordpress-develop', + owner: context.repo.owner, + repo: context.repo.repo, workflow_id: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.workflow_id || steps.current-workflow-id.outputs.result }}, branch: '${{ env.CURRENT_BRANCH }}', per_page: 1, @@ -107,18 +94,31 @@ jobs: id: previous-conclusion run: echo "::set-output name=previous_conclusion::${{ steps.previous-result.outputs.result }}" + - name: Get the commit message + id: current-commit-message + uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }} + with: + script: | + const commit_details = await github.rest.repos.getCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: '${{ github.sha }}' + }); + return commit_details.data.commit.message; + - name: Prepare commit message. id: commit-message run: | - COMMIT_MESSAGE=$(cat <<'EOF' | awk 'NR==1' | sed 's/`/\\`/g' | sed 's/\"/\\\\"/g' | sed 's/\$/\\$/g' - ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_commit.message || github.event.head_commit.message }} + COMMIT_MESSAGE=$(cat <<'EOF' | awk 'NR==1' | sed 's/`/\\`/g' | sed 's/\"/\\\\\\"/g' | sed 's/\$/\\$/g' + ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_commit.message || ( github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' ) && fromJson( steps.current-commit-message.outputs.result ) || github.event.head_commit.message }} EOF ) echo "::set-output name=commit_message_escaped::${COMMIT_MESSAGE}" - name: Construct payload and store as an output id: create-payload - run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.name || github.workflow }}\",\"ref_name\":\"${{ env.CURRENT_BRANCH }}\",\"run_url\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.html_url || steps.current-workflow-url.outputs.result }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" + run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.name || github.workflow }}\",\"ref_name\":\"${{ env.CURRENT_BRANCH }}\",\"run_url\":\"https://github.com/WordPress/wordpress-develop/runs/${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.run_id }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" # Posts notifications when a workflow fails. failure: From 86c54d872e071fbc146fd6e50568cc49c020ddcb Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Fri, 29 Oct 2021 20:33:19 +0000 Subject: [PATCH 246/257] Build/Test Tools: Use correct URL for a GitHub Action workflow run. Follow up to [51921], [51937], [51953]. Unprops desrosj. See #53363. git-svn-id: https://develop.svn.wordpress.org/trunk@51954 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/slack-notifications.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slack-notifications.yml b/.github/workflows/slack-notifications.yml index 2f8a65c992cb2..0e435eb5b7382 100644 --- a/.github/workflows/slack-notifications.yml +++ b/.github/workflows/slack-notifications.yml @@ -118,7 +118,7 @@ jobs: - name: Construct payload and store as an output id: create-payload - run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.name || github.workflow }}\",\"ref_name\":\"${{ env.CURRENT_BRANCH }}\",\"run_url\":\"https://github.com/WordPress/wordpress-develop/runs/${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.run_id }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" + run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.name || github.workflow }}\",\"ref_name\":\"${{ env.CURRENT_BRANCH }}\",\"run_url\":\"https://github.com/WordPress/wordpress-develop/actions/runs/${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.run_id }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" # Posts notifications when a workflow fails. failure: From ec5ed03bf62ebf72b36da58d3578d0c98cdda1d2 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 29 Oct 2021 23:11:32 +0000 Subject: [PATCH 247/257] Docs: Miscellaneous docblock improvements. See #53399 git-svn-id: https://develop.svn.wordpress.org/trunk@51955 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/plugin.php | 43 +++++++++++++++++++------------ src/wp-admin/includes/upgrade.php | 1 - src/wp-includes/cron.php | 4 +-- src/wp-includes/formatting.php | 4 +-- src/wp-includes/theme.php | 2 +- src/wp-login.php | 24 ++++++++--------- 6 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 6e332c247e85e..0934d6b8100f7 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -1278,7 +1278,7 @@ function uninstall_plugin( $plugin ) { // /** - * Add a top-level menu page. + * Adds a top-level menu page. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1350,7 +1350,7 @@ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $func } /** - * Add a submenu page. + * Adds a submenu page. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1474,7 +1474,7 @@ function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, } /** - * Add submenu page to the Tools main menu. + * Adds a submenu page to the Tools main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1498,7 +1498,7 @@ function add_management_page( $page_title, $menu_title, $capability, $menu_slug, } /** - * Add submenu page to the Settings main menu. + * Adds a submenu page to the Settings main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1522,7 +1522,7 @@ function add_options_page( $page_title, $menu_title, $capability, $menu_slug, $f } /** - * Add submenu page to the Appearance main menu. + * Adds a submenu page to the Appearance main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1546,7 +1546,7 @@ function add_theme_page( $page_title, $menu_title, $capability, $menu_slug, $fun } /** - * Add submenu page to the Plugins main menu. + * Adds a submenu page to the Plugins main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1570,7 +1570,7 @@ function add_plugins_page( $page_title, $menu_title, $capability, $menu_slug, $f } /** - * Add submenu page to the Users/Profile main menu. + * Adds a submenu page to the Users/Profile main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1599,7 +1599,7 @@ function add_users_page( $page_title, $menu_title, $capability, $menu_slug, $fun } /** - * Add submenu page to the Dashboard main menu. + * Adds a submenu page to the Dashboard main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1623,7 +1623,7 @@ function add_dashboard_page( $page_title, $menu_title, $capability, $menu_slug, } /** - * Add submenu page to the Posts main menu. + * Adds a submenu page to the Posts main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1647,7 +1647,7 @@ function add_posts_page( $page_title, $menu_title, $capability, $menu_slug, $fun } /** - * Add submenu page to the Media main menu. + * Adds a submenu page to the Media main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1671,7 +1671,7 @@ function add_media_page( $page_title, $menu_title, $capability, $menu_slug, $fun } /** - * Add submenu page to the Links main menu. + * Adds a submenu page to the Links main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1695,7 +1695,7 @@ function add_links_page( $page_title, $menu_title, $capability, $menu_slug, $fun } /** - * Add submenu page to the Pages main menu. + * Adds a submenu page to the Pages main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1719,7 +1719,7 @@ function add_pages_page( $page_title, $menu_title, $capability, $menu_slug, $fun } /** - * Add submenu page to the Comments main menu. + * Adds a submenu page to the Comments main menu. * * This function takes a capability which will be used to determine whether * or not a page is included in the menu. @@ -1743,7 +1743,12 @@ function add_comments_page( $page_title, $menu_title, $capability, $menu_slug, $ } /** - * Remove a top-level admin menu. + * Removes a top-level admin menu. + * + * Example usage: + * + * - `remove_menu_page( 'tools.php' )` + * - `remove_menu_page( 'plugin_menu_slug' )` * * @since 3.1.0 * @@ -1766,7 +1771,13 @@ function remove_menu_page( $menu_slug ) { } /** - * Remove an admin submenu. + * Removes an admin submenu. + * + * Example usage: + * + * - `remove_submenu_page( 'themes.php', 'nav-menus.php' )` + * - `remove_submenu_page( 'tools.php', 'plugin_submenu_slug' )` + * - `remove_submenu_page( 'plugin_menu_slug', 'plugin_submenu_slug' )` * * @since 3.1.0 * @@ -1794,7 +1805,7 @@ function remove_submenu_page( $menu_slug, $submenu_slug ) { } /** - * Get the URL to access a particular menu page based on the slug it was registered with. + * Gets the URL to access a particular menu page based on the slug it was registered with. * * If the slug hasn't been registered properly, no URL will be returned. * diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index b5588197715c3..3be073f7af26a 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -1878,7 +1878,6 @@ function upgrade_370() { * * @ignore * @since 3.7.2 - * @since 3.8.0 * * @global int $wp_current_db_version The old (current) database version. */ diff --git a/src/wp-includes/cron.php b/src/wp-includes/cron.php index 41efeebc44f18..8e50349f5d652 100644 --- a/src/wp-includes/cron.php +++ b/src/wp-includes/cron.php @@ -1040,7 +1040,7 @@ function _wp_cron() { * @since 2.1.0 * @since 5.4.0 The 'weekly' schedule was added. * - * @return array + * @return array[] */ function wp_get_schedules() { $schedules = array( @@ -1067,7 +1067,7 @@ function wp_get_schedules() { * * @since 2.1.0 * - * @param array $new_schedules An array of non-default cron schedules. Default empty. + * @param array[] $new_schedules An array of non-default cron schedule arrays. Default empty. */ return array_merge( apply_filters( 'cron_schedules', array() ), $schedules ); } diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 2eccfb0fde3e3..9461297dee7a3 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3539,8 +3539,8 @@ function get_gmt_from_date( $string, $format = 'Y-m-d H:i:s' ) { /** * Given a date in UTC or GMT timezone, returns that date in the timezone of the site. * - * Requires and returns a date in the Y-m-d H:i:s format. - * Return format can be overridden using the $format parameter. + * Requires a date in the Y-m-d H:i:s format. + * Default return format of 'Y-m-d H:i:s' can be overridden using the `$format` parameter. * * @since 1.2.0 * diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 384c056404b2a..e233b4127a7da 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -3073,7 +3073,7 @@ function require_if_theme_supports( $feature, $include ) { * @type string $type The type of data associated with this feature. * Valid values are 'string', 'boolean', 'integer', * 'number', 'array', and 'object'. Defaults to 'boolean'. - * @type boolean $variadic Does this feature utilize the variadic support + * @type bool $variadic Does this feature utilize the variadic support * of add_theme_support(), or are all arguments specified * as the second parameter. Must be used with the "array" type. * @type string $description A short description of the feature. Included in diff --git a/src/wp-login.php b/src/wp-login.php index 0d030863ed1f7..b7c14b4c83f60 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -434,18 +434,18 @@ function wp_login_viewport_meta() { * * Possible hook names include: * - * - 'login_form_checkemail' - * - 'login_form_confirm_admin_email' - * - 'login_form_confirmaction' - * - 'login_form_entered_recovery_mode' - * - 'login_form_login' - * - 'login_form_logout' - * - 'login_form_lostpassword' - * - 'login_form_postpass' - * - 'login_form_register' - * - 'login_form_resetpass' - * - 'login_form_retrievepassword' - * - 'login_form_rp' + * - `login_form_checkemail` + * - `login_form_confirm_admin_email` + * - `login_form_confirmaction` + * - `login_form_entered_recovery_mode` + * - `login_form_login` + * - `login_form_logout` + * - `login_form_lostpassword` + * - `login_form_postpass` + * - `login_form_register` + * - `login_form_resetpass` + * - `login_form_retrievepassword` + * - `login_form_rp` * * @since 2.8.0 */ From 1f1f1d64d8bea6fe718ca9a83de5e3dcf8f1547e Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sat, 30 Oct 2021 13:39:24 +0000 Subject: [PATCH 248/257] Docs: Further update the `debug_information` filter description per the documentation standards. Specifically, this ensures that the DocBlock follows the line wrapping recommendations. Follow-up to [44986], [45156], [45259], [51949]. See #53399. git-svn-id: https://develop.svn.wordpress.org/trunk@51956 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-debug-data.php | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 4746546b968c6..91c6b837c1069 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -1410,48 +1410,58 @@ public static function debug_data() { /** * Add to or modify the debug information shown on the Tools -> Site Health -> Info screen. * - * Plugin or themes may wish to introduce their own debug information without creating additional admin pages - * they can utilize this filter to introduce their own sections or add more data to existing sections. + * Plugin or themes may wish to introduce their own debug information without creating + * additional admin pages. They can utilize this filter to introduce their own sections + * or add more data to existing sections. * - * Array keys for sections added by core are all prefixed with `wp-`, plugins and themes should use their own slug as - * a prefix, both for consistency as well as avoiding key collisions. Note that the array keys are used as labels - * for the copied data. + * Array keys for sections added by core are all prefixed with `wp-`. Plugins and themes + * should use their own slug as a prefix, both for consistency as well as avoiding + * key collisions. Note that the array keys are used as labels for the copied data. * - * All strings are expected to be plain text except `$description` that can contain inline HTML tags (see below). + * All strings are expected to be plain text except `$description` that can contain + * inline HTML tags (see below). * * @since 5.2.0 * * @param array $args { * The debug information to be added to the core information page. * - * This is an associative multi-dimensional array, up to three levels deep. The topmost array holds the sections, keyed by section ID. + * This is an associative multi-dimensional array, up to three levels deep. + * The topmost array holds the sections, keyed by section ID. * * @type array ...$0 { - * Each section has a `$fields` associative array (see below), and each `$value` in `$fields` can be - * another associative array of name/value pairs when there is more structured data to display. + * Each section has a `$fields` associative array (see below), and each `$value` in `$fields` + * can be another associative array of name/value pairs when there is more structured data + * to display. * * @type string $label Required. The title for this section of the debug output. - * @type string $description Optional. A description for your information section which may contain basic HTML - * markup, inline tags only as it is outputted in a paragraph. - * @type bool $show_count Optional. If set to `true` the amount of fields will be included in the title for - * this section. - * @type bool $private Optional. If set to `true` the section and all associated fields will be excluded - * from the copied data. Default false. + * @type string $description Optional. A description for your information section which + * may contain basic HTML markup, inline tags only as it is + * outputted in a paragraph. + * @type bool $show_count Optional. If set to `true`, the amount of fields will be included + * in the title for this section. Default false. + * @type bool $private Optional. If set to `true`, the section and all associated fields + * will be excluded from the copied data. Default false. * @type array $fields { - * Required. An associative array containing the fields to be displayed in the section, keyed by field ID. + * Required. An associative array containing the fields to be displayed in the section, + * keyed by field ID. * * @type array ...$0 { * An associative array containing the data to be displayed for the field. * * @type string $label Required. The label for this piece of information. - * @type mixed $value Required. The output that is displayed for this field. Text should be translated. Can be - * an associative array that is displayed as name/value pairs. + * @type mixed $value Required. The output that is displayed for this field. + * Text should be translated. Can be an associative array + * that is displayed as name/value pairs. * Accepted types: `string|int|float|(string|int|float)[]`. - * @type string $debug Optional. The output that is used for this field when the user copies the data. - * It should be more concise and not translated. If not set, the content of `$value` is used. - * Note that the array keys are used as labels for the copied data. - * @type bool $private Optional. If set to `true` the field will not be included in the copied data - * allowing you to show, for example, API keys here. Default false. + * @type string $debug Optional. The output that is used for this field when + * the user copies the data. It should be more concise and + * not translated. If not set, the content of `$value` + * is used. Note that the array keys are used as labels + * for the copied data. + * @type bool $private Optional. If set to `true`, the field will be excluded + * from the copied data, allowing you to show, for example, + * API keys here. Default false. * } * } * } From 2716cc52afce7c59cb470b8d587ae3e9ab75d76e Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 30 Oct 2021 20:15:59 +0000 Subject: [PATCH 249/257] Docs: Miscellaneous docblock improvements. See #53399 git-svn-id: https://develop.svn.wordpress.org/trunk@51957 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/media.php | 2 +- src/wp-includes/functions.php | 7 ++++++- src/wp-includes/general-template.php | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 3cb96b2bb4106..8768c75ee28c3 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -1521,7 +1521,7 @@ function get_attachment_fields_to_edit( $post, $errors = null ) { * * @global WP_Query $wp_the_query WordPress Query object. * - * @param int $post_id Optional. Post ID. + * @param int $post_id Post ID. * @param array $errors Errors for attachment, if any. * @return string */ diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 5101e142bfa51..0950f794659c4 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -565,7 +565,12 @@ function human_readable_duration( $duration = '' ) { * * @param string $mysqlstring Date or datetime field type from MySQL. * @param int|string $start_of_week Optional. Start of the week as an integer. Default empty string. - * @return array Keys are 'start' and 'end'. + * @return int[] { + * Week start and end dates as Unix timestamps. + * + * @type int $start The week start date as a Unix timestamp. + * @type int $end The week end date as a Unix timestamp. + * } */ function get_weekstartend( $mysqlstring, $start_of_week = '' ) { // MySQL string year. diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 4ab79e8ddc48d..4a9835a478f75 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -189,7 +189,7 @@ function get_template_part( $slug, $name = null, $args = array() ) { $templates[] = "{$slug}.php"; /** - * Fires before a template part is loaded. + * Fires before an attempt is made to locate and load a template part. * * @since 5.2.0 * @since 5.5.0 The `$args` parameter was added. From 9f9c81f5677944bc13b139989d3d3463d618b0ec Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 30 Oct 2021 20:25:19 +0000 Subject: [PATCH 250/257] Application Passwords: Various docblock improvements. See #53399, #42790 git-svn-id: https://develop.svn.wordpress.org/trunk@51958 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-application-passwords.php | 50 +++++++++++++++---- src/wp-includes/rest-api.php | 2 +- ...-rest-application-passwords-controller.php | 14 +++--- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/class-wp-application-passwords.php b/src/wp-includes/class-wp-application-passwords.php index a86660b27b7be..fd7fac2162c80 100644 --- a/src/wp-includes/class-wp-application-passwords.php +++ b/src/wp-includes/class-wp-application-passwords.php @@ -43,7 +43,7 @@ class WP_Application_Passwords { /** * Checks if Application Passwords are being used by the site. * - * This returns true if at least one App Password has ever been created. + * This returns true if at least one Application Password has ever been created. * * @since 5.6.0 * @@ -61,7 +61,12 @@ public static function is_in_use() { * @since 5.7.0 Returns WP_Error if application name already exists. * * @param int $user_id User ID. - * @param array $args Information about the application password. + * @param array $args { + * Arguments used to create the application password. + * + * @type string $name The name of the application password. + * @type string $app_id A UUID provided by the application to uniquely identify it. + * } * @return array|WP_Error The first key in the array is the new password, the second is its detailed information. * A WP_Error instance is returned on error. */ @@ -110,9 +115,24 @@ public static function create_new_application_password( $user_id, $args = array( * @since 5.6.0 * * @param int $user_id The user ID. - * @param array $new_item The details about the created password. - * @param string $new_password The unhashed generated app password. - * @param array $args Information used to create the application password. + * @param array $new_item { + * The details about the created password. + * + * @type string $uuid The unique identifier for the application password. + * @type string $app_id A UUID provided by the application to uniquely identify it. + * @type string $name The name of the application password. + * @type string $password A one-way hash of the password. + * @type int $created Unix timestamp of when the password was created. + * @type null $last_used Null. + * @type null $last_ip Null. + * } + * @param string $new_password The unhashed generated application password. + * @param array $args { + * Arguments used to create the application password. + * + * @type string $name The name of the application password. + * @type string $app_id A UUID provided by the application to uniquely identify it. + * } */ do_action( 'wp_create_application_password', $user_id, $new_item, $new_password, $args ); @@ -125,7 +145,19 @@ public static function create_new_application_password( $user_id, $args = array( * @since 5.6.0 * * @param int $user_id User ID. - * @return array The list of app passwords. + * @return array { + * The list of app passwords. + * + * @type array ...$0 { + * @type string $uuid The unique identifier for the application password. + * @type string $app_id A UUID provided by the application to uniquely identify it. + * @type string $name The name of the application password. + * @type string $password A one-way hash of the password. + * @type int $created Unix timestamp of when the password was created. + * @type int|null $last_used The Unix timestamp of the GMT date the application password was last used. + * @type string|null $last_ip The IP address the application password was last used by. + * } + * } */ public static function get_user_application_passwords( $user_id ) { $passwords = get_user_meta( $user_id, static::USERMETA_KEY_APPLICATION_PASSWORDS, true ); @@ -172,13 +204,13 @@ public static function get_user_application_password( $user_id, $uuid ) { } /** - * Checks if application name exists for this user. + * Checks if an application password with the given name exists for this user. * * @since 5.7.0 * * @param int $user_id User ID. * @param string $name Application name. - * @return bool Whether provided application name exists or not. + * @return bool Whether the provided application name exists. */ public static function application_name_exists_for_user( $user_id, $name ) { $passwords = static::get_user_application_passwords( $user_id ); @@ -352,7 +384,7 @@ public static function delete_all_application_passwords( $user_id ) { } /** - * Sets a users application passwords. + * Sets a user's application passwords. * * @since 5.6.0 * diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 7e66ecd801218..3d89f16009196 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1091,7 +1091,7 @@ function rest_application_password_collect_status( $user_or_error, $app_password * * @global string|null $wp_rest_application_password_uuid * - * @return string|null The App Password UUID, or null if Application Passwords was not used. + * @return string|null The Application Password UUID, or null if Application Passwords was not used. */ function rest_get_authenticated_app_password() { global $wp_rest_application_password_uuid; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php index ecdc221b5fcbc..bdf1959c623b3 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php @@ -356,7 +356,7 @@ public function update_item( $request ) { } /** - * Checks if a given request has access to delete all application passwords. + * Checks if a given request has access to delete all application passwords for a user. * * @since 5.6.0 * @@ -382,7 +382,7 @@ public function delete_items_permissions_check( $request ) { } /** - * Deletes all application passwords. + * Deletes all application passwords for a user. * * @since 5.6.0 * @@ -411,7 +411,7 @@ public function delete_items( $request ) { } /** - * Checks if a given request has access to delete a specific application password. + * Checks if a given request has access to delete a specific application password for a user. * * @since 5.6.0 * @@ -437,7 +437,7 @@ public function delete_item_permissions_check( $request ) { } /** - * Deletes one application password. + * Deletes an application password for a user. * * @since 5.6.0 * @@ -474,7 +474,7 @@ public function delete_item( $request ) { } /** - * Checks if a given request has access to get the currently used application password. + * Checks if a given request has access to get the currently used application password for a user. * * @since 5.7.0 * @@ -500,7 +500,7 @@ public function get_current_item_permissions_check( $request ) { } /** - * Retrieves the application password being currently used for authentication. + * Retrieves the application password being currently used for authentication of a user. * * @since 5.7.0 * @@ -723,7 +723,7 @@ protected function get_user( $request ) { } /** - * Gets the requested application password. + * Gets the requested application password for a user. * * @since 5.6.0 * From 57ded3c85da779b5e1bd1c6b01bcb5b97e131d09 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Sun, 31 Oct 2021 05:17:53 +0000 Subject: [PATCH 251/257] REST API: Add visibility information to the Post Types controller. Props spacedmonkey, peterwilsoncc. Fixes #54055. git-svn-id: https://develop.svn.wordpress.org/trunk@51959 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-rest-post-types-controller.php | 23 +++++++++++++++++++ .../rest-api/rest-post-types-controller.php | 8 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php index 1301246dea206..e2598ac89d01e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php @@ -197,6 +197,13 @@ public function prepare_item_for_response( $item, $request ) { $data['hierarchical'] = $post_type->hierarchical; } + if ( in_array( 'visibility', $fields, true ) ) { + $data['visibility'] = array( + 'show_in_nav_menus' => (bool) $post_type->show_in_nav_menus, + 'show_ui' => (bool) $post_type->show_ui, + ); + } + if ( in_array( 'viewable', $fields, true ) ) { $data['viewable'] = is_post_type_viewable( $post_type ); } @@ -337,6 +344,22 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), + 'visibility' => array( + 'description' => __( 'The visibility settings for the post type.' ), + 'type' => 'object', + 'context' => array( 'edit' ), + 'readonly' => true, + 'properties' => array( + 'show_ui' => array( + 'description' => __( 'Whether to generate a default UI for managing this post type.' ), + 'type' => 'boolean', + ), + 'show_in_nav_menus' => array( + 'description' => __( 'Whether to make the post type is available for selection in navigation menus.' ), + 'type' => 'boolean', + ), + ), + ), ), ); diff --git a/tests/phpunit/tests/rest-api/rest-post-types-controller.php b/tests/phpunit/tests/rest-api/rest-post-types-controller.php index 0e06bf9d15410..d93f5814c19a7 100644 --- a/tests/phpunit/tests/rest-api/rest-post-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-types-controller.php @@ -144,7 +144,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 10, $properties ); + $this->assertCount( 11, $properties ); $this->assertArrayHasKey( 'capabilities', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'hierarchical', $properties ); @@ -155,6 +155,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'supports', $properties ); $this->assertArrayHasKey( 'taxonomies', $properties ); $this->assertArrayHasKey( 'rest_base', $properties ); + $this->assertArrayHasKey( 'visibility', $properties ); } public function test_get_additional_field_registration() { @@ -216,6 +217,11 @@ protected function check_post_type_obj( $context, $post_type_obj, $data, $links $viewable = is_post_type_viewable( $post_type_obj ); } $this->assertSame( $viewable, $data['viewable'] ); + $visibility = array( + 'show_in_nav_menus' => (bool) $post_type_obj->show_in_nav_menus, + 'show_ui' => (bool) $post_type_obj->show_ui, + ); + $this->assertSame( $visibility, $data['visibility'] ); $this->assertSame( get_all_post_type_supports( $post_type_obj->name ), $data['supports'] ); } else { $this->assertArrayNotHasKey( 'capabilities', $data ); From 07ad6efdf7157d22424496d39d8c5635f28ecfbb Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Sun, 31 Oct 2021 06:06:02 +0000 Subject: [PATCH 252/257] REST API: Send a 500 status code when JSON encoding fails. Previously, a 200 status code would be sent despite the 500 status code present in the response body. Props hermpheus, lalitjalandhar. Fixes #53056. git-svn-id: https://develop.svn.wordpress.org/trunk@51960 602fd350-edb4-49c9-b593-d223f7449a82 --- .../rest-api/class-wp-rest-server.php | 1 + tests/phpunit/includes/spy-rest-server.php | 9 ++++++++ tests/phpunit/tests/rest-api/rest-server.php | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 55f0ce6fc89ef..e2999f7c8231f 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -497,6 +497,7 @@ public function serve_request( $path = null ) { $json_error_message = $this->get_json_last_error(); if ( $json_error_message ) { + $this->set_status( 500 ); $json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, diff --git a/tests/phpunit/includes/spy-rest-server.php b/tests/phpunit/includes/spy-rest-server.php index 596117347ce00..5bfd1d2dba306 100644 --- a/tests/phpunit/includes/spy-rest-server.php +++ b/tests/phpunit/includes/spy-rest-server.php @@ -6,6 +6,7 @@ class Spy_REST_Server extends WP_REST_Server { public $sent_body = ''; public $last_request = null; public $override_by_default = false; + public $status = null; /** * Gets the raw $endpoints data from the server. @@ -37,6 +38,14 @@ public function send_header( $header, $value ) { $this->sent_headers[ $header ] = $value; } + /** + * Stores last set status. + * @param int $code HTTP status. + */ + public function set_status( $status ) { + $this->status = $status; + } + /** * Removes a header from the list of sent headers. * diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 03d1cf21faf19..30977958fa911 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -2022,6 +2022,28 @@ public function test_index_includes_link_to_active_theme_if_authenticated() { $this->assertArrayHasKey( 'https://api.w.org/active-theme', $index->get_links() ); } + /** + * @ticket 53056 + */ + public function test_json_encode_error_results_in_500_status_code() { + register_rest_route( + 'test-ns/v1', + '/test', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => function() { + return new \WP_REST_Response( INF ); + }, + 'permission_callback' => '__return_true', + 'args' => array(), + ), + ) + ); + rest_get_server()->serve_request( '/test-ns/v1/test' ); + $this->assertSame( 500, rest_get_server()->status ); + } + public function _validate_as_integer_123( $value, $request, $key ) { if ( ! is_int( $value ) ) { return new WP_Error( 'some-error', 'This is not valid!' ); From 045bec51a4f379ab5b75cdbe7ff18ccac3d38c95 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 31 Oct 2021 20:26:07 +0000 Subject: [PATCH 253/257] Docs: Add a `@since` note to `WP_REST_Post_Types_Controller::get_item_schema()` for the `supports` and `visibility` properties. The `taxonomies` and `rest_base` properties were also added after the method was initially introduced, but that happened during the same release cycle, so they don't need a separate `@since` note. Follow-up to [38832], [39097], [39191], [39647], [51959]. See #53399. git-svn-id: https://develop.svn.wordpress.org/trunk@51961 602fd350-edb4-49c9-b593-d223f7449a82 --- .../rest-api/endpoints/class-wp-rest-post-types-controller.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php index e2598ac89d01e..09b4e68bc04c8 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php @@ -268,6 +268,8 @@ public function prepare_item_for_response( $item, $request ) { * Retrieves the post type's schema, conforming to JSON Schema. * * @since 4.7.0 + * @since 4.8.0 The `supports` property was added. + * @since 5.9.0 The `visibility` property was added. * * @return array Item schema data. */ From bb6c5dbb8d613cb450d3f73983ecca27a75b003b Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Sun, 31 Oct 2021 23:15:10 +0000 Subject: [PATCH 254/257] REST API: Support custom namespaces for custom post types. While a custom post type can define a custom route by using the `rest_base` argument, a namespace of `wp/v2` was assumed. This commit introduces support for a `rest_namespace` argument. A new `rest_get_route_for_post_type_items` function has been introduced and the `rest_get_route_for_post` function updated to facilitate getting the correct route for custom post types. While the WordPress Core Block Editor bootstrap code has been updated to use these API functions, for maximum compatibility sticking with the default `wp/v2` namespace is recommended until the API functions see wider use. Props spacedmonkey, swissspidy. Fixes #53656. git-svn-id: https://develop.svn.wordpress.org/trunk@51962 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/edit-form-blocks.php | 10 ++-- src/wp-admin/widgets-form-blocks.php | 2 +- src/wp-includes/class-wp-post-type.php | 14 +++++ src/wp-includes/post.php | 1 + src/wp-includes/rest-api.php | 51 +++++++++++++------ .../rest-api/class-wp-rest-server.php | 2 +- .../class-wp-rest-autosaves-controller.php | 2 +- ...class-wp-rest-post-statuses-controller.php | 5 +- .../class-wp-rest-post-types-controller.php | 37 +++++++++----- .../class-wp-rest-posts-controller.php | 6 +-- .../class-wp-rest-revisions-controller.php | 2 +- .../class-wp-rest-templates-controller.php | 2 +- .../class-wp-rest-terms-controller.php | 7 ++- tests/phpunit/tests/rest-api.php | 45 +++++++++++++++- .../rest-api/rest-post-types-controller.php | 21 +++++++- tests/qunit/fixtures/wp-api-generated.js | 8 ++- 16 files changed, 165 insertions(+), 50 deletions(-) diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index 7e335fa49b9cb..969d138ecb99d 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -49,7 +49,7 @@ static function( $classes ) { wp_enqueue_script( 'heartbeat' ); wp_enqueue_script( 'wp-edit-post' ); -$rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; +$rest_path = rest_get_route_for_post( $post ); // Preload common data. $preload_paths = array( @@ -57,12 +57,12 @@ static function( $classes ) { '/wp/v2/types?context=edit', '/wp/v2/taxonomies?per_page=-1&context=edit', '/wp/v2/themes?status=active', - sprintf( '/wp/v2/%s/%s?context=edit', $rest_base, $post->ID ), + add_query_arg( 'context', 'edit', $rest_path ), sprintf( '/wp/v2/types/%s?context=edit', $post_type ), sprintf( '/wp/v2/users/me?post_type=%s&context=edit', $post_type ), - array( '/wp/v2/media', 'OPTIONS' ), - array( '/wp/v2/blocks', 'OPTIONS' ), - sprintf( '/wp/v2/%s/%d/autosaves?context=edit', $rest_base, $post->ID ), + array( rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ), + array( rest_get_route_for_post_type_items( 'wp_block' ), 'OPTIONS' ), + sprintf( '%s/autosaves?context=edit', $rest_path ), ); block_editor_rest_api_preload( $preload_paths, $block_editor_context ); diff --git a/src/wp-admin/widgets-form-blocks.php b/src/wp-admin/widgets-form-blocks.php index 95908e1e9e2d8..563bbf2a699d9 100644 --- a/src/wp-admin/widgets-form-blocks.php +++ b/src/wp-admin/widgets-form-blocks.php @@ -18,7 +18,7 @@ $block_editor_context = new WP_Block_Editor_Context(); $preload_paths = array( - array( '/wp/v2/media', 'OPTIONS' ), + array( rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ), '/wp/v2/sidebars?context=edit&per_page=-1', '/wp/v2/widgets?context=edit&per_page=-1&_embed=about', ); diff --git a/src/wp-includes/class-wp-post-type.php b/src/wp-includes/class-wp-post-type.php index 43e8a61514d57..e284994401dd1 100644 --- a/src/wp-includes/class-wp-post-type.php +++ b/src/wp-includes/class-wp-post-type.php @@ -358,6 +358,14 @@ final class WP_Post_Type { */ public $rest_base; + /** + * The namespace for this post type's REST API endpoints. + * + * @since 5.9 + * @var string|bool $rest_namespace + */ + public $rest_namespace; + /** * The controller for this post type's REST API endpoints. * @@ -452,6 +460,7 @@ public function set_props( $args ) { 'delete_with_user' => null, 'show_in_rest' => false, 'rest_base' => false, + 'rest_namespace' => false, 'rest_controller_class' => false, 'template' => array(), 'template_lock' => false, @@ -473,6 +482,11 @@ public function set_props( $args ) { $args['show_ui'] = $args['public']; } + // If not set, default rest_namespace to wp/v2 if show_in_rest is true. + if ( false === $args['rest_namespace'] && ! empty( $args['show_in_rest'] ) ) { + $args['rest_namespace'] = 'wp/v2'; + } + // If not set, default to the setting for 'show_ui'. if ( null === $args['show_in_menu'] || ! $args['show_ui'] ) { $args['show_in_menu'] = $args['show_ui']; diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index fb90f626f43b9..5cd73d1bd8197 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -1418,6 +1418,7 @@ function get_post_types( $args = array(), $output = 'names', $operator = 'and' ) * @type bool $show_in_rest Whether to include the post type in the REST API. Set this to true * for the post type to be available in the block editor. * @type string $rest_base To change the base URL of REST API route. Default is $post_type. + * @type string $rest_namespace To change the namespace URL of REST API route. Default is wp/v2. * @type string $rest_controller_class REST API controller class name. Default is 'WP_REST_Posts_Controller'. * @type int $menu_position The position in the menu order the post type should appear. To work, * $show_in_menu must be true. Default null (at the bottom). diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 3d89f16009196..a30a246d95ef6 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -3049,24 +3049,12 @@ function rest_get_route_for_post( $post ) { return ''; } - $post_type = get_post_type_object( $post->post_type ); - if ( ! $post_type ) { + $post_type_route = rest_get_route_for_post_type_items( $post->post_type ); + if ( ! $post_type_route ) { return ''; } - $controller = $post_type->get_rest_controller(); - if ( ! $controller ) { - return ''; - } - - $route = ''; - - // The only two controllers that we can detect are the Attachments and Posts controllers. - if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) { - $namespace = 'wp/v2'; - $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; - $route = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID ); - } + $route = sprintf( '%s/%d', $post_type_route, $post->ID ); /** * Filters the REST API route for a post. @@ -3079,6 +3067,39 @@ function rest_get_route_for_post( $post ) { return apply_filters( 'rest_route_for_post', $route, $post ); } +/** + * Gets the REST API route for a post type. + * + * @since 5.9.0 + * + * @param string $post_type The name of a registered post type. + * @return string The route path with a leading slash for the given post type, or an empty string if there is not a route. + */ +function rest_get_route_for_post_type_items( $post_type ) { + $post_type = get_post_type_object( $post_type ); + if ( ! $post_type ) { + return ''; + } + + if ( ! $post_type->show_in_rest ) { + return ''; + } + + $namespace = ! empty( $post_type->rest_namespace ) ? $post_type->rest_namespace : 'wp/v2'; + $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; + $route = sprintf( '/%s/%s', $namespace, $rest_base ); + + /** + * Filters the REST API route for a post type. + * + * @since 5.9.0 + * + * @param string $route The route path. + * @param WP_Post_Type $post_type The post type object. + */ + return apply_filters( 'rest_route_for_post_type_items', $route, $post_type ); +} + /** * Gets the REST API route for a term. * diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index e2999f7c8231f..3585aa7656b5e 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1288,7 +1288,7 @@ protected function add_site_logo_to_index( WP_REST_Response $response ) { if ( $site_logo_id ) { $response->add_link( 'https://api.w.org/featuredmedia', - rest_url( 'wp/v2/media/' . $site_logo_id ), + rest_url( rest_get_route_for_post( $site_logo_id ) ), array( 'embeddable' => true, ) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index 3cb3d7efe58e8..0a2cf5d49b4b1 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -67,8 +67,8 @@ public function __construct( $parent_post_type ) { $this->parent_controller = $parent_controller; $this->revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type ); - $this->namespace = 'wp/v2'; $this->rest_base = 'autosaves'; + $this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2'; $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php index 8ef94ea21a33e..4801d7943d576 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php @@ -263,10 +263,11 @@ public function prepare_item_for_response( $item, $request ) { $response = rest_ensure_response( $data ); + $rest_url = rest_url( rest_get_route_for_post_type_items( 'post' ) ); if ( 'publish' === $status->name ) { - $response->add_link( 'archives', rest_url( 'wp/v2/posts' ) ); + $response->add_link( 'archives', $rest_url ); } else { - $response->add_link( 'archives', add_query_arg( 'status', $status->name, rest_url( 'wp/v2/posts' ) ) ); + $response->add_link( 'archives', add_query_arg( 'status', $status->name, $rest_url ) ); } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php index 09b4e68bc04c8..088d238883cd0 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php @@ -180,6 +180,7 @@ public function prepare_item_for_response( $item, $request ) { $taxonomies = wp_list_filter( get_object_taxonomies( $post_type->name, 'objects' ), array( 'show_in_rest' => true ) ); $taxonomies = wp_list_pluck( $taxonomies, 'name' ); $base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; + $namespace = ! empty( $post_type->rest_namespace ) ? $post_type->rest_namespace : 'wp/v2'; $supports = get_all_post_type_supports( $post_type->name ); $fields = $this->get_fields_for_response( $request ); @@ -232,6 +233,10 @@ public function prepare_item_for_response( $item, $request ) { $data['rest_base'] = $base; } + if ( in_array( 'rest_namespace', $fields, true ) ) { + $data['rest_namespace'] = $namespace; + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); @@ -245,7 +250,7 @@ public function prepare_item_for_response( $item, $request ) { 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), 'https://api.w.org/items' => array( - 'href' => rest_url( sprintf( 'wp/v2/%s', $base ) ), + 'href' => rest_url( rest_get_route_for_post_type_items( $post_type->name ) ), ), ) ); @@ -269,7 +274,7 @@ public function prepare_item_for_response( $item, $request ) { * * @since 4.7.0 * @since 4.8.0 The `supports` property was added. - * @since 5.9.0 The `visibility` property was added. + * @since 5.9.0 The `visibility` and `rest_namespace` properties were added. * * @return array Item schema data. */ @@ -283,55 +288,55 @@ public function get_item_schema() { 'title' => 'type', 'type' => 'object', 'properties' => array( - 'capabilities' => array( + 'capabilities' => array( 'description' => __( 'All capabilities used by the post type.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), - 'description' => array( + 'description' => array( 'description' => __( 'A human-readable description of the post type.' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'hierarchical' => array( + 'hierarchical' => array( 'description' => __( 'Whether or not the post type should have children.' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'viewable' => array( + 'viewable' => array( 'description' => __( 'Whether or not the post type can be viewed.' ), 'type' => 'boolean', 'context' => array( 'edit' ), 'readonly' => true, ), - 'labels' => array( + 'labels' => array( 'description' => __( 'Human-readable labels for the post type for various contexts.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), - 'name' => array( + 'name' => array( 'description' => __( 'The title for the post type.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'slug' => array( + 'slug' => array( 'description' => __( 'An alphanumeric identifier for the post type.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'supports' => array( + 'supports' => array( 'description' => __( 'All features, supported by the post type.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), - 'taxonomies' => array( + 'taxonomies' => array( 'description' => __( 'Taxonomies associated with post type.' ), 'type' => 'array', 'items' => array( @@ -340,13 +345,19 @@ public function get_item_schema() { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'rest_base' => array( + 'rest_base' => array( 'description' => __( 'REST base route for the post type.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'visibility' => array( + 'rest_namespace' => array( + 'description' => __( 'REST route\'s namespace for the post type.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'visibility' => array( 'description' => __( 'The visibility settings for the post type.' ), 'type' => 'object', 'context' => array( 'edit' ), diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 068cc70bc2a9d..0cc548b0b6e4c 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -48,9 +48,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { */ public function __construct( $post_type ) { $this->post_type = $post_type; - $this->namespace = 'wp/v2'; $obj = get_post_type_object( $post_type ); $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; + $this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2'; $this->meta = new WP_REST_Post_Meta_Fields( $this->post_type ); } @@ -2037,7 +2037,7 @@ protected function prepare_links( $post ) { // If we have a featured media, add that. $featured_media = get_post_thumbnail_id( $post->ID ); if ( $featured_media ) { - $image_url = rest_url( 'wp/v2/media/' . $featured_media ); + $image_url = rest_url( rest_get_route_for_post( $featured_media ) ); $links['https://api.w.org/featuredmedia'] = array( 'href' => $image_url, @@ -2046,7 +2046,7 @@ protected function prepare_links( $post ) { } if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) { - $attachments_url = rest_url( 'wp/v2/media' ); + $attachments_url = rest_url( rest_get_route_for_post_type_items( 'attachment' ) ); $attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url ); $links['https://api.w.org/attachment'] = array( diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php index 5449812e1979a..bea3ddac789c6 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php @@ -49,10 +49,10 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { */ public function __construct( $parent_post_type ) { $this->parent_post_type = $parent_post_type; - $this->namespace = 'wp/v2'; $this->rest_base = 'revisions'; $post_type_object = get_post_type_object( $parent_post_type ); $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; + $this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2'; $this->parent_controller = $post_type_object->get_rest_controller(); if ( ! $this->parent_controller ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 1b4df794e81a1..e7712363f7f33 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -33,9 +33,9 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { */ public function __construct( $post_type ) { $this->post_type = $post_type; - $this->namespace = 'wp/v2'; $obj = get_post_type_object( $post_type ); $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; + $this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2'; } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php index 093b98a4911ad..183b7c7ffa45c 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php @@ -914,15 +914,14 @@ protected function prepare_links( $term ) { $post_type_links = array(); foreach ( $taxonomy_obj->object_type as $type ) { - $post_type_object = get_post_type_object( $type ); + $rest_path = rest_get_route_for_post_type_items( $type ); - if ( empty( $post_type_object->show_in_rest ) ) { + if ( empty( $rest_path ) ) { continue; } - $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; $post_type_links[] = array( - 'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( sprintf( 'wp/v2/%s', $rest_base ) ) ), + 'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( $rest_path ) ), ); } diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index 4ec818013bda6..5770f68841b42 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -1829,6 +1829,48 @@ public function test_rest_get_route_for_post_invalid_post_type() { $this->assertSame( '', rest_get_route_for_post( $post ) ); } + /** + * @ticket 53656 + */ + public function test_rest_get_route_for_post_custom_namespace() { + register_post_type( + 'cpt', + array( + 'show_in_rest' => true, + 'rest_base' => 'cpt', + 'rest_namespace' => 'wordpress/v1', + ) + ); + $post = self::factory()->post->create_and_get( array( 'post_type' => 'cpt' ) ); + + $this->assertSame( '/wordpress/v1/cpt/' . $post->ID, rest_get_route_for_post( $post ) ); + unregister_post_type( 'cpt' ); + } + + /** + * @ticket 53656 + */ + public function test_rest_get_route_for_post_type_items() { + $this->assertSame( '/wp/v2/posts', rest_get_route_for_post_type_items( 'post' ) ); + } + + /** + * @ticket 53656 + */ + public function test_rest_get_route_for_post_type_items_custom_namespace() { + register_post_type( + 'cpt', + array( + 'show_in_rest' => true, + 'rest_base' => 'cpt', + 'rest_namespace' => 'wordpress/v1', + ) + ); + + $this->assertSame( '/wordpress/v1/cpt', rest_get_route_for_post_type_items( 'cpt' ) ); + unregister_post_type( 'cpt' ); + } + /** * @ticket 49116 */ @@ -1839,10 +1881,11 @@ public function test_rest_get_route_for_post_non_rest() { /** * @ticket 49116 + * @ticket 53656 */ public function test_rest_get_route_for_post_custom_controller() { $post = self::factory()->post->create_and_get( array( 'post_type' => 'wp_block' ) ); - $this->assertSame( '', rest_get_route_for_post( $post ) ); + $this->assertSame( '/wp/v2/blocks/' . $post->ID, rest_get_route_for_post( $post ) ); } /** diff --git a/tests/phpunit/tests/rest-api/rest-post-types-controller.php b/tests/phpunit/tests/rest-api/rest-post-types-controller.php index d93f5814c19a7..22ff225a69a52 100644 --- a/tests/phpunit/tests/rest-api/rest-post-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-types-controller.php @@ -62,6 +62,23 @@ public function test_get_item() { $this->assertSame( array( 'category', 'post_tag' ), $data['taxonomies'] ); } + /** + * @ticket 53656 + */ + public function test_get_item_cpt() { + register_post_type( + 'cpt', + array( + 'show_in_rest' => true, + 'rest_base' => 'cpt', + 'rest_namespace' => 'wordpress/v1', + ) + ); + $request = new WP_REST_Request( 'GET', '/wp/v2/types/cpt' ); + $response = rest_get_server()->dispatch( $request ); + $this->check_post_type_object_response( 'view', $response, 'cpt' ); + } + public function test_get_item_page() { $request = new WP_REST_Request( 'GET', '/wp/v2/types/page' ); $response = rest_get_server()->dispatch( $request ); @@ -144,7 +161,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 11, $properties ); + $this->assertCount( 12, $properties ); $this->assertArrayHasKey( 'capabilities', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'hierarchical', $properties ); @@ -155,6 +172,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'supports', $properties ); $this->assertArrayHasKey( 'taxonomies', $properties ); $this->assertArrayHasKey( 'rest_base', $properties ); + $this->assertArrayHasKey( 'rest_namespace', $properties ); $this->assertArrayHasKey( 'visibility', $properties ); } @@ -204,6 +222,7 @@ protected function check_post_type_obj( $context, $post_type_obj, $data, $links $this->assertSame( $post_type_obj->description, $data['description'] ); $this->assertSame( $post_type_obj->hierarchical, $data['hierarchical'] ); $this->assertSame( $post_type_obj->rest_base, $data['rest_base'] ); + $this->assertSame( $post_type_obj->rest_namespace, $data['rest_namespace'] ); $links = test_rest_expand_compact_links( $links ); $this->assertSame( rest_url( 'wp/v2/types' ), $links['collection'][0]['href'] ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 0d5b2230e0b47..63b70dd2c8d60 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -8336,6 +8336,7 @@ mockedApiResponse.TypesCollection = { "post_tag" ], "rest_base": "posts", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8363,6 +8364,7 @@ mockedApiResponse.TypesCollection = { "slug": "page", "taxonomies": [], "rest_base": "pages", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8390,6 +8392,7 @@ mockedApiResponse.TypesCollection = { "slug": "attachment", "taxonomies": [], "rest_base": "media", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8417,6 +8420,7 @@ mockedApiResponse.TypesCollection = { "slug": "wp_block", "taxonomies": [], "rest_base": "blocks", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8444,6 +8448,7 @@ mockedApiResponse.TypesCollection = { "slug": "wp_template", "taxonomies": [], "rest_base": "templates", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8475,7 +8480,8 @@ mockedApiResponse.TypeModel = { "category", "post_tag" ], - "rest_base": "posts" + "rest_base": "posts", + "rest_namespace": "wp/v2" }; mockedApiResponse.StatusesCollection = { From 9ca3e8f36b07c41e9298c545135a451718f5d805 Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Mon, 1 Nov 2021 02:12:09 +0000 Subject: [PATCH 255/257] KSES: Add options for restricting tags based upon their attributes. This change adds two now attribute-related config options to KSES: - An array of allowed values can be defined for attributes. If the attribute value doesn't fall into the list, the attribute will be removed from the tag. - Attributes can be marked as required. If a required attribute is not present, KSES will remove all attributes from the tag. As KSES doesn't match opening and closing tags, it's not possible to safely remove the tag itself, the safest fallback is to strip all attributes from the tag, instead. Included with this change is an implementation of these options, allowing the `` tag to be stored in posts, but only when it has a `type` attribute set to `application/pdf`. Props pento, swissspidy, peterwilsoncc, dd32, jorbin. Fixes #54261. git-svn-id: https://develop.svn.wordpress.org/trunk@51963 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/kses.php | 50 ++++++ tests/phpunit/tests/kses.php | 294 +++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 0b24a1ba0c13f..3a7f99dd1a998 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -271,6 +271,13 @@ 'lang' => true, 'xml:lang' => true, ), + 'object' => array( + 'data' => true, + 'type' => array( + 'required' => true, + 'values' => array( 'application/pdf' ), + ), + ), 'p' => array( 'align' => true, 'dir' => true, @@ -1165,15 +1172,47 @@ function wp_kses_attr( $element, $attr, $allowed_html, $allowed_protocols ) { // Split it. $attrarr = wp_kses_hair( $attr, $allowed_protocols ); + // Check if there are attributes that are required. + $required_attrs = array_filter( + $allowed_html[ $element_low ], + function( $required_attr_limits ) { + return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required']; + } + ); + + // If a required attribute check fails, we can return nothing for a self-closing tag, + // but for a non-self-closing tag the best option is to return the element with attributes, + // as KSES doesn't handle matching the relevant closing tag. + $stripped_tag = ''; + if ( empty( $xhtml_slash ) ) { + $stripped_tag = "<$element>"; + } + // Go through $attrarr, and save the allowed attributes for this element // in $attr2. $attr2 = ''; foreach ( $attrarr as $arreach ) { + // Check if this attribute is required. + $required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] ); + if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) { $attr2 .= ' ' . $arreach['whole']; + + // If this was a required attribute, we can mark it as found. + if ( $required ) { + unset( $required_attrs[ strtolower( $arreach['name'] ) ] ); + } + } elseif ( $required ) { + // This attribute was required, but didn't pass the check. The entire tag is not allowed. + return $stripped_tag; } } + // If some required attributes weren't set, the entire tag is not allowed. + if ( ! empty( $required_attrs ) ) { + return $stripped_tag; + } + // Remove any "<" or ">" characters. $attr2 = preg_replace( '/[<>]/', '', $attr2 ); @@ -1600,6 +1639,17 @@ function wp_kses_check_attr_val( $value, $vless, $checkname, $checkvalue ) { $ok = false; } break; + + case 'values': + /* + * The values check is used when you want to make sure that the attribute + * has one of the given values. + */ + + if ( false === array_search( strtolower( $value ), $checkvalue, true ) ) { + $ok = false; + } + break; } // End switch. return $ok; diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 961ed71b1652d..11434d37e8ab4 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -1496,4 +1496,298 @@ function test_wp_kses_main_tag_standard_attributes() { $this->assertSame( $html, wp_kses_post( $html ) ); } + + /** + * Test that object tags are allowed under limited circumstances. + * + * @ticket 54261 + * + * @dataProvider data_wp_kses_object_tag_allowed + * + * @param string $html A string of HTML to test. + * @param string $expected The expected result from KSES. + */ + function test_wp_kses_object_tag_allowed( $html, $expected ) { + $this->assertSame( $expected, wp_kses_post( $html ) ); + } + + /** + * Data provider for test_wp_kses_object_tag_allowed(). + */ + function data_wp_kses_object_tag_allowed() { + return array( + 'valid value for type' => array( + '', + '', + ), + 'invalid value for type' => array( + '', + '', + ), + 'multiple type attributes, last invalid' => array( + '', + '', + ), + 'multiple type attributes, first uppercase, last invalid' => array( + '', + '', + ), + 'multiple type attributes, last upper case and invalid' => array( + '', + '', + ), + 'multiple type attributes, first invalid' => array( + '', + '', + ), + 'multiple type attributes, first upper case and invalid' => array( + '', + '', + ), + 'multiple type attributes, first invalid, last uppercase' => array( + '', + '', + ), + 'multiple object tags, last invalid' => array( + '', + '', + ), + 'multiple object tags, first invalid' => array( + '', + '', + ), + 'type attribute with partially incorrect value' => array( + '', + '', + ), + 'type attribute with empty value' => array( + '', + '', + ), + 'type attribute with no value' => array( + '', + '', + ), + 'no type attribute' => array( + '', + '', + ), + ); + } + + /** + * Test that object tags will continue to function if they've been added using the + * 'wp_kses_allowed_html' filter. + * + * @ticket 54261 + */ + function test_wp_kses_object_added_in_html_filter() { + $html = << + + + +HTML; + + add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ), 10, 2 ); + + $filtered_html = wp_kses_post( $html ); + + remove_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ) ); + + $this->assertSame( $html, $filtered_html ); + } + + function filter_wp_kses_object_added_in_html_filter( $tags, $context ) { + if ( 'post' === $context ) { + $tags['object'] = array( + 'type' => true, + 'data' => true, + ); + + $tags['param'] = array( + 'name' => true, + 'value' => true, + ); + } + + return $tags; + } + + /** + * Test that attributes with a list of allowed values are filtered correctly. + * + * @ticket 54261 + * + * @dataProvider data_wp_kses_allowed_values_list + * + * @param string $html A string of HTML to test. + * @param string $expected The expected result from KSES. + * @param array $allowed_html The allowed HTML to pass to KSES. + */ + function test_wp_kses_allowed_values_list( $html, $expected, $allowed_html ) { + $this->assertSame( $expected, wp_kses( $html, $allowed_html ) ); + } + + /** + * Data provider for test_wp_kses_allowed_values_list(). + */ + function data_wp_kses_allowed_values_list() { + $data = array( + 'valid dir attribute value' => array( + '

foo

', + '

foo

', + ), + 'valid dir attribute value, upper case' => array( + '

foo

', + '

foo

', + ), + 'invalid dir attribute value' => array( + '

foo

', + '

foo

', + ), + 'dir attribute with empty value' => array( + '

foo

', + '

foo

', + ), + 'dir attribute with no value' => array( + '

foo

', + '

foo

', + ), + ); + + return array_map( + function ( $datum ) { + $datum[] = array( + 'p' => array( + 'dir' => array( + 'values' => array( 'ltr', 'rtl' ), + ), + ), + ); + + return $datum; + }, + $data + ); + } + + /** + * Test that attributes with the required flag are handled correctly. + * + * @ticket 54261 + * + * @dataProvider data_wp_kses_required_attribute + * + * @param string $html A string of HTML to test. + * @param string $expected The expected result from KSES. + * @param array $allowed_html The allowed HTML to pass to KSES. + */ + function test_wp_kses_required_attribute( $html, $expected, $allowed_html ) { + $this->assertSame( $expected, wp_kses( $html, $allowed_html ) ); + } + + /** + * Data provider for test_wp_kses_required_attribute(). + */ + function data_wp_kses_required_attribute() { + $data = array( + 'valid dir attribute value' => array( + '

foo

', // Test HTML. + '

foo

', // Expected result when dir is not required. + '

foo

', // Expected result when dir is required. + '

foo

', // Expected result when dir is required, but has no value filter. + ), + 'valid dir attribute value, upper case' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + 'invalid dir attribute value' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + 'dir attribute with empty value' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + 'dir attribute with no value' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + 'dir attribute not set' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + ); + + $return_data = array(); + + foreach ( $data as $description => $datum ) { + // Test that the required flag defaults to false. + $return_data[ "$description - required flag not set" ] = array( + $datum[0], + $datum[1], + array( + 'p' => array( + 'dir' => array( + 'values' => array( 'ltr', 'rtl' ), + ), + ), + ), + ); + + // Test when the attribute is not required, but has allowed values. + $return_data[ "$description - required flag set to false" ] = array( + $datum[0], + $datum[1], + array( + 'p' => array( + 'dir' => array( + 'required' => false, + 'values' => array( 'ltr', 'rtl' ), + ), + ), + ), + ); + + // Test when the attribute is required, but has allowed values. + $return_data[ "$description - required flag set to true" ] = array( + $datum[0], + $datum[2], + array( + 'p' => array( + 'dir' => array( + 'required' => true, + 'values' => array( 'ltr', 'rtl' ), + ), + ), + ), + ); + + // Test when the attribute is required, but has no allowed values. + $return_data[ "$description - required flag set to true, no allowed values specified" ] = array( + $datum[0], + $datum[3], + array( + 'p' => array( + 'dir' => array( + 'required' => true, + ), + ), + ), + ); + } + + return $return_data; + } } From 2cd084aeb0af3506d34bff67e4ad805c72f31dd1 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Mon, 1 Nov 2021 03:26:06 +0000 Subject: [PATCH 256/257] REST API: Support custom namespaces for taxonomies. While a taxonomy can define a custom route by using the rest_base argument, a namespace of wp/v2 was assumed. This commit introduces support for a rest_namespace argument. A new rest_get_route_for_taxonomy_items function has been introduced and the rest_get_route_for_term function updated to facilitate getting the correct route for taxonomies. For maximum compatibility sticking with the default wp/v2 namespace is recommended until the API functions see wider use. Props spacedmonkey. Fixes #54267. See [51962]. git-svn-id: https://develop.svn.wordpress.org/trunk@51964 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-taxonomy.php | 14 +++++ src/wp-includes/rest-api.php | 51 +++++++++++++------ .../class-wp-rest-posts-controller.php | 9 ++-- .../class-wp-rest-taxonomies-controller.php | 32 ++++++++---- .../class-wp-rest-terms-controller.php | 2 +- src/wp-includes/taxonomy.php | 1 + tests/phpunit/tests/rest-api.php | 44 ++++++++++++++++ .../rest-api/rest-taxonomies-controller.php | 4 +- tests/qunit/fixtures/wp-api-generated.js | 5 +- 9 files changed, 127 insertions(+), 35 deletions(-) diff --git a/src/wp-includes/class-wp-taxonomy.php b/src/wp-includes/class-wp-taxonomy.php index 9db7403809306..21c954ad0571b 100644 --- a/src/wp-includes/class-wp-taxonomy.php +++ b/src/wp-includes/class-wp-taxonomy.php @@ -199,6 +199,14 @@ final class WP_Taxonomy { */ public $rest_base; + /** + * The namespace for this taxonomy's REST API endpoints. + * + * @since 5.9 + * @var string|bool $rest_namespace + */ + public $rest_namespace; + /** * The controller for this taxonomy's REST API endpoints. * @@ -319,6 +327,7 @@ public function set_props( $object_type, $args ) { 'update_count_callback' => '', 'show_in_rest' => false, 'rest_base' => false, + 'rest_namespace' => false, 'rest_controller_class' => false, 'default_term' => null, 'sort' => null, @@ -384,6 +393,11 @@ public function set_props( $object_type, $args ) { $args['show_in_quick_edit'] = $args['show_ui']; } + // If not set, default rest_namespace to wp/v2 if show_in_rest is true. + if ( false === $args['rest_namespace'] && ! empty( $args['show_in_rest'] ) ) { + $args['rest_namespace'] = 'wp/v2'; + } + $default_caps = array( 'manage_terms' => 'manage_categories', 'edit_terms' => 'manage_categories', diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index a30a246d95ef6..03860a915cef8 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -3115,24 +3115,12 @@ function rest_get_route_for_term( $term ) { return ''; } - $taxonomy = get_taxonomy( $term->taxonomy ); - if ( ! $taxonomy ) { - return ''; - } - - $controller = $taxonomy->get_rest_controller(); - if ( ! $controller ) { + $taxonomy_route = rest_get_route_for_taxonomy_items( $term->taxonomy ); + if ( ! $taxonomy_route ) { return ''; } - $route = ''; - - // The only controller that works is the Terms controller. - if ( $controller instanceof WP_REST_Terms_Controller ) { - $namespace = 'wp/v2'; - $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - $route = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id ); - } + $route = sprintf( '%s/%d', $taxonomy_route, $term->term_id ); /** * Filters the REST API route for a term. @@ -3145,6 +3133,39 @@ function rest_get_route_for_term( $term ) { return apply_filters( 'rest_route_for_term', $route, $term ); } +/** + * Gets the REST API route for a taxonomy. + * + * @since 5.9.0 + * + * @param string $taxonomy Name of taxonomy. + * @return string The route path with a leading slash for the given taxonomy. + */ +function rest_get_route_for_taxonomy_items( $taxonomy ) { + $taxonomy = get_taxonomy( $taxonomy ); + if ( ! $taxonomy ) { + return ''; + } + + if ( ! $taxonomy->show_in_rest ) { + return ''; + } + + $namespace = ! empty( $taxonomy->rest_namespace ) ? $taxonomy->rest_namespace : 'wp/v2'; + $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + $route = sprintf( '/%s/%s', $namespace, $rest_base ); + + /** + * Filters the REST API route for a taxonomy. + * + * @since 5.9.0 + * + * @param string $route The route path. + * @param WP_Taxonomy $taxonomy The taxonomy object. + */ + return apply_filters( 'rest_route_for_taxonomy_items', $route, $taxonomy ); +} + /** * Gets the REST route for the currently queried object. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 0cc548b0b6e4c..ffb4466892340 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -2060,19 +2060,16 @@ protected function prepare_links( $post ) { $links['https://api.w.org/term'] = array(); foreach ( $taxonomies as $tax ) { - $taxonomy_obj = get_taxonomy( $tax ); + $taxonomy_route = rest_get_route_for_taxonomy_items( $tax ); // Skip taxonomies that are not public. - if ( empty( $taxonomy_obj->show_in_rest ) ) { + if ( empty( $taxonomy_route ) ) { continue; } - - $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax; - $terms_url = add_query_arg( 'post', $post->ID, - rest_url( 'wp/v2/' . $tax_base ) + rest_url( $taxonomy_route ) ); $links['https://api.w.org/term'][] = array( diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php index 9d43ca2874b71..d05abcbcea2f0 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php @@ -250,6 +250,10 @@ public function prepare_item_for_response( $item, $request ) { $data['rest_base'] = $base; } + if ( in_array( 'rest_namespace', $fields, true ) ) { + $data['rest_namespace'] = $taxonomy->rest_namespace; + } + if ( in_array( 'visibility', $fields, true ) ) { $data['visibility'] = array( 'public' => (bool) $taxonomy->public, @@ -274,7 +278,7 @@ public function prepare_item_for_response( $item, $request ) { 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), 'https://api.w.org/items' => array( - 'href' => rest_url( sprintf( 'wp/v2/%s', $base ) ), + 'href' => rest_url( rest_get_route_for_taxonomy_items( $taxonomy->name ) ), ), ) ); @@ -310,49 +314,49 @@ public function get_item_schema() { 'title' => 'taxonomy', 'type' => 'object', 'properties' => array( - 'capabilities' => array( + 'capabilities' => array( 'description' => __( 'All capabilities used by the taxonomy.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), - 'description' => array( + 'description' => array( 'description' => __( 'A human-readable description of the taxonomy.' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'hierarchical' => array( + 'hierarchical' => array( 'description' => __( 'Whether or not the taxonomy should have children.' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'labels' => array( + 'labels' => array( 'description' => __( 'Human-readable labels for the taxonomy for various contexts.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), - 'name' => array( + 'name' => array( 'description' => __( 'The title for the taxonomy.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'slug' => array( + 'slug' => array( 'description' => __( 'An alphanumeric identifier for the taxonomy.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'show_cloud' => array( + 'show_cloud' => array( 'description' => __( 'Whether or not the term cloud should be displayed.' ), 'type' => 'boolean', 'context' => array( 'edit' ), 'readonly' => true, ), - 'types' => array( + 'types' => array( 'description' => __( 'Types associated with the taxonomy.' ), 'type' => 'array', 'items' => array( @@ -361,13 +365,19 @@ public function get_item_schema() { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'rest_base' => array( + 'rest_base' => array( 'description' => __( 'REST base route for the taxonomy.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'visibility' => array( + 'rest_namespace' => array( + 'description' => __( 'REST namespace route for the taxonomy.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'visibility' => array( 'description' => __( 'The visibility settings for the taxonomy.' ), 'type' => 'object', 'context' => array( 'edit' ), diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php index 183b7c7ffa45c..3a92b774f0fd2 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php @@ -57,9 +57,9 @@ class WP_REST_Terms_Controller extends WP_REST_Controller { */ public function __construct( $taxonomy ) { $this->taxonomy = $taxonomy; - $this->namespace = 'wp/v2'; $tax_obj = get_taxonomy( $taxonomy ); $this->rest_base = ! empty( $tax_obj->rest_base ) ? $tax_obj->rest_base : $tax_obj->name; + $this->namespace = ! empty( $tax_obj->rest_namespace ) ? $tax_obj->rest_namespace : 'wp/v2'; $this->meta = new WP_REST_Term_Meta_Fields( $taxonomy ); } diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index f81af0351ec4d..0273a4aac7ed3 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -387,6 +387,7 @@ function is_taxonomy_hierarchical( $taxonomy ) { * @type bool $show_in_rest Whether to include the taxonomy in the REST API. Set this to true * for the taxonomy to be available in the block editor. * @type string $rest_base To change the base url of REST API route. Default is $taxonomy. + * @type string $rest_namespace To change the namespace URL of REST API route. Default is wp/v2. * @type string $rest_controller_class REST API Controller class name. Default is 'WP_REST_Terms_Controller'. * @type bool $show_tagcloud Whether to list the taxonomy in the Tag Cloud Widget controls. If not set, * the default is inherited from `$show_ui` (default true). diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index 5770f68841b42..34b414903daf4 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -1962,6 +1962,50 @@ public function test_rest_get_route_for_term_id() { $this->assertSame( '/wp/v2/tags/' . $term->term_id, rest_get_route_for_term( $term->term_id ) ); } + /** + * @ticket 54267 + */ + public function test_rest_get_route_for_taxonomy_custom_namespace() { + register_taxonomy( + 'ct', + 'post', + array( + 'show_in_rest' => true, + 'rest_base' => 'ct', + 'rest_namespace' => 'wordpress/v1', + ) + ); + $term = self::factory()->term->create_and_get( array( 'taxonomy' => 'ct' ) ); + + $this->assertSame( '/wordpress/v1/ct/' . $term->term_id, rest_get_route_for_term( $term ) ); + unregister_taxonomy( 'ct' ); + } + + /** + * @ticket 54267 + */ + public function test_rest_get_route_for_taxonomy_items() { + $this->assertSame( '/wp/v2/categories', rest_get_route_for_taxonomy_items( 'category' ) ); + } + + /** + * @ticket 54267 + */ + public function test_rest_get_route_for_taxonomy_items_custom_namespace() { + register_taxonomy( + 'ct', + 'post', + array( + 'show_in_rest' => true, + 'rest_base' => 'ct', + 'rest_namespace' => 'wordpress/v1', + ) + ); + + $this->assertSame( '/wordpress/v1/ct', rest_get_route_for_taxonomy_items( 'ct' ) ); + unregister_post_type( 'ct' ); + } + /** * @ticket 50300 * diff --git a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php index 357e1c7e17607..17be251c1d352 100644 --- a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php +++ b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php @@ -219,7 +219,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 10, $properties ); + $this->assertCount( 11, $properties ); $this->assertArrayHasKey( 'capabilities', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'hierarchical', $properties ); @@ -230,6 +230,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'types', $properties ); $this->assertArrayHasKey( 'visibility', $properties ); $this->assertArrayHasKey( 'rest_base', $properties ); + $this->assertArrayHasKey( 'rest_namespace', $properties ); } /** @@ -252,6 +253,7 @@ protected function check_taxonomy_object( $context, $tax_obj, $data, $links ) { $this->assertSame( $tax_obj->description, $data['description'] ); $this->assertSame( $tax_obj->hierarchical, $data['hierarchical'] ); $this->assertSame( $tax_obj->rest_base, $data['rest_base'] ); + $this->assertSame( $tax_obj->rest_namespace, $data['rest_namespace'] ); $this->assertSame( rest_url( 'wp/v2/taxonomies' ), $links['collection'][0]['href'] ); $this->assertArrayHasKey( 'https://api.w.org/items', $links ); if ( 'edit' === $context ) { diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 63b70dd2c8d60..7852fd34aff37 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -8589,6 +8589,7 @@ mockedApiResponse.TaxonomiesCollection = { ], "hierarchical": true, "rest_base": "categories", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8618,6 +8619,7 @@ mockedApiResponse.TaxonomiesCollection = { ], "hierarchical": false, "rest_base": "tags", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8648,7 +8650,8 @@ mockedApiResponse.TaxonomyModel = { "post" ], "hierarchical": true, - "rest_base": "categories" + "rest_base": "categories", + "rest_namespace": "wp/v2" }; mockedApiResponse.CategoriesCollection = [ From 175ad4c91c68c1f3df89f4efcbf2c12a1b410cf0 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 1 Nov 2021 14:29:42 +0000 Subject: [PATCH 257/257] Docs: Add a `@since` note for the `rest_namespace` argument of `register_post_type()` and `register_taxonomy()`. Use 3-digit, x.x.x-style semantic versioning for `@since` tags of the `$rest_namespace` property in `WP_Post_Type` and `WP_Taxonomy`. Add a `@since` note to `WP_REST_Taxonomies_Controller::get_item_schema()` for the `visibility` and `rest_namespace` properties. The `rest_base` property was also added after the method was initially introduced, but that happened during the same release cycle, so it doesn't need a separate `@since` note. Follow-up to [38832], [39191], [42729], [51959], [51961], [51962], [51964]. See #53399. git-svn-id: https://develop.svn.wordpress.org/trunk@51965 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-post-type.php | 2 +- src/wp-includes/class-wp-taxonomy.php | 2 +- src/wp-includes/post.php | 1 + .../rest-api/endpoints/class-wp-rest-taxonomies-controller.php | 2 ++ src/wp-includes/taxonomy.php | 1 + 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-post-type.php b/src/wp-includes/class-wp-post-type.php index e284994401dd1..029bd26fe76b4 100644 --- a/src/wp-includes/class-wp-post-type.php +++ b/src/wp-includes/class-wp-post-type.php @@ -361,7 +361,7 @@ final class WP_Post_Type { /** * The namespace for this post type's REST API endpoints. * - * @since 5.9 + * @since 5.9.0 * @var string|bool $rest_namespace */ public $rest_namespace; diff --git a/src/wp-includes/class-wp-taxonomy.php b/src/wp-includes/class-wp-taxonomy.php index 21c954ad0571b..9ef1fdce4a6d3 100644 --- a/src/wp-includes/class-wp-taxonomy.php +++ b/src/wp-includes/class-wp-taxonomy.php @@ -202,7 +202,7 @@ final class WP_Taxonomy { /** * The namespace for this taxonomy's REST API endpoints. * - * @since 5.9 + * @since 5.9.0 * @var string|bool $rest_namespace */ public $rest_namespace; diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 5cd73d1bd8197..0f75bf537e246 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -1371,6 +1371,7 @@ function get_post_types( $args = array(), $output = 'names', $operator = 'and' ) * arguments to register the post type in REST API. * @since 5.0.0 The `template` and `template_lock` arguments were added. * @since 5.3.0 The `supports` argument will now accept an array of arguments for a feature. + * @since 5.9.0 The `rest_namespace` argument was added. * * @global array $wp_post_types List of post types. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php index d05abcbcea2f0..ede47585b10ea 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php @@ -301,6 +301,8 @@ public function prepare_item_for_response( $item, $request ) { * Retrieves the taxonomy's schema, conforming to JSON Schema. * * @since 4.7.0 + * @since 5.0.0 The `visibility` property was added. + * @since 5.9.0 The `rest_namespace` property was added. * * @return array Item schema data. */ diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 0273a4aac7ed3..5b1acb4fc6eeb 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -355,6 +355,7 @@ function is_taxonomy_hierarchical( $taxonomy ) { * @since 5.1.0 Introduced `meta_box_sanitize_cb` argument. * @since 5.4.0 Added the registered taxonomy object as a return value. * @since 5.5.0 Introduced `default_term` argument. + * @since 5.9.0 Introduced `rest_namespace` argument. * * @global WP_Taxonomy[] $wp_taxonomies Registered taxonomies. *