From ee22a186e251bc6591f4bd5fe2f287e23a957a39 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 29 Sep 2025 16:28:00 +0000 Subject: [PATCH 01/16] Code Modernization: Fix instances of using `null` as an array offset. Addresses a new [https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_using_values_null_as_an_array_offset_and_when_calling_array_key_exists deprecation in PHP 8.5]. Props swissspidy. Fixes #63957. git-svn-id: https://develop.svn.wordpress.org/trunk@60809 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-bindings.php | 5 +++-- src/wp-includes/blocks.php | 4 ++-- src/wp-includes/class-wp-block-type-registry.php | 6 +++--- src/wp-includes/class-wp-hook.php | 10 +++++++++- src/wp-includes/class-wp-theme-json.php | 2 +- src/wp-includes/functions.php | 4 ++-- .../data/html-api/token-counting-html-processor.php | 4 ++++ tests/phpunit/tests/functions/wpArrayGet.php | 13 ------------- tests/phpunit/tests/post/isPostStatusViewable.php | 1 - 9 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/wp-includes/block-bindings.php b/src/wp-includes/block-bindings.php index 0b5cee3dab412..23337398c2cbd 100644 --- a/src/wp-includes/block-bindings.php +++ b/src/wp-includes/block-bindings.php @@ -148,8 +148,9 @@ function get_block_bindings_supported_attributes( $block_type ) { ); $supported_block_attributes = - $block_bindings_supported_attributes[ $block_type ] ?? - array(); + isset( $block_type, $block_bindings_supported_attributes[ $block_type ] ) ? + $block_bindings_supported_attributes[ $block_type ] : + array(); /** * Filters the supported block attributes for block bindings. diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 8ce30c1524449..d8ea09d09049b 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -938,7 +938,7 @@ function get_hooked_blocks() { */ function insert_hooked_blocks( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { $anchor_block_type = $parsed_anchor_block['blockName']; - $hooked_block_types = isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) + $hooked_block_types = isset( $anchor_block_type, $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] : array(); @@ -1029,7 +1029,7 @@ function insert_hooked_blocks( &$parsed_anchor_block, $relative_position, $hooke */ function set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { $anchor_block_type = $parsed_anchor_block['blockName']; - $hooked_block_types = isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) + $hooked_block_types = isset( $anchor_block_type, $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] : array(); diff --git a/src/wp-includes/class-wp-block-type-registry.php b/src/wp-includes/class-wp-block-type-registry.php index 49e7bd60aee61..969f0f0f64a42 100644 --- a/src/wp-includes/class-wp-block-type-registry.php +++ b/src/wp-includes/class-wp-block-type-registry.php @@ -134,7 +134,7 @@ public function unregister( $name ) { * * @since 5.0.0 * - * @param string $name Block type name including namespace. + * @param string|null $name Block type name including namespace. * @return WP_Block_Type|null The registered block type, or null if it is not registered. */ public function get_registered( $name ) { @@ -161,11 +161,11 @@ public function get_all_registered() { * * @since 5.0.0 * - * @param string $name Block type name including namespace. + * @param string|null $name Block type name including namespace. * @return bool True if the block type is registered, false otherwise. */ public function is_registered( $name ) { - return isset( $this->registered_block_types[ $name ] ); + return isset( $name, $this->registered_block_types[ $name ] ); } public function __wakeup() { diff --git a/src/wp-includes/class-wp-hook.php b/src/wp-includes/class-wp-hook.php index d95691df6ad0b..f9f0f8826be94 100644 --- a/src/wp-includes/class-wp-hook.php +++ b/src/wp-includes/class-wp-hook.php @@ -80,6 +80,10 @@ final class WP_Hook implements Iterator, ArrayAccess { * @param int $accepted_args The number of arguments the function accepts. */ public function add_filter( $hook_name, $callback, $priority, $accepted_args ) { + if ( null === $priority ) { + $priority = 0; + } + $idx = _wp_filter_build_unique_id( $hook_name, $callback, $priority ); $priority_existed = isset( $this->callbacks[ $priority ] ); @@ -187,9 +191,13 @@ private function resort_active_iterations( $new_priority = false, $priority_exis * @return bool Whether the callback existed before it was removed. */ public function remove_filter( $hook_name, $callback, $priority ) { + if ( null === $priority ) { + $priority = 0; + } + $function_key = _wp_filter_build_unique_id( $hook_name, $callback, $priority ); - $exists = isset( $this->callbacks[ $priority ][ $function_key ] ); + $exists = isset( $function_key, $this->callbacks[ $priority ][ $function_key ] ); if ( $exists ) { unset( $this->callbacks[ $priority ][ $function_key ] ); diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 588aeaa89e38e..0458a2be11b7b 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -2889,7 +2889,7 @@ static function ( $split_selector ) use ( $clean_style_variation_selector ) { $element_pseudo_allowed = array(); - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { + if ( isset( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ]; } diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index e30ac7ff2570e..c420ebc232f1c 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5101,7 +5101,7 @@ function _wp_array_get( $input_array, $path, $default_value = null ) { * We check with `isset()` first, as it is a lot faster * than `array_key_exists()`. */ - if ( isset( $input_array[ $path_element ] ) ) { + if ( isset( $path_element, $input_array[ $path_element ] ) ) { $input_array = $input_array[ $path_element ]; continue; } @@ -5110,7 +5110,7 @@ function _wp_array_get( $input_array, $path, $default_value = null ) { * If `isset()` returns false, we check with `array_key_exists()`, * which also checks for `null` values. */ - if ( array_key_exists( $path_element, $input_array ) ) { + if ( isset( $path_element ) && array_key_exists( $path_element, $input_array ) ) { $input_array = $input_array[ $path_element ]; continue; } diff --git a/tests/phpunit/data/html-api/token-counting-html-processor.php b/tests/phpunit/data/html-api/token-counting-html-processor.php index b511acaf8bacb..854e2bd9d2927 100644 --- a/tests/phpunit/data/html-api/token-counting-html-processor.php +++ b/tests/phpunit/data/html-api/token-counting-html-processor.php @@ -23,6 +23,10 @@ public function next_token(): bool { $token_name = $this->get_token_name(); } + if ( null === $token_name ) { + $token_name = ''; + } + if ( ! isset( $this->token_seen_count[ $token_name ] ) ) { $this->token_seen_count[ $token_name ] = 1; } else { diff --git a/tests/phpunit/tests/functions/wpArrayGet.php b/tests/phpunit/tests/functions/wpArrayGet.php index 0c12cfba9b962..1035a5f200f4a 100644 --- a/tests/phpunit/tests/functions/wpArrayGet.php +++ b/tests/phpunit/tests/functions/wpArrayGet.php @@ -238,19 +238,6 @@ public function test_wp_array_get_null() { ), true ); - - $this->assertSame( - _wp_array_get( - array( - 'key' => array( - null => 4, - ), - ), - array( 'key', null ), - true - ), - 4 - ); } /** diff --git a/tests/phpunit/tests/post/isPostStatusViewable.php b/tests/phpunit/tests/post/isPostStatusViewable.php index d6933d18de5a4..398a2026cb13d 100644 --- a/tests/phpunit/tests/post/isPostStatusViewable.php +++ b/tests/phpunit/tests/post/isPostStatusViewable.php @@ -140,7 +140,6 @@ public function data_built_unregistered_in_status_types() { array( false, false ), array( true, false ), array( 20, false ), - array( null, false ), array( '', false ), ); } From 6db003c2c3c7f87b4977f4501a70ce2ba8510ddc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 29 Sep 2025 17:58:54 +0000 Subject: [PATCH 02/16] Media: Fix HEIF mime type assertion in test for PHP 8.5. In PHP 8.5, the test image's mime type is now detected as `image/heif`, not `image/heic`. Props swissspidy, adamsilverstein. Fixes #64050. git-svn-id: https://develop.svn.wordpress.org/trunk@60810 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/functions.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php index 2c458d968c531..3294d15480034 100644 --- a/tests/phpunit/tests/functions.php +++ b/tests/phpunit/tests/functions.php @@ -1284,7 +1284,11 @@ public function test_wp_get_image_mime( $file, $expected ) { $this->markTestSkipped( 'The exif PHP extension is not loaded.' ); } - $this->assertSame( $expected, wp_get_image_mime( $file ) ); + if ( is_array( $expected ) ) { + $this->assertContains( wp_get_image_mime( $file ), $expected ); + } else { + $this->assertSame( $expected, wp_get_image_mime( $file ) ); + } } /** @@ -1360,7 +1364,8 @@ public function data_wp_get_image_mime() { // HEIC. array( DIR_TESTDATA . '/images/test-image.heic', - 'image/heic', + // In PHP 8.5, it returns 'image/heif'. Before that, it returns 'image/heic'. + array( 'image/heic', 'image/heif' ), ), ); From c6cfbe9882200abc083d34143f7c2d6bbd80f161 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 29 Sep 2025 18:39:54 +0000 Subject: [PATCH 03/16] Code Modernization: Avoid NAN coercion/cast deprecation warnings. Coercing `INF` or `NAN` to a string or casting to int is deprecated in PHP 8.5+. This change addresses two occurrences in core where this was happening. Props swissspidy. Fixes #64047. See #63061. git-svn-id: https://develop.svn.wordpress.org/trunk@60811 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/deprecated.php | 2 +- tests/phpunit/tests/option/wpPrimeOptionCaches.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index 97183f86479a5..7b5abfe52bff3 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -3371,7 +3371,7 @@ function wp_convert_bytes_to_hr( $bytes ) { $units = array( 0 => 'B', 1 => 'KB', 2 => 'MB', 3 => 'GB', 4 => 'TB' ); $log = log( $bytes, KB_IN_BYTES ); - $power = (int) $log; + $power = ! is_nan( $log ) && ! is_infinite( $log ) ? (int) $log : 0; $size = KB_IN_BYTES ** ( $log - $power ); if ( ! is_nan( $size ) && array_key_exists( $power, $units ) ) { diff --git a/tests/phpunit/tests/option/wpPrimeOptionCaches.php b/tests/phpunit/tests/option/wpPrimeOptionCaches.php index 8b75bc286c4f5..3e3a148f3e202 100644 --- a/tests/phpunit/tests/option/wpPrimeOptionCaches.php +++ b/tests/phpunit/tests/option/wpPrimeOptionCaches.php @@ -455,7 +455,6 @@ public function data_option_types() { 'empty object' => array( new stdClass() ), 'populated object' => array( (object) array( 'string' ) ), 'INF' => array( INF ), - 'NAN' => array( NAN ), ); } } From 6b6d2d1975cf72ace0f37ba440d380e16bdbb0d2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 29 Sep 2025 19:11:01 +0000 Subject: [PATCH 04/16] External Libraries: Backport upstream PHP 8.5 fixes for getID3. In absence of a new release, this cherry-picks [https://github.com/JamesHeinrich/getID3/commit/8cb292333f60be1c0a2ba65acbfdb5f06f174536 8cb29233] and [https://github.com/JamesHeinrich/getID3/commit/dbda40de6dae134a1381452a1e78ea9d02d895c2 dbda40de]. Props swissspidy, vidugupta, TobiasBg, desrosj. Fixes #64051. See #63061. git-svn-id: https://develop.svn.wordpress.org/trunk@60812 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/ID3/getid3.lib.php | 2 +- src/wp-includes/ID3/getid3.php | 6 +++--- src/wp-includes/ID3/module.audio.ogg.php | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/ID3/getid3.lib.php b/src/wp-includes/ID3/getid3.lib.php index aec550b30d3cf..e8d5433672315 100644 --- a/src/wp-includes/ID3/getid3.lib.php +++ b/src/wp-includes/ID3/getid3.lib.php @@ -1788,7 +1788,7 @@ public static function getFileSizeSyscall($path) { $commandline = 'ls -l '.escapeshellarg($path).' | awk \'{print $5}\''; } if (isset($commandline)) { - $output = trim(`$commandline`); + $output = trim(shell_exec($commandline)); if (ctype_digit($output)) { $filesize = (float) $output; } diff --git a/src/wp-includes/ID3/getid3.php b/src/wp-includes/ID3/getid3.php index 41ef6408b2980..13461c6910397 100644 --- a/src/wp-includes/ID3/getid3.php +++ b/src/wp-includes/ID3/getid3.php @@ -481,7 +481,7 @@ public function __construct() { if (strpos($value, ' ') !== false) { if (!empty($path_so_far)) { $commandline = 'dir /x '.escapeshellarg(implode(DIRECTORY_SEPARATOR, $path_so_far)); - $dir_listing = `$commandline`; + $dir_listing = shell_exec($commandline); $lines = explode("\n", $dir_listing); foreach ($lines as $line) { $line = trim($line); @@ -1809,7 +1809,7 @@ public function getHashdata($algorithm) { if (file_exists(GETID3_HELPERAPPSDIR.'vorbiscomment.exe')) { $commandline = '"'.GETID3_HELPERAPPSDIR.'vorbiscomment.exe" -w -c "'.$empty.'" "'.$file.'" "'.$temp.'"'; - $VorbisCommentError = `$commandline`; + $VorbisCommentError = shell_exec($commandline); } else { @@ -1820,7 +1820,7 @@ public function getHashdata($algorithm) { } else { $commandline = 'vorbiscomment -w -c '.escapeshellarg($empty).' '.escapeshellarg($file).' '.escapeshellarg($temp).' 2>&1'; - $VorbisCommentError = `$commandline`; + $VorbisCommentError = shell_exec($commandline); } diff --git a/src/wp-includes/ID3/module.audio.ogg.php b/src/wp-includes/ID3/module.audio.ogg.php index ebd2b946c552a..bda166a8a90b6 100644 --- a/src/wp-includes/ID3/module.audio.ogg.php +++ b/src/wp-includes/ID3/module.audio.ogg.php @@ -787,29 +787,29 @@ public function ParseVorbisComments() { switch ($index) { case 'rg_audiophile': case 'replaygain_album_gain': - $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0]; + $info['replay_gain']['album']['adjustment'] = (float) $commentvalue[0]; unset($info['ogg']['comments'][$index]); break; case 'rg_radio': case 'replaygain_track_gain': - $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0]; + $info['replay_gain']['track']['adjustment'] = (float) $commentvalue[0]; unset($info['ogg']['comments'][$index]); break; case 'replaygain_album_peak': - $info['replay_gain']['album']['peak'] = (double) $commentvalue[0]; + $info['replay_gain']['album']['peak'] = (float) $commentvalue[0]; unset($info['ogg']['comments'][$index]); break; case 'rg_peak': case 'replaygain_track_peak': - $info['replay_gain']['track']['peak'] = (double) $commentvalue[0]; + $info['replay_gain']['track']['peak'] = (float) $commentvalue[0]; unset($info['ogg']['comments'][$index]); break; case 'replaygain_reference_loudness': - $info['replay_gain']['reference_volume'] = (double) $commentvalue[0]; + $info['replay_gain']['reference_volume'] = (float) $commentvalue[0]; unset($info['ogg']['comments'][$index]); break; From 90919a2f7642471b1274542d7af7dff780f6a11a Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 30 Sep 2025 12:46:53 +0000 Subject: [PATCH 05/16] External Libraries: Upgrade PHPMailer to version 6.11.0. This is a large maintenance release with address parser improvements. References: * [https://github.com/PHPMailer/PHPMailer/releases/tag/v6.11.0 PHPMailer 6.11.0 release notes] * [https://github.com/PHPMailer/PHPMailer/compare/v6.10.0...v6.11.0 Full list of changes in PHPMailer 6.11.0] Follow-up to [54937], [55557], [56484], [57137], [59246], [59481], [60623]. Props SirLouen, jrf, mukesh27, SergeyBiryukov. Fixes #64052. git-svn-id: https://develop.svn.wordpress.org/trunk@60813 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/PHPMailer/PHPMailer.php | 378 +++++++++++++++--------- src/wp-includes/PHPMailer/POP3.php | 2 +- src/wp-includes/PHPMailer/SMTP.php | 53 +++- src/wp-includes/class-wp-phpmailer.php | 12 +- 4 files changed, 295 insertions(+), 150 deletions(-) diff --git a/src/wp-includes/PHPMailer/PHPMailer.php b/src/wp-includes/PHPMailer/PHPMailer.php index 2444bcf3518a1..865f3a4e140f4 100644 --- a/src/wp-includes/PHPMailer/PHPMailer.php +++ b/src/wp-includes/PHPMailer/PHPMailer.php @@ -561,9 +561,9 @@ class PHPMailer * string $body the email body * string $from email address of sender * string $extra extra information of possible use - * "smtp_transaction_id' => last smtp transaction id + * 'smtp_transaction_id' => last smtp transaction id * - * @var string + * @var callable|callable-string */ public $action_function = ''; @@ -711,7 +711,7 @@ class PHPMailer * * @var array */ - protected $language = []; + protected static $language = []; /** * The number of errors encountered. @@ -768,7 +768,7 @@ class PHPMailer * * @var string */ - const VERSION = '6.10.0'; + const VERSION = '6.11.0'; /** * Error severity: message only, continue processing. @@ -1102,7 +1102,7 @@ protected function addOrEnqueueAnAddress($kind, $address, $name) //At-sign is missing. $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $kind, $address ); @@ -1187,7 +1187,7 @@ protected function addAnAddress($kind, $address, $name = '') if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { $error_message = sprintf( '%s: %s', - $this->lang('Invalid recipient kind'), + self::lang('Invalid recipient kind'), $kind ); $this->setError($error_message); @@ -1201,7 +1201,7 @@ protected function addAnAddress($kind, $address, $name = '') if (!static::validateAddress($address)) { $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $kind, $address ); @@ -1220,12 +1220,16 @@ protected function addAnAddress($kind, $address, $name = '') return true; } - } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) { - $this->ReplyTo[strtolower($address)] = [$address, $name]; + } else { + foreach ($this->ReplyTo as $replyTo) { + if (0 === strcasecmp($replyTo[0], $address)) { + return false; + } + } + $this->ReplyTo[] = [$address, $name]; return true; } - return false; } @@ -1237,16 +1241,19 @@ protected function addAnAddress($kind, $address, $name = '') * * @see https://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation * - * @param string $addrstr The address list string - * @param bool $useimap Whether to use the IMAP extension to parse the list - * @param string $charset The charset to use when decoding the address list string. + * @param string $addrstr The address list string + * @param string|null $deprecatedArg Deprecated argument since 6.11.0. + * @param string $charset The charset to use when decoding the address list string. * * @return array */ - public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591) + public static function parseAddresses($addrstr, $deprecatedArg = null, $charset = self::CHARSET_ISO88591) { + if ($deprecatedArg !== null) { + trigger_error(self::lang('deprecated_argument'), E_USER_DEPRECATED); + } $addresses = []; - if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { + if (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. @@ -1256,20 +1263,13 @@ public static function parseAddresses($addrstr, $useimap = true, $charset = self '.SYNTAX-ERROR.' !== $address->host && static::validateAddress($address->mailbox . '@' . $address->host) ) { - //Decode the name part if it's present and encoded + //Decode the name part if it's present and maybe encoded if ( - property_exists($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) + property_exists($address, 'personal') + && is_string($address->personal) + && $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); + $address->personal = static::decodeHeader($address->personal, $charset); } $addresses[] = [ @@ -1280,40 +1280,51 @@ public static function parseAddresses($addrstr, $useimap = true, $charset = self } } else { //Use this simpler parser - $list = explode(',', $addrstr); - foreach ($list as $address) { - $address = trim($address); - //Is there a separate name part? - if (strpos($address, '<') === false) { - //No separate name, just use the whole thing - if (static::validateAddress($address)) { - $addresses[] = [ - 'name' => '', - 'address' => $address, - ]; - } - } else { - list($name, $email) = explode('<', $address); - $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 (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 - 'name' => trim($name, '\'" '), - 'address' => $email, - ]; - } + $addresses = static::parseSimplerAddresses($addrstr, $charset); + } + + return $addresses; + } + + /** + * Parse a string containing one or more RFC822-style comma-separated email addresses + * with the form "display name
" into an array of name/address pairs. + * Uses a simpler parser that does not require the IMAP extension but doesnt support + * the full RFC822 spec. For full RFC822 support, use the PHP IMAP extension. + * + * @param string $addrstr The address list string + * @param string $charset The charset to use when decoding the address list string. + * + * @return array + */ + protected static function parseSimplerAddresses($addrstr, $charset) + { + // Emit a runtime notice to recommend using the IMAP extension for full RFC822 parsing + trigger_error(self::lang('imap_recommended'), E_USER_NOTICE); + + $addresses = []; + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + $parsed = static::parseEmailString($address); + $email = $parsed['email']; + if (static::validateAddress($email)) { + $name = static::decodeHeader($parsed['name'], $charset); + $addresses[] = [ + //Remove any surrounding quotes and spaces from the name + 'name' => trim($name, '\'" '), + 'address' => $email, + ]; } } } @@ -1321,6 +1332,42 @@ public static function parseAddresses($addrstr, $useimap = true, $charset = self return $addresses; } + /** + * Parse a string containing an email address with an optional name + * and divide it into a name and email address. + * + * @param string $input The email with name. + * + * @return array{name: string, email: string} + */ + private static function parseEmailString($input) + { + $input = trim((string)$input); + + if ($input === '') { + return ['name' => '', 'email' => '']; + } + + $pattern = '/^\s*(?:(?:"([^"]*)"|\'([^\']*)\'|([^<]*?))\s*)?<\s*([^>]+)\s*>\s*$/'; + if (preg_match($pattern, $input, $matches)) { + $name = ''; + // Double quotes including special scenarios. + if (isset($matches[1]) && $matches[1] !== '') { + $name = $matches[1]; + // Single quotes including special scenarios. + } elseif (isset($matches[2]) && $matches[2] !== '') { + $name = $matches[2]; + // Simplest scenario, name and email are in the format "Name ". + } elseif (isset($matches[3])) { + $name = trim($matches[3]); + } + + return ['name' => $name, 'email' => trim($matches[4])]; + } + + return ['name' => '', 'email' => $input]; + } + /** * Set the From and FromName properties. * @@ -1334,6 +1381,10 @@ public static function parseAddresses($addrstr, $useimap = true, $charset = self */ public function setFrom($address, $name = '', $auto = true) { + if (is_null($name)) { + //Helps avoid a deprecation warning in the preg_replace() below + $name = ''; + } $address = trim((string)$address); $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim //Don't validate now addresses with IDN. Will be done in send(). @@ -1345,7 +1396,7 @@ public function setFrom($address, $name = '', $auto = true) ) { $error_message = sprintf( '%s (From): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $address ); $this->setError($error_message); @@ -1601,7 +1652,7 @@ public function preSend() && ini_get('mail.add_x_header') === '1' && stripos(PHP_OS, 'WIN') === 0 ) { - trigger_error($this->lang('buggy_php'), E_USER_WARNING); + trigger_error(self::lang('buggy_php'), E_USER_WARNING); } try { @@ -1631,7 +1682,7 @@ public function preSend() call_user_func_array([$this, 'addAnAddress'], $params); } if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { - throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); + throw new Exception(self::lang('provide_address'), self::STOP_CRITICAL); } //Validate From, Sender, and ConfirmReadingTo addresses @@ -1648,7 +1699,7 @@ public function preSend() if (!static::validateAddress($this->{$address_kind})) { $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $address_kind, $this->{$address_kind} ); @@ -1670,7 +1721,7 @@ public function preSend() $this->setMessageType(); //Refuse to send an empty message unless we are specifically allowing it if (!$this->AllowEmpty && empty($this->Body)) { - throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + throw new Exception(self::lang('empty_message'), self::STOP_CRITICAL); } //Trim subject consistently @@ -1809,8 +1860,10 @@ protected function sendmailSend($header, $body) } else { $sendmailFmt = '%s -oi -f%s -t'; } + } elseif ($this->Mailer === 'qmail') { + $sendmailFmt = '%s'; } else { - //allow sendmail to choose a default envelope sender. It may + //Allow sendmail to choose a default envelope sender. It may //seem preferable to force it to use the From header as with //SMTP, but that introduces new problems (see //), and @@ -1828,33 +1881,35 @@ protected function sendmailSend($header, $body) foreach ($this->SingleToArray as $toAddr) { $mail = @popen($sendmail, 'w'); if (!$mail) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } $this->edebug("To: {$toAddr}"); fwrite($mail, 'To: ' . $toAddr . "\n"); fwrite($mail, $header); fwrite($mail, $body); $result = pclose($mail); - $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); - $this->doCallback( - ($result === 0), - [[$addrinfo['address'], $addrinfo['name']]], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); + $addrinfo = static::parseAddresses($toAddr, null, $this->CharSet); + foreach ($addrinfo as $addr) { + $this->doCallback( + ($result === 0), + [[$addr['address'], $addr['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + } $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } } else { $mail = @popen($sendmail, 'w'); if (!$mail) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } fwrite($mail, $header); fwrite($mail, $body); @@ -1871,7 +1926,7 @@ protected function sendmailSend($header, $body) ); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } @@ -2010,17 +2065,19 @@ 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, true, $this->CharSet); - $this->doCallback( - $result, - [[$addrinfo['address'], $addrinfo['name']]], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); + $addrinfo = static::parseAddresses($toAddr, null, $this->CharSet); + foreach ($addrinfo as $addr) { + $this->doCallback( + $result, + [[$addr['address'], $addr['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + } } } else { $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); @@ -2030,7 +2087,7 @@ protected function mailSend($header, $body) ini_set('sendmail_from', $old_from); } if (!$result) { - throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); + throw new Exception(self::lang('instantiate'), self::STOP_CRITICAL); } return true; @@ -2116,12 +2173,12 @@ protected function smtpSend($header, $body) $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $bad_rcpt = []; if (!$this->smtpConnect($this->SMTPOptions)) { - throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + throw new Exception(self::lang('smtp_connect_failed'), self::STOP_CRITICAL); } //If we have recipient addresses that need Unicode support, //but the server doesn't support it, stop here if ($this->UseSMTPUTF8 && !$this->smtp->getServerExt('SMTPUTF8')) { - throw new Exception($this->lang('no_smtputf8'), self::STOP_CRITICAL); + throw new Exception(self::lang('no_smtputf8'), self::STOP_CRITICAL); } //Sender already validated in preSend() if ('' === $this->Sender) { @@ -2133,7 +2190,7 @@ protected function smtpSend($header, $body) $this->smtp->xclient($this->SMTPXClient); } if (!$this->smtp->mail($smtp_from)) { - $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + $this->setError(self::lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); } @@ -2155,7 +2212,7 @@ protected function smtpSend($header, $body) //Only send the DATA command if we have viable recipients if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) { - throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); + throw new Exception(self::lang('data_not_accepted'), self::STOP_CRITICAL); } $smtp_transaction_id = $this->smtp->getLastTransactionID(); @@ -2186,7 +2243,7 @@ protected function smtpSend($header, $body) foreach ($bad_rcpt as $bad) { $errstr .= $bad['to'] . ': ' . $bad['error']; } - throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE); + throw new Exception(self::lang('recipients_failed') . $errstr, self::STOP_CONTINUE); } return true; @@ -2240,7 +2297,7 @@ public function smtpConnect($options = null) $hostinfo ) ) { - $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry)); + $this->edebug(self::lang('invalid_hostentry') . ' ' . trim($hostentry)); //Not a valid host entry continue; } @@ -2252,7 +2309,7 @@ public function smtpConnect($options = null) //Check the host name is a valid name or IP address before trying to use it if (!static::isValidHost($hostinfo[2])) { - $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); + $this->edebug(self::lang('invalid_host') . ' ' . $hostinfo[2]); continue; } $prefix = ''; @@ -2272,7 +2329,7 @@ public function smtpConnect($options = null) if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled if (!$sslext) { - throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + throw new Exception(self::lang('extension_missing') . 'openssl', self::STOP_CRITICAL); } } $host = $hostinfo[2]; @@ -2324,7 +2381,7 @@ public function smtpConnect($options = null) $this->oauth ) ) { - throw new Exception($this->lang('authenticate')); + throw new Exception(self::lang('authenticate')); } return true; @@ -2374,7 +2431,7 @@ public function smtpClose() * * @return bool Returns true if the requested language was loaded, false otherwise. */ - public function setLanguage($langcode = 'en', $lang_path = '') + public static function setLanguage($langcode = 'en', $lang_path = '') { //Backwards compatibility for renamed language codes $renamed_langcodes = [ @@ -2423,6 +2480,9 @@ public function setLanguage($langcode = 'en', $lang_path = '') 'smtp_error' => 'SMTP server error: ', 'variable_set' => 'Cannot set or reset variable: ', 'no_smtputf8' => 'Server does not support SMTPUTF8 needed to send to Unicode addresses', + 'imap_recommended' => 'Using simplified address parser is not recommended. ' . + 'Install the PHP IMAP extension for full RFC822 parsing.', + 'deprecated_argument' => 'Argument $deprecatedArg is deprecated', ]; if (empty($lang_path)) { //Calculate an absolute path so it can work if CWD is not here @@ -2489,7 +2549,7 @@ public function setLanguage($langcode = 'en', $lang_path = '') } } } - $this->language = $PHPMAILER_LANG; + self::$language = $PHPMAILER_LANG; return $foundlang; //Returns false if language not found } @@ -2501,11 +2561,11 @@ public function setLanguage($langcode = 'en', $lang_path = '') */ public function getTranslations() { - if (empty($this->language)) { - $this->setLanguage(); // Set the default language. + if (empty(self::$language)) { + self::setLanguage(); // Set the default language. } - return $this->language; + return self::$language; } /** @@ -2928,10 +2988,6 @@ public function createBody() //Create unique IDs and preset boundaries $this->setBoundaries(); - if ($this->sign_key_file) { - $body .= $this->getMailMIME() . static::$LE; - } - $this->setWordWrap(); $bodyEncoding = $this->Encoding; @@ -2963,6 +3019,12 @@ public function createBody() if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) { $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; } + + if ($this->sign_key_file) { + $this->Encoding = $bodyEncoding; + $body .= $this->getMailMIME() . static::$LE; + } + //Use this as a preamble in all multipart message types $mimepre = ''; switch ($this->message_type) { @@ -3144,12 +3206,12 @@ public function createBody() if ($this->isError()) { $body = ''; if ($this->exceptions) { - throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + throw new Exception(self::lang('empty_message'), self::STOP_CRITICAL); } } elseif ($this->sign_key_file) { try { if (!defined('PKCS7_TEXT')) { - throw new Exception($this->lang('extension_missing') . 'openssl'); + throw new Exception(self::lang('extension_missing') . 'openssl'); } $file = tempnam(sys_get_temp_dir(), 'srcsign'); @@ -3187,7 +3249,7 @@ public function createBody() $body = $parts[1]; } else { @unlink($signed); - throw new Exception($this->lang('signing') . openssl_error_string()); + throw new Exception(self::lang('signing') . openssl_error_string()); } } catch (Exception $exc) { $body = ''; @@ -3332,7 +3394,7 @@ public function addAttachment( ) { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name @@ -3345,7 +3407,7 @@ public function addAttachment( $name = $filename; } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } $this->attachment[] = [ @@ -3506,11 +3568,11 @@ protected function encodeFile($path, $encoding = self::ENCODING_BASE64) { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = file_get_contents($path); if (false === $file_buffer) { - throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = $this->encodeString($file_buffer, $encoding); @@ -3563,9 +3625,9 @@ public function encodeString($str, $encoding = self::ENCODING_BASE64) $encoded = $this->encodeQP($str); break; default: - $this->setError($this->lang('encoding') . $encoding); + $this->setError(self::lang('encoding') . $encoding); if ($this->exceptions) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } break; } @@ -3671,6 +3733,42 @@ public function encodeHeader($str, $position = 'text') return trim(static::normalizeBreaks($encoded)); } + /** + * Decode an RFC2047-encoded header value + * Attempts multiple strategies so it works even when the mbstring extension is disabled. + * + * @param string $value The header value to decode + * @param string $charset The target charset to convert to, defaults to ISO-8859-1 for BC + * + * @return string The decoded header value + */ + public static function decodeHeader($value, $charset = self::CHARSET_ISO88591) + { + if (!is_string($value) || $value === '') { + return ''; + } + // Detect the presence of any RFC2047 encoded-words + $hasEncodedWord = (bool) preg_match('/=\?.*\?=/s', $value); + if ($hasEncodedWord && defined('MB_CASE_UPPER')) { + $origCharset = mb_internal_encoding(); + // Always decode to UTF-8 to provide a consistent, modern output encoding. + mb_internal_encoding($charset); + if (PHP_VERSION_ID < 80300) { + // Undo any RFC2047-encoded spaces-as-underscores. + $value = str_replace('_', '=20', $value); + } else { + // PHP 8.3+ already interprets underscores as spaces. Remove additional + // linear whitespace between adjacent encoded words to avoid double spacing. + $value = preg_replace('/(\?=)\s+(=\?)/', '$1$2', $value); + } + // Decode the header value + $value = mb_decode_mimeheader($value); + mb_internal_encoding($origCharset); + } + + return $value; + } + /** * Check if a string contains multi-byte characters. * @@ -3840,7 +3938,7 @@ public function addStringAttachment( } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } //Append to $attachment array @@ -3899,7 +3997,7 @@ public function addEmbeddedImage( ) { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name @@ -3908,7 +4006,7 @@ public function addEmbeddedImage( } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); @@ -3974,7 +4072,7 @@ public function addStringEmbeddedImage( } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } //Append to $attachment array @@ -4231,7 +4329,7 @@ public function replaceCustomHeader($name, $value = null) } if (strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception($this->lang('invalid_header')); + throw new Exception(self::lang('invalid_header')); } return false; @@ -4255,15 +4353,15 @@ protected function setError($msg) if ('smtp' === $this->Mailer && null !== $this->smtp) { $lasterror = $this->smtp->getError(); if (!empty($lasterror['error'])) { - $msg .= ' ' . $this->lang('smtp_error') . $lasterror['error']; + $msg .= ' ' . self::lang('smtp_error') . $lasterror['error']; if (!empty($lasterror['detail'])) { - $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail']; + $msg .= ' ' . self::lang('smtp_detail') . $lasterror['detail']; } if (!empty($lasterror['smtp_code'])) { - $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code']; + $msg .= ' ' . self::lang('smtp_code') . $lasterror['smtp_code']; } if (!empty($lasterror['smtp_code_ex'])) { - $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex']; + $msg .= ' ' . self::lang('smtp_code_ex') . $lasterror['smtp_code_ex']; } } } @@ -4388,21 +4486,21 @@ public function needsSMTPUTF8() * * @return string */ - protected function lang($key) + protected static function lang($key) { - if (count($this->language) < 1) { - $this->setLanguage(); //Set the default language + if (count(self::$language) < 1) { + self::setLanguage(); //Set the default language } - if (array_key_exists($key, $this->language)) { + if (array_key_exists($key, self::$language)) { if ('smtp_connect_failed' === $key) { //Include a link to troubleshooting docs on SMTP connection failure. //This is by far the biggest cause of support questions //but it's usually not PHPMailer's fault. - return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + return self::$language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; } - return $this->language[$key]; + return self::$language[$key]; } //Return the key as a fallback @@ -4417,7 +4515,7 @@ protected function lang($key) */ private function getSmtpErrorMessage($base_key) { - $message = $this->lang($base_key); + $message = self::lang($base_key); $error = $this->smtp->getError(); if (!empty($error['error'])) { $message .= ' ' . $error['error']; @@ -4461,7 +4559,7 @@ public function addCustomHeader($name, $value = null) //Ensure name is not empty, and that neither name nor value contain line breaks if (empty($name) || strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception($this->lang('invalid_header')); + throw new Exception(self::lang('invalid_header')); } return false; @@ -4854,7 +4952,7 @@ public function set($name, $value = '') return true; } - $this->setError($this->lang('variable_set') . $name); + $this->setError(self::lang('variable_set') . $name); return false; } @@ -4992,7 +5090,7 @@ public function DKIM_Sign($signHeader) { if (!defined('PKCS7_TEXT')) { if ($this->exceptions) { - throw new Exception($this->lang('extension_missing') . 'openssl'); + throw new Exception(self::lang('extension_missing') . 'openssl'); } return ''; diff --git a/src/wp-includes/PHPMailer/POP3.php b/src/wp-includes/PHPMailer/POP3.php index 1190a1e20da26..d45b4ad78de29 100644 --- a/src/wp-includes/PHPMailer/POP3.php +++ b/src/wp-includes/PHPMailer/POP3.php @@ -46,7 +46,7 @@ class POP3 * * @var string */ - const VERSION = '6.10.0'; + const VERSION = '6.11.0'; /** * Default POP3 port number. diff --git a/src/wp-includes/PHPMailer/SMTP.php b/src/wp-includes/PHPMailer/SMTP.php index 7226ee93be93b..c448a0aea9465 100644 --- a/src/wp-includes/PHPMailer/SMTP.php +++ b/src/wp-includes/PHPMailer/SMTP.php @@ -35,7 +35,7 @@ class SMTP * * @var string */ - const VERSION = '6.10.0'; + const VERSION = '6.11.0'; /** * SMTP line break constant. @@ -205,6 +205,7 @@ class SMTP 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', 'ZoneMTA' => '/[\d]{3} Message queued as (.*)/', 'Mailjet' => '/[\d]{3} OK queued as (.*)/', + 'Gsmtp' => '/[\d]{3} 2\.0\.0 OK (.*) - gsmtp/', ]; /** @@ -633,10 +634,41 @@ public function authenticate( return false; } $oauth = $OAuth->getOauth64(); - - //Start authentication - if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { - return false; + /* + * An SMTP command line can have a maximum length of 512 bytes, including the command name, + * so the base64-encoded OAUTH token has a maximum length of: + * 512 - 13 (AUTH XOAUTH2) - 2 (CRLF) = 497 bytes + * If the token is longer than that, the command and the token must be sent separately as described in + * https://www.rfc-editor.org/rfc/rfc4954#section-4 + */ + if ($oauth === '') { + //Sending an empty auth token is legitimate, but it must be encoded as '=' + //to indicate it's not a 2-part command + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 =', 235)) { + return false; + } + } elseif (strlen($oauth) <= 497) { + //Authenticate using a token in the initial-response part + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { + return false; + } + } else { + //The token is too long, so we need to send it in two parts. + //Send the auth command without a token and expect a 334 + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2', 334)) { + return false; + } + //Send the token + if (!$this->sendCommand('OAuth TOKEN', $oauth, [235, 334])) { + return false; + } + //If the server answers with 334, send an empty line and wait for a 235 + if ( + substr($this->last_reply, 0, 3) === '334' + && $this->sendCommand('AUTH End', '', 235) + ) { + return false; + } } break; default: @@ -1309,7 +1341,16 @@ protected function get_lines() //stream_select returns false when the `select` system call is interrupted //by an incoming signal, try the select again - if (stripos($message, 'interrupted system call') !== false) { + if ( + stripos($message, 'interrupted system call') !== false || + ( + // on applications with a different locale than english, the message above is not found because + // it's translated. So we also check for the SOCKET_EINTR constant which is defined under + // Windows and UNIX-like platforms (if available on the platform). + defined('SOCKET_EINTR') && + stripos($message, 'stream_select(): Unable to select [' . SOCKET_EINTR . ']') !== false + ) + ) { $this->edebug( 'SMTP -> get_lines(): retrying stream_select', self::DEBUG_LOWLEVEL diff --git a/src/wp-includes/class-wp-phpmailer.php b/src/wp-includes/class-wp-phpmailer.php index 3b2ae8d487801..24d59c113cd6c 100644 --- a/src/wp-includes/class-wp-phpmailer.php +++ b/src/wp-includes/class-wp-phpmailer.php @@ -24,7 +24,7 @@ class WP_PHPMailer extends PHPMailer\PHPMailer\PHPMailer { */ public function __construct( $exceptions = false ) { parent::__construct( $exceptions ); - $this->setLanguage(); + static::setLanguage(); } /** @@ -36,8 +36,8 @@ public function __construct( $exceptions = false ) { * @param string $lang_path Optional. Unused. Path to the language file directory. Default empty string. * @return true Always returns true. */ - public function setLanguage( $langcode = 'en', $lang_path = '' ) { - $this->language = array( + public static function setLanguage( $langcode = 'en', $lang_path = '' ) { + static::$language = array( 'authenticate' => __( 'SMTP Error: Could not authenticate.' ), 'buggy_php' => sprintf( /* translators: 1: mail.add_x_header. 2: php.ini */ @@ -89,6 +89,12 @@ public function setLanguage( $langcode = 'en', $lang_path = '' ) { /* translators: There is a space after the colon. */ 'variable_set' => __( 'Cannot set or reset variable: ' ), 'no_smtputf8' => __( 'Server does not support SMTPUTF8 needed to send to Unicode addresses' ), + 'imap_recommended' => __( 'Using simplified address parser is not recommended. Install the PHP IMAP extension for full RFC822 parsing.' ), + 'deprecated_argument' => sprintf( + /* translators: %s: $deprecatedArg */ + __( 'Argument %s is deprecated' ), + '$deprecatedArg' + ), ); return true; From 52992d2ba947a87de83fa0f3ff1e3ff44f70d5b6 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 30 Sep 2025 15:49:18 +0000 Subject: [PATCH 06/16] REST API: Increase the specificity of capability checks for collections when the `edit` context is in use. The edit access in now taken into account for each individual post, term, or user in the response. Props andraganescu, desrosj, ehti, hurayraiit, iandunn, joehoyle, johnbillion, jorbin, mnelson4, noisysocks, peterwilsoncc, rmccue, timothyblynjacobs, vortfu, whyisjake, zieladam. git-svn-id: https://develop.svn.wordpress.org/trunk@60814 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-rest-posts-controller.php | 8 +++++++- .../class-wp-rest-terms-controller.php | 4 ++++ .../class-wp-rest-users-controller.php | 18 ++++++++++++------ .../tests/rest-api/rest-users-controller.php | 2 +- 4 files changed, 24 insertions(+), 8 deletions(-) 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 19e399f55e64f..3812d28a1b021 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 @@ -463,7 +463,13 @@ static function ( $format ) { } foreach ( $query_result as $post ) { - if ( ! $this->check_read_permission( $post ) ) { + if ( 'edit' === $request['context'] ) { + $permission = $this->check_update_permission( $post ); + } else { + $permission = $this->check_read_permission( $post ); + } + + if ( ! $permission ) { continue; } 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 72632fa96a258..1f7d676dfa893 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 @@ -365,6 +365,10 @@ public function get_items( $request ) { if ( ! $is_head_request ) { $response = array(); foreach ( $query_result as $term ) { + if ( 'edit' === $request['context'] && ! current_user_can( 'edit_term', $term->term_id ) ) { + continue; + } + $data = $this->prepare_item_for_response( $term, $request ); $response[] = $this->prepare_response_for_collection( $data ); } 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 78e1b38b1b4e2..dc9dfc81aae40 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 @@ -220,7 +220,7 @@ public function get_items_permissions_check( $request ) { if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_forbidden_context', - __( 'Sorry, you are not allowed to list users.' ), + __( 'Sorry, you are not allowed to edit users.' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -379,6 +379,10 @@ static function ( $column ) use ( $search_columns_mapping ) { $users = array(); foreach ( $query->get_results() as $user ) { + if ( 'edit' === $request['context'] && ! current_user_can( 'edit_user', $user->ID ) ) { + continue; + } + $data = $this->prepare_item_for_response( $user, $request ); $users[] = $this->prepare_response_for_collection( $data ); } @@ -479,13 +483,15 @@ public function get_item_permissions_check( $request ) { return true; } - if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { + if ( 'edit' === $request['context'] && ! current_user_can( 'edit_user', $user->ID ) ) { return new WP_Error( - 'rest_user_cannot_view', - __( 'Sorry, you are not allowed to list users.' ), + 'rest_forbidden_context', + __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => rest_authorization_required_code() ) ); - } elseif ( ! count_user_posts( $user->ID, $types ) && ! current_user_can( 'edit_user', $user->ID ) && ! current_user_can( 'list_users' ) ) { + } + + if ( ! current_user_can( 'edit_user', $user->ID ) && ! current_user_can( 'list_users' ) && ! count_user_posts( $user->ID, $types ) ) { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to list users.' ), @@ -1086,7 +1092,7 @@ public function prepare_item_for_response( $item, $request ) { $data['slug'] = $user->user_nicename; } - if ( in_array( 'roles', $fields, true ) ) { + if ( in_array( 'roles', $fields, true ) && ( current_user_can( 'list_users' ) || current_user_can( 'edit_user', $user->ID ) ) ) { // Defensively call array_values() to ensure an array is returned. $data['roles'] = array_values( $user->roles ); } diff --git a/tests/phpunit/tests/rest-api/rest-users-controller.php b/tests/phpunit/tests/rest-api/rest-users-controller.php index db34f88752e23..b78e95b95f48d 100644 --- a/tests/phpunit/tests/rest-api/rest-users-controller.php +++ b/tests/phpunit/tests/rest-api/rest-users-controller.php @@ -1313,7 +1313,7 @@ public function test_get_item_published_author_wrong_context() { $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $author_id ) ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); } /** From 984538422633a6ff0b31c8151e02e8d1807b3454 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 30 Sep 2025 16:26:44 +0000 Subject: [PATCH 07/16] Menus: Prevent HTML in menu item titles from being rendered unexpectedly. Props audrasjb, desrosj, johnbillion, jorbin, phillsav, vortfu, westonruter git-svn-id: https://develop.svn.wordpress.org/trunk@60815 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/lib/nav-menu.js | 116 ++++++++++-------- src/js/_enqueues/wp/customize/nav-menus.js | 9 +- .../class-wp-customize-nav-menus.php | 46 ++++--- ...ass-wp-customize-nav-menu-item-setting.php | 47 ++++--- .../tests/customize/nav-menu-item-setting.php | 15 ++- tests/phpunit/tests/customize/nav-menus.php | 84 +++++++------ 6 files changed, 179 insertions(+), 138 deletions(-) diff --git a/src/js/_enqueues/lib/nav-menu.js b/src/js/_enqueues/lib/nav-menu.js index d16df605dbce3..666780a9e399b 100644 --- a/src/js/_enqueues/lib/nav-menu.js +++ b/src/js/_enqueues/lib/nav-menu.js @@ -298,44 +298,46 @@ $.each( parentDropdowns, function() { var parentDropdown = $( this ), - $html = '', - $selected = '', - currentItemID = parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-db-id' ).val(), - currentparentID = parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-parent-id' ).val(), + currentItemID = parseInt( parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-db-id' ).val() ), + currentParentID = parseInt( parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-parent-id' ).val() ), currentItem = parentDropdown.closest( 'li.menu-item' ), currentMenuItemChild = currentItem.childMenuItems(), - excludeMenuItem = [ currentItemID ]; + excludeMenuItem = /** @type {number[]} */ [ currentItemID ]; + + parentDropdown.empty(); if ( currentMenuItemChild.length > 0 ) { $.each( currentMenuItemChild, function(){ var childItem = $(this), - childID = childItem.find( '.menu-item-data-db-id' ).val(); + childID = parseInt( childItem.find( '.menu-item-data-db-id' ).val() ); excludeMenuItem.push( childID ); }); } - if ( currentparentID == 0 ) { - $selected = 'selected'; - } - - $html += ''; + parentDropdown.append( + $( ''; + parentDropdown.append( + $( ''; + orderDropdown.append( + $( ''; + orderDropdown.append( + $( '