Skip to content

Commit 1af116d

Browse files
committed
Code Quality: Correct nullable WP_Http_Cookie property types.
A cookie created without a name, value, domain, expiration or host-only flag, or from incomplete data, leaves those properties `null`, and an unparsable expiration date is stored as `false`, but their documented types said otherwise. PHPStan therefore treated `isset()` checks on them as always true, so correcting the types retires the `booleanOr.alwaysFalse` baseline entirely along with several `isset.property` entries. The corrected types surfaced real errors in callers, which are fixed rather than baselined: * In `WP_Http::normalize_cookies()`, a `WP_Http_Cookie` value was passed to `WpOrg\Requests\Cookie` as is, which throws an `InvalidArgument` exception for anything but a string, failing the request. A cookie without a value or with a non-scalar value is now skipped, as `WP_Http_Cookie::getHeaderValue()` already does, and other scalar values are cast to strings. * `wp_remote_retrieve_cookie_value()` now returns an empty string instead of `null` for a cookie without a value. The handling of empty or missing cookie names is unchanged, as it is being addressed separately in #66305. Developed in #14283. Follow-up to r38164, r45135, r57501, r63023. Props huzaifaalmesbah, westonruter. See #65817, #66305. git-svn-id: https://develop.svn.wordpress.org/trunk@64350 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 7ef3b33 commit 1af116d

10 files changed

Lines changed: 344 additions & 60 deletions

File tree

‎phpstan.neon.dist‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ includes:
3131
- tests/phpstan/baselines/booleanAnd.rightAlwaysTrue.neon
3232
- tests/phpstan/baselines/booleanNot.alwaysFalse.neon
3333
- tests/phpstan/baselines/booleanNot.alwaysTrue.neon
34-
- tests/phpstan/baselines/booleanOr.alwaysFalse.neon
3534
- tests/phpstan/baselines/class.notFound.neon
3635
- tests/phpstan/baselines/deadCode.unreachable.neon
3736
- tests/phpstan/baselines/empty.offset.neon

