1<?php
2/**
3 * HTTP API: WP_Http_Cookie class
4 *
5 * @package WordPress
6 * @subpackage HTTP
7 * @since 4.4.0
8 */
9
10/**
11 * Core class used to encapsulate a single cookie object for internal use.
12 *
13 * Returned cookies are represented using this class, and when cookies are set, if they are not
14 * already a WP_Http_Cookie() object, then they are turned into one.
15 *
16 * @todo The WordPress convention is to use underscores instead of camelCase for function and method
17 * names. Need to switch to use underscores instead for the methods.
18 *
19 * @since 2.8.0
20 */
21#[AllowDynamicProperties]
22class WP_Http_Cookie {
23
24 /**
25 * Cookie name.
26 *
27 * @since 2.8.0
28 *
29 * @var string
30 */
31 public $name;
32
33 /**
34 * Cookie value.
35 *
36 * @since 2.8.0
37 *
38 * @var string
39 */
40 public $value;
41
42 /**
43 * When the cookie expires. Unix timestamp or formatted date.
44 *
45 * @since 2.8.0
46 *
47 * @var string|int|null
48 */
49 public $expires;
50
51 /**
52 * Cookie URL path.
53 *
54 * @since 2.8.0
55 *
56 * @var string
57 */
58 public $path;
59
60 /**
61 * Cookie Domain.
62 *
63 * @since 2.8.0
64 *
65 * @var string
66 */
67 public $domain;
68
69 /**
70 * Cookie port or comma-separated list of ports.
71 *
72 * @since 2.8.0
73 *
74 * @var int|string
75 */
76 public $port;
77
78 /**
79 * host-only flag.
80 *
81 * @since 5.2.0
82 *
83 * @var bool
84 */
85 public $host_only;
86
87 /**
88 * Sets up this cookie object.
89 *
90 * The parameter $data should be either an associative array containing the indices names below
91 * or a header string detailing it.
92 *
93 * @since 2.8.0
94 * @since 5.2.0 Added `host_only` to the `$data` parameter.
95 *
96 * @param string|array $data {
97 * Raw cookie data as header string or data array.
98 *
99 * @type string $name Cookie name.
100 * @type mixed $value Value. Should NOT already be urlencoded.
101 * @type string|int|null $expires Optional. Unix timestamp or formatted date. Default null.
102 * @type string $path Optional. Path. Default '/'.
103 * @type string $domain Optional. Domain. Default host of parsed $requested_url.
104 * @type int|string $port Optional. Port or comma-separated list of ports. Default null.
105 * @type bool $host_only Optional. host-only storage flag. Default true.
106 * }
107 * @param string $requested_url The URL which the cookie was set on, used for default $domain
108 * and $port values.
109 */
110 public function __construct( $data, $requested_url = '' ) {
111 if ( $requested_url ) {
112 $parsed_url = parse_url( $requested_url );
113 }
114 if ( isset( $parsed_url['host'] ) ) {
115 $this->domain = $parsed_url['host'];
116 }
117 $this->path = isset( $parsed_url['path'] ) ? $parsed_url['path'] : '/';
118 if ( ! str_ends_with( $this->path, '/' ) ) {
119 $this->path = dirname( $this->path ) . '/';
120 }
121
122 if ( is_string( $data ) ) {
123 // Assume it's a header string direct from a previous request.
124 $pairs = explode( ';', $data );
125
126 // Special handling for first pair; name=value. Also be careful of "=" in value.
127 $name = trim( substr( $pairs[0], 0, strpos( $pairs[0], '=' ) ) );
128 $value = substr( $pairs[0], strpos( $pairs[0], '=' ) + 1 );
129 $this->name = $name;
130 $this->value = urldecode( $value );
131
132 // Removes name=value from items.
133 array_shift( $pairs );
134
135 // Set everything else as a property.
136 foreach ( $pairs as $pair ) {
137 $pair = rtrim( $pair );
138
139 // Handle the cookie ending in ; which results in an empty final pair.
140 if ( empty( $pair ) ) {
141 continue;
142 }
143
144 list( $key, $val ) = strpos( $pair, '=' ) ? explode( '=', $pair ) : array( $pair, '' );
145 $key = strtolower( trim( $key ) );
146 if ( 'expires' === $key ) {
147 $val = strtotime( $val );
148 }
149 $this->$key = $val;
150 }
151 } else {
152 if ( ! isset( $data['name'] ) ) {
153 return;
154 }
155
156 // Set properties based directly on parameters.
157 foreach ( array( 'name', 'value', 'path', 'domain', 'port', 'host_only' ) as $field ) {
158 if ( isset( $data[ $field ] ) ) {
159 $this->$field = $data[ $field ];
160 }
161 }
162
163 if ( isset( $data['expires'] ) ) {
164 $this->expires = is_int( $data['expires'] ) ? $data['expires'] : strtotime( $data['expires'] );
165 } else {
166 $this->expires = null;
167 }
168 }
169 }
170
171 /**
172 * Confirms that it's OK to send this cookie to the URL checked against.
173 *
174 * Decision is based on RFC 2109/2965, so look there for details on validity.
175 *
176 * @since 2.8.0
177 *
178 * @param string $url URL you intend to send this cookie to
179 * @return bool true if allowed, false otherwise.
180 */
181 public function test( $url ) {
182 if ( is_null( $this->name ) ) {
183 return false;
184 }
185
186 // Expires - if expired then nothing else matters.
187 if ( isset( $this->expires ) && time() > $this->expires ) {
188 return false;
189 }
190
191 // Get details on the URL we're thinking about sending to.
192 $url = parse_url( $url );
193 $url['port'] = isset( $url['port'] ) ? $url['port'] : ( 'https' === $url['scheme'] ? 443 : 80 );
194 $url['path'] = isset( $url['path'] ) ? $url['path'] : '/';
195
196 // Values to use for comparison against the URL.
197 $path = isset( $this->path ) ? $this->path : '/';
198 $port = isset( $this->port ) ? $this->port : null;
199 $domain = isset( $this->domain ) ? strtolower( $this->domain ) : strtolower( $url['host'] );
200 if ( false === stripos( $domain, '.' ) ) {
201 $domain .= '.local';
202 }
203
204 // Host - very basic check that the request URL ends with the domain restriction (minus leading dot).
205 $domain = ( str_starts_with( $domain, '.' ) ) ? substr( $domain, 1 ) : $domain;
206 if ( ! str_ends_with( $url['host'], $domain ) ) {
207 return false;
208 }
209
210 // Port - supports "port-lists" in the format: "80,8000,8080".
211 if ( ! empty( $port ) && ! in_array( $url['port'], array_map( 'intval', explode( ',', $port ) ), true ) ) {
212 return false;
213 }
214
215 // Path - request path must start with path restriction.
216 if ( ! str_starts_with( $url['path'], $path ) ) {
217 return false;
218 }
219
220 return true;
221 }
222
223 /**
224 * Convert cookie name and value back to header string.
225 *
226 * @since 2.8.0
227 *
228 * @return string Header encoded cookie name and value.
229 */
230 public function getHeaderValue() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
231 if ( ! isset( $this->name ) || ! isset( $this->value ) ) {
232 return '';
233 }
234
235 /**
236 * Filters the header-encoded cookie value.
237 *
238 * @since 3.4.0
239 *
240 * @param string $value The cookie value.
241 * @param string $name The cookie name.
242 */
243 return $this->name . '=' . apply_filters( 'wp_http_cookie_value', $this->value, $this->name );
244 }
245
246 /**
247 * Retrieve cookie header for usage in the rest of the WordPress HTTP API.
248 *
249 * @since 2.8.0
250 *
251 * @return string
252 */
253 public function getFullHeader() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
254 return 'Cookie: ' . $this->getHeaderValue();
255 }
256
257 /**
258 * Retrieves cookie attributes.
259 *
260 * @since 4.6.0
261 *
262 * @return array {
263 * List of attributes.
264 *
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.
268 * }
269 */
270 public function get_attributes() {
271 return array(
272 'expires' => $this->expires,
273 'path' => $this->path,
274 'domain' => $this->domain,
275 );
276 }
277}
278