‎src/wp-includes/class-wp-http-cookie.php‎

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class WP_Http_Cookie {
2626
*
2727
* @since 2.8.0
2828
*
29-
* @var string
29+
* @var string|null
3030
*/
3131
public $name;
3232

@@ -35,16 +35,18 @@ class WP_Http_Cookie {
3535
*
3636
* @since 2.8.0
3737
*
38-
* @var string
38+
* @var string|null
3939
*/
4040
public $value;
4141

4242
/**
43-
* When the cookie expires. Unix timestamp or formatted date.
43+
* When the cookie expires, as a Unix timestamp.
44+
*
45+
* False if the expiration date could not be parsed, or null if not provided.
4446
*
4547
* @since 2.8.0
4648
*
47-
* @var string|int|null
49+
* @var int|false|null
4850
*/
4951
public $expires;
5052

@@ -62,7 +64,7 @@ class WP_Http_Cookie {
6264
*
6365
* @since 2.8.0
6466
*
65-
* @var string
67+
* @var string|null
6668
*/
6769
public $domain;
6870

@@ -76,11 +78,13 @@ class WP_Http_Cookie {
7678
public $port;
7779

7880
/**
79-
* host-only flag.
81+
* Host-only flag.
82+
*
83+
* Null if not provided.
8084
*
8185
* @since 5.2.0
8286
*
83-
* @var bool
87+
* @var bool|null
8488
*/
8589
public $host_only;
8690

@@ -100,9 +104,9 @@ class WP_Http_Cookie {
100104
* @type mixed $value Value. Should NOT already be urlencoded.
101105
* @type string|int|null $expires Optional. Unix timestamp or formatted date. Default null.
102106
* @type string|null $path Optional. Path. Default '/'.
103-
* @type string $domain Optional. Domain. Default host of parsed $requested_url.
107+
* @type string|null $domain Optional. Domain. Default host of parsed $requested_url.
104108
* @type int|string|null $port Optional. Port or comma-separated list of ports. Default null.
105-
* @type bool $host_only Optional. host-only storage flag. Default true.
109+
* @type bool|null $host_only Optional. Host-only storage flag. Default null.
106110
* }
107111
* @param string $requested_url The URL which the cookie was set on, used for default $domain
108112
* and $port values.
@@ -262,9 +266,9 @@ public function getFullHeader() { // phpcs:ignore WordPress.NamingConventions.Va
262266
* @return array {
263267
* List of attributes.
264268
*
265-
* @type string|int|null $expires When the cookie expires. Unix timestamp or formatted date.
266-
* @type string $path Cookie URL path.
267-
* @type string $domain Cookie domain.
269+
* @type int|false|null $expires When the cookie expires, as a Unix timestamp.
270+
* @type string $path Cookie URL path.
271+
* @type string|null $domain Cookie domain.
268272
* }
269273
*/
270274
public function get_attributes() {

‎src/wp-includes/class-wp-http.php‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -474,24 +474,38 @@ public function request( $url, $args = array() ) {
474474
*
475475
* @since 4.6.0
476476
*
477-
* @param array $cookies Array of cookies to send with the request.
477+
* @param array<int|string, WP_Http_Cookie|scalar> $cookies Array of cookies to send with the request,
478+
* as WP_Http_Cookie objects or name/value pairs.
479+
* Cookies without a scalar value are ignored.
478480
* @return WpOrg\Requests\Cookie\Jar Cookie holder object.
479481
*/
480482
public static function normalize_cookies( $cookies ) {
481483
$cookie_jar = new WpOrg\Requests\Cookie\Jar();
482484

483485
foreach ( $cookies as $name => $value ) {
484486
if ( $value instanceof WP_Http_Cookie ) {
485-
$attributes = array_filter(
487+
$cookie_name = (string) $value->name;
488+
$cookie_value = $value->value;
489+
$attributes = array_filter(
486490
$value->get_attributes(),
487491
static function ( $attr ) {
488492
return null !== $attr;
489493
}
490494
);
491-
$cookie_jar[ $value->name ] = new WpOrg\Requests\Cookie( (string) $value->name, $value->value, $attributes, array( 'host-only' => $value->host_only ) );
492-
} elseif ( is_scalar( $value ) ) {
493-
$cookie_jar[ $name ] = new WpOrg\Requests\Cookie( (string) $name, (string) $value );
495+
$flags = array( 'host-only' => $value->host_only );
496+
} else {
497+
$cookie_name = (string) $name;
498+
$cookie_value = $value;
499+
$attributes = array();
500+
$flags = array();
494501
}
502+
503+
// A cookie without a value is not sent by WP_Http_Cookie::getHeaderValue() either, and a non-scalar value cannot be represented.
504+
if ( ! is_scalar( $cookie_value ) ) {
505+
continue;
506+
}
507+
508+
$cookie_jar[ $cookie_name ] = new WpOrg\Requests\Cookie( $cookie_name, (string) $cookie_value, $attributes, $flags );
495509
}
496510

497511
return $cookie_jar;

‎src/wp-includes/http.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ function wp_remote_retrieve_cookie_value( $response, $name ) {
381381
return '';
382382
}
383383

384-
return $cookie->value;
384+
return $cookie->value ?? '';
385385
}
386386

387387
/**

‎tests/phpstan/baselines/booleanOr.alwaysFalse.neon‎

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

‎tests/phpstan/baselines/isset.property.neon‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,21 +113,6 @@ parameters:
113113
identifier: isset.property
114114
count: 1
115115
path: ../../../src/wp-includes/class-wp-customize-widgets.php
116-
-
117-
message: '#^Property WP_Http_Cookie\:\:\$domain \(string\) in isset\(\) is not nullable\.$#'
118-
identifier: isset.property
119-
count: 1
120-
path: ../../../src/wp-includes/class-wp-http-cookie.php
121-
-
122-
message: '#^Property WP_Http_Cookie\:\:\$name \(string\) in isset\(\) is not nullable\.$#'
123-
identifier: isset.property
124-
count: 1
125-
path: ../../../src/wp-includes/class-wp-http-cookie.php
126-
-
127-
message: '#^Property WP_Http_Cookie\:\:\$value \(string\) in isset\(\) is not nullable\.$#'
128-
identifier: isset.property
129-
count: 1
130-
path: ../../../src/wp-includes/class-wp-http-cookie.php
131116
-
132117
message: '#^Property WP_Post\:\:\$ID \(int\<0, max\>\) in isset\(\) is not nullable\.$#'
133118
identifier: isset.property

‎tests/phpstan/baselines/offsetAssign.valueType.neon‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ parameters:
2121
-
2222
message: '#^WpOrg\\Requests\\Cookie\\Jar does not accept WpOrg\\Requests\\Cookie\.$#'
2323
identifier: offsetAssign.valueType
24-
count: 2
24+
count: 1
2525
path: ../../../src/wp-includes/class-wp-http.php

‎tests/phpunit/tests/http/functions.php‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,20 @@ public function test_get_response_cookies() {
225225
$this->assertSame( '', $no_cookie );
226226
}
227227

228+
/**
229+
* Tests that {@see wp_remote_retrieve_cookie_value()} returns an empty string for a cookie without a value.
230+
*
231+
* @covers ::wp_remote_retrieve_cookie_value
232+
*/
233+
public function test_wp_remote_retrieve_cookie_value_for_cookie_without_value(): void {
234+
$cookie = new WP_Http_Cookie( array( 'name' => 'flavor' ) );
235+
$this->assertNull( $cookie->value, 'Expected the cookie value to be null.' );
236+
237+
$response = array( 'cookies' => array( $cookie ) );
238+
239+
$this->assertSame( '', wp_remote_retrieve_cookie_value( $response, 'flavor' ) );
240+
}
241+
228242
/**
229243
* @ticket 37437
230244
*

‎tests/phpunit/tests/http/http.php‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,4 +696,76 @@ public function test_normalize_cookies_casts_cookie_name_integer_to_string() {
696696
$this->assertInstanceOf( 'WpOrg\Requests\Cookie\Jar', $cookie_jar );
697697
$this->assertInstanceOf( 'WpOrg\Requests\Cookie', $cookie_jar['1'] );
698698
}
699+
700+
/**
701+
* Tests that {@see WP_Http::normalize_cookies()} skips a cookie without a value, as {@see WP_Http_Cookie::getHeaderValue()} does.
702+
*
703+
* @covers WP_Http::normalize_cookies
704+
*/
705+
public function test_normalize_cookies_skips_cookie_without_value(): void {
706+
$cookie = new WP_Http_Cookie( array( 'name' => 'flavor' ) );
707+
$this->assertNull( $cookie->value, 'Expected the cookie value to be null.' );
708+
$this->assertSame( '', $cookie->getHeaderValue(), 'Expected the cookie to be omitted from the header value.' );
709+
710+
$cookie_jar = WP_Http::normalize_cookies( array( $cookie ) );
711+
712+
$this->assertSame( array(), iterator_to_array( $cookie_jar ) );
713+
}
714+
715+
/**
716+
* Tests that {@see WP_Http::normalize_cookies()} skips a cookie with a non-scalar value.
717+
*
718+
* @covers WP_Http::normalize_cookies
719+
*/
720+
public function test_normalize_cookies_skips_cookie_with_non_scalar_value(): void {
721+
$cookie = new WP_Http_Cookie(
722+
array(
723+
'name' => 'prefs',
724+
'value' => array( 'a' => 1 ),
725+
)
726+
);
727+
728+
$cookie_jar = WP_Http::normalize_cookies( array( $cookie ) );
729+
730+
$this->assertSame( array(), iterator_to_array( $cookie_jar ) );
731+
}
732+
733+
/**
734+
* Tests that {@see WP_Http::normalize_cookies()} sends a cookie with a scalar value as a string.
735+
*
736+
* @covers WP_Http::normalize_cookies
737+
*/
738+
public function test_normalize_cookies_casts_scalar_cookie_value_to_string(): void {
739+
$cookie = new WP_Http_Cookie(
740+
array(
741+
'name' => 'count',
742+
'value' => 5,
743+
)
744+
);
745+
746+
$cookie_jar = WP_Http::normalize_cookies( array( $cookie ) );
747+
748+
$this->assertInstanceOf( 'WpOrg\Requests\Cookie', $cookie_jar['count'] );
749+
$this->assertSame( '5', $cookie_jar['count']->value );
750+
}
751+
752+
/**
753+
* Tests that {@see WP_Http::normalize_cookies()} sends a cookie with an empty value.
754+
*
755+
* @covers WP_Http::normalize_cookies
756+
*/
757+
public function test_normalize_cookies_sends_cookie_with_empty_value(): void {
758+
$cookie = new WP_Http_Cookie(
759+
array(
760+
'name' => 'flavor',
761+
'value' => '',
762+
)
763+
);
764+
$this->assertSame( 'flavor=', $cookie->getHeaderValue(), 'Expected the cookie to be in the header value.' );
765+
766+
$cookie_jar = WP_Http::normalize_cookies( array( $cookie ) );
767+
768+
$this->assertInstanceOf( 'WpOrg\Requests\Cookie', $cookie_jar['flavor'] );
769+
$this->assertSame( 'flavor=', $cookie_jar['flavor']->format_for_header() );
770+
}
699771
}

0 commit comments

Comments
 (0)