Coverage Report

Created: 2026-10-28 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/php-src/ext/uri/uri_parser_whatwg.c
Line
Count
Source
1
/*
2
   +----------------------------------------------------------------------+
3
   | Copyright © The PHP Group and Contributors.                          |
4
   +----------------------------------------------------------------------+
5
   | This source file is subject to the Modified BSD License that is      |
6
   | bundled with this package in the file LICENSE, and is available      |
7
   | through the World Wide Web at <https://www.php.net/license/>.        |
8
   |                                                                      |
9
   | SPDX-License-Identifier: BSD-3-Clause                                |
10
   +----------------------------------------------------------------------+
11
   | Authors: Máté Kocsis <kocsismate@php.net>                            |
12
   +----------------------------------------------------------------------+
13
*/
14
15
#include "php.h"
16
#include "uri_parser_whatwg.h"
17
#include "php_uri_common.h"
18
#include "Zend/zend_enum.h"
19
#include "Zend/zend_smart_str.h"
20
#include "Zend/zend_exceptions.h"
21
#ifdef HAVE_ARPA_INET_H
22
#include <arpa/inet.h>
23
#endif
24
25
ZEND_TLS lexbor_mraw_t lexbor_mraw = {0};
26
ZEND_TLS lxb_url_parser_t lexbor_parser = {0};
27
ZEND_TLS lxb_unicode_idna_t lexbor_idna = {0};
28
ZEND_TLS uint8_t lexbor_custom_url_map[256] = {0};
29
30
static const size_t lexbor_mraw_byte_size = 8192;
31
32
ZEND_ATTRIBUTE_NONNULL static zend_always_inline void zval_string_or_null_to_lexbor_str(const zval *value, lexbor_str_t *lexbor_str)
33
0
{
34
0
  if (Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) {
35
0
    lexbor_str->data = (lxb_char_t *) Z_STRVAL_P(value);
36
0
    lexbor_str->length = Z_STRLEN_P(value);
37
0
  } else {
38
0
    ZEND_ASSERT(Z_ISNULL_P(value) || (Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) == 0));
39
0
    lexbor_str->data = (lxb_char_t *) "";
40
0
    lexbor_str->length = 0;
41
0
  }
42
0
}
43
44
static zend_always_inline bool zval_long_or_null_to_lexbor_str(const zval *value, lexbor_str_t *lexbor_str)
45
0
{
46
0
  if (Z_TYPE_P(value) == IS_LONG) {
47
0
    char buf[MAX_LENGTH_OF_LONG + 1];
48
0
    const char *res = zend_print_long_to_buf(buf + sizeof(buf) - 1, Z_LVAL_P(value));
49
0
    const lxb_char_t *appended = lexbor_str_init_append(lexbor_str, lexbor_parser.mraw, (const lxb_char_t *) res, strlen(res));
50
0
    if (UNEXPECTED(appended == NULL)) {
51
0
      return false;
52
0
    }
53
0
  } else {
54
0
    ZEND_ASSERT(Z_ISNULL_P(value));
55
0
    lexbor_str->data = (lxb_char_t *) "";
56
0
    lexbor_str->length = 0;
57
0
  }
58
59
0
  return true;
60
0
}
61
62
ZEND_ATTRIBUTE_NONNULL static bool get_reason_from_error_type(const lxb_url_error_type_t error_type, const char **error_str)
63
0
{
64
0
  switch (error_type) {
65
0
    case LXB_URL_ERROR_TYPE_DOMAIN_TO_ASCII:
66
0
      *error_str = "DomainToAscii";
67
0
      return true;
68
0
    case LXB_URL_ERROR_TYPE_DOMAIN_TO_UNICODE:
69
0
      *error_str = "DomainToUnicode";
70
0
      return false;
71
0
    case LXB_URL_ERROR_TYPE_DOMAIN_INVALID_CODE_POINT:
72
0
      *error_str = "DomainInvalidCodePoint";
73
0
      return true;
74
0
    case LXB_URL_ERROR_TYPE_HOST_INVALID_CODE_POINT:
75
0
      *error_str = "HostInvalidCodePoint";
76
0
      return true;
77
0
    case LXB_URL_ERROR_TYPE_IPV4_EMPTY_PART:
78
0
      *error_str = "Ipv4EmptyPart";
79
0
      return false;
80
0
    case LXB_URL_ERROR_TYPE_IPV4_TOO_MANY_PARTS:
81
0
      *error_str = "Ipv4TooManyParts";
82
0
      return true;
83
0
    case LXB_URL_ERROR_TYPE_IPV4_NON_NUMERIC_PART:
84
0
      *error_str = "Ipv4NonNumericPart";
85
0
      return true;
86
0
    case LXB_URL_ERROR_TYPE_IPV4_NON_DECIMAL_PART:
87
0
      *error_str = "Ipv4NonDecimalPart";
88
0
      return false;
89
0
    case LXB_URL_ERROR_TYPE_IPV4_OUT_OF_RANGE_PART:
90
0
      *error_str = "Ipv4OutOfRangePart";
91
0
      return true;
92
0
    case LXB_URL_ERROR_TYPE_IPV6_UNCLOSED:
93
0
      *error_str = "Ipv6Unclosed";
94
0
      return true;
95
0
    case LXB_URL_ERROR_TYPE_IPV6_INVALID_COMPRESSION:
96
0
      *error_str = "Ipv6InvalidCompression";
97
0
      return true;
98
0
    case LXB_URL_ERROR_TYPE_IPV6_TOO_MANY_PIECES:
99
0
      *error_str = "Ipv6TooManyPieces";
100
0
      return true;
101
0
    case LXB_URL_ERROR_TYPE_IPV6_MULTIPLE_COMPRESSION:
102
0
      *error_str = "Ipv6MultipleCompression";
103
0
      return true;
104
0
    case LXB_URL_ERROR_TYPE_IPV6_INVALID_CODE_POINT:
105
0
      *error_str = "Ipv6InvalidCodePoint";
106
0
      return true;
107
0
    case LXB_URL_ERROR_TYPE_IPV6_TOO_FEW_PIECES:
108
0
      *error_str = "Ipv6TooFewPieces";
109
0
      return true;
110
0
    case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_TOO_MANY_PIECES:
111
0
      *error_str = "Ipv4InIpv6TooManyPieces";
112
0
      return true;
113
0
    case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_INVALID_CODE_POINT:
114
0
      *error_str = "Ipv4InIpv6InvalidCodePoint";
115
0
      return true;
116
0
    case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_OUT_OF_RANGE_PART:
117
0
      *error_str = "Ipv4InIpv6OutOfRangePart";
118
0
      return true;
119
0
    case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_TOO_FEW_PARTS:
120
0
      *error_str = "Ipv4InIpv6TooFewParts";
121
0
      return true;
122
0
    case LXB_URL_ERROR_TYPE_INVALID_URL_UNIT:
123
0
      *error_str = "InvalidUrlUnit";
124
0
      return false;
125
0
    case LXB_URL_ERROR_TYPE_SPECIAL_SCHEME_MISSING_FOLLOWING_SOLIDUS:
126
0
      *error_str = "SpecialSchemeMissingFollowingSolidus";
127
0
      return false;
128
0
    case LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL:
129
0
      *error_str = "MissingSchemeNonRelativeUrl";
130
0
      return true;
131
0
    case LXB_URL_ERROR_TYPE_INVALID_REVERSE_SOLIDUS:
132
0
      *error_str = "InvalidReverseSoldius";
133
0
      return false;
134
0
    case LXB_URL_ERROR_TYPE_INVALID_CREDENTIALS:
135
0
      *error_str = "InvalidCredentials";
136
0
      return false;
137
0
    case LXB_URL_ERROR_TYPE_HOST_MISSING:
138
0
      *error_str = "HostMissing";
139
0
      return true;
140
0
    case LXB_URL_ERROR_TYPE_PORT_OUT_OF_RANGE:
141
0
      *error_str = "PortOutOfRange";
142
0
      return true;
143
0
    case LXB_URL_ERROR_TYPE_PORT_INVALID:
144
0
      *error_str = "PortInvalid";
145
0
      return true;
146
0
    case LXB_URL_ERROR_TYPE_FILE_INVALID_WINDOWS_DRIVE_LETTER:
147
0
      *error_str = "FileInvalidWindowsDriveLetter";
148
0
      return false;
149
0
    case LXB_URL_ERROR_TYPE_FILE_INVALID_WINDOWS_DRIVE_LETTER_HOST:
150
0
      *error_str = "FileInvalidWindowsDriveLetterHost";
151
0
      return false;
152
0
    default: ZEND_UNREACHABLE();
153
0
  }
154
0
}
155
156
ZEND_ATTRIBUTE_NONNULL static bool append_validation_error(
157
  HashTable *errors, lxb_url_error_type_t error_type, const char *context, const char **reason
158
0
) {
159
0
  zval error;
160
0
  object_init_ex(&error, php_uri_ce_whatwg_url_validation_error);
161
0
  zend_update_property_string(php_uri_ce_whatwg_url_validation_error, Z_OBJ(error), ZEND_STRL("context"), context);
162
163
0
  bool failure = get_reason_from_error_type(error_type, reason);
164
0
  zval type;
165
0
  ZVAL_OBJ(&type, zend_enum_get_case_cstr(php_uri_ce_whatwg_url_validation_error_type, *reason));
166
0
  zend_update_property_ex(php_uri_ce_whatwg_url_validation_error, Z_OBJ(error), ZSTR_KNOWN(ZEND_STR_TYPE), &type);
167
0
  zend_update_property_bool(php_uri_ce_whatwg_url_validation_error, Z_OBJ(error), ZEND_STRL("failure"), failure);
168
0
  zend_hash_next_index_insert(errors, &error);
169
170
0
  return failure;
171
0
}
172
173
/**
174
 * Creates a Uri\WhatWg\UrlValidationError class by mapping error codes listed in
175
 * https://url.spec.whatwg.org/#writing to a Uri\WhatWg\UrlValidationErrorType enum.
176
 * The result is passed by reference to the errors parameter.
177
 */
178
ZEND_ATTRIBUTE_NONNULL static const char *fill_errors_inner(HashTable *errors)
179
0
{
180
0
  const char *result = NULL;
181
0
  lexbor_plog_t *log = lexbor_parser.log;
182
0
  const size_t length = lexbor_plog_length(log);
183
184
0
  for (size_t i = 0; i < length; i++) {
185
0
    const lexbor_plog_entry_t *lxb_error = lexbor_array_obj_get(&log->list, i);
186
0
    const char *reason;
187
0
    if (append_validation_error(errors, lxb_error->id, (const char *) lxb_error->data, &reason) && result == NULL) {
188
0
      result = reason;
189
0
    }
190
191
0
    if (ZEND_HAS_OOM()) {
192
0
      break;
193
0
    }
194
0
  }
195
0
  lexbor_plog_clean(log);
196
197
0
  return result;
198
0
}
199
200
/**
201
 * Creates a Uri\WhatWg\UrlValidationError class by mapping error codes listed in
202
 * https://url.spec.whatwg.org/#writing to a Uri\WhatWg\UrlValidationErrorType enum.
203
 * The result is passed by reference to the errors parameter.
204
 */
205
ZEND_ATTRIBUTE_NONNULL static const char *fill_errors(zval *errors)
206
0
{
207
0
  size_t log_len;
208
0
  if (lexbor_parser.log == NULL || (log_len = lexbor_plog_length(lexbor_parser.log)) == 0) {
209
0
    ZVAL_EMPTY_ARRAY(errors);
210
0
    return NULL;
211
0
  }
212
213
0
  array_init_size(errors, log_len);
214
215
0
  return fill_errors_inner(Z_ARRVAL_P(errors));
216
0
}
217
218
static void throw_invalid_url_exception_with_reason(zval *errors, const char *component, const char *reason, zval *err)
219
0
{
220
0
  if (ZEND_HAS_OOM()) {
221
0
    zend_handle_delayed_oom();
222
0
    return;
223
0
  }
224
225
0
  zend_object *exception = zend_throw_exception_ex(
226
0
    php_uri_ce_whatwg_invalid_url_exception,
227
0
    0,
228
0
    "The specified %s is malformed%s%s%s",
229
0
    component,
230
0
    reason ? " (" : "",
231
0
    reason ? reason : "",
232
0
    reason ? ")" : ""
233
0
  );
234
0
  zend_update_property(exception->ce, exception, ZEND_STRL("errors"), err);
235
0
  if (errors) {
236
0
    zval_ptr_dtor(errors);
237
0
    ZVAL_COPY_VALUE(errors, err);
238
0
  } else {
239
0
    zval_ptr_dtor(err);
240
0
  }
241
0
}
242
243
static void throw_invalid_url_exception_during_write(zval *errors, const char *component)
244
0
{
245
0
  zval err;
246
0
  const char *reason = fill_errors(&err);
247
248
0
  throw_invalid_url_exception_with_reason(errors, component, reason, &err);
249
0
}
250
251
static lxb_status_t serialize_to_smart_str_callback(const lxb_char_t *data, const size_t length, void *ctx)
252
0
{
253
0
  smart_str *uri_str = ctx;
254
255
0
  if (data != NULL && length > 0) {
256
0
    smart_str_appendl(uri_str, (const char *) data, length);
257
0
  }
258
259
0
  return LXB_STATUS_OK;
260
0
}
261
262
static zend_result php_uri_parser_whatwg_scheme_read(void *uri, const php_uri_component_read_mode read_mode, zval *retval)
263
0
{
264
0
  const lxb_url_t *lexbor_uri = uri;
265
266
0
  ZEND_ASSERT(lexbor_uri->scheme.type != LXB_URL_SCHEMEL_TYPE__UNDEF);
267
268
0
  ZVAL_STRINGL(retval, (const char *) lexbor_uri->scheme.name.data, lexbor_uri->scheme.name.length);
269
270
0
  return SUCCESS;
271
0
}
272
273
static zend_result php_uri_parser_whatwg_scheme_write(void *uri, const zval *value, zval *errors)
274
0
{
275
0
  lxb_url_t *lexbor_uri = uri;
276
0
  lexbor_str_t str = {0};
277
278
0
  zval_string_or_null_to_lexbor_str(value, &str);
279
280
0
  lxb_url_parser_clean(&lexbor_parser);
281
282
0
  if (lxb_url_api_protocol_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) {
283
0
    throw_invalid_url_exception_during_write(errors, "scheme");
284
285
0
    return FAILURE;
286
0
  }
287
288
0
  return SUCCESS;
289
0
}
290
291
ZEND_ATTRIBUTE_NONNULL bool php_uri_parser_whatwg_is_special(const lxb_url_t *lexbor_uri)
292
0
{
293
0
  return lxb_url_is_special(lexbor_uri);
294
0
}
295
296
/* 4.2. URL miscellaneous: A URL includes credentials if its username or password is not the empty string. */
297
static bool includes_credentials(const lxb_url_t *lexbor_uri)
298
0
{
299
0
  return lexbor_uri->username.length > 0 || lexbor_uri->password.length > 0;
300
0
}
301
302
static zend_result php_uri_parser_whatwg_username_read(void *uri, php_uri_component_read_mode read_mode, zval *retval)
303
0
{
304
0
  const lxb_url_t *lexbor_uri = uri;
305
306
0
  if (includes_credentials(lexbor_uri)) {
307
0
    ZVAL_STRINGL_FAST(retval, (const char *) lexbor_uri->username.data, lexbor_uri->username.length);
308
0
  } else {
309
0
    ZVAL_NULL(retval);
310
0
  }
311
312
0
  return SUCCESS;
313
0
}
314
315
static zend_result php_uri_parser_whatwg_username_write(void *uri, const zval *value, zval *errors)
316
0
{
317
0
  lxb_url_t *lexbor_uri = uri;
318
0
  lexbor_str_t str = {0};
319
320
0
  zval_string_or_null_to_lexbor_str(value, &str);
321
322
0
  lxb_url_parser_clean(&lexbor_parser);
323
324
0
  if (lxb_url_api_username_set(lexbor_uri, str.data, str.length) != LXB_STATUS_OK) {
325
0
    throw_invalid_url_exception_during_write(errors, "username");
326
327
0
    return FAILURE;
328
0
  }
329
330
0
  return SUCCESS;
331
0
}
332
333
static zend_result php_uri_parser_whatwg_password_read(void *uri, php_uri_component_read_mode read_mode, zval *retval)
334
0
{
335
0
  const lxb_url_t *lexbor_uri = uri;
336
337
0
  if (includes_credentials(lexbor_uri)) {
338
0
    ZVAL_STRINGL_FAST(retval, (const char *) lexbor_uri->password.data, lexbor_uri->password.length);
339
0
  } else {
340
0
    ZVAL_NULL(retval);
341
0
  }
342
343
0
  return SUCCESS;
344
0
}
345
346
static zend_result php_uri_parser_whatwg_password_write(void *uri, const zval *value, zval *errors)
347
0
{
348
0
  lxb_url_t *lexbor_uri = uri;
349
0
  lexbor_str_t str = {0};
350
351
0
  zval_string_or_null_to_lexbor_str(value, &str);
352
353
0
  lxb_url_parser_clean(&lexbor_parser);
354
355
0
  if (lxb_url_api_password_set(lexbor_uri, str.data, str.length) != LXB_STATUS_OK) {
356
0
    throw_invalid_url_exception_during_write(errors, "password");
357
358
0
    return FAILURE;
359
0
  }
360
361
0
  return SUCCESS;
362
0
}
363
364
static zend_result php_uri_parser_whatwg_host_read(void *uri, const php_uri_component_read_mode read_mode, zval *retval)
365
0
{
366
0
  const lxb_url_t *lexbor_uri = uri;
367
368
0
  if (lexbor_uri->host.type == LXB_URL_HOST_TYPE_IPV4) {
369
0
    smart_str host_str = {0};
370
371
0
    lxb_url_serialize_host_ipv4(lexbor_uri->host.u.ipv4, serialize_to_smart_str_callback, &host_str);
372
0
    if (ZEND_HAS_OOM()) {
373
0
      smart_str_free(&host_str);
374
0
      return FAILURE;
375
0
    }
376
377
0
    ZVAL_NEW_STR(retval, smart_str_extract(&host_str));
378
0
  } else if (lexbor_uri->host.type == LXB_URL_HOST_TYPE_IPV6) {
379
0
    smart_str host_str = {0};
380
381
0
    smart_str_appendc(&host_str, '[');
382
0
    lxb_url_serialize_host_ipv6(lexbor_uri->host.u.ipv6, serialize_to_smart_str_callback, &host_str);
383
0
    smart_str_appendc(&host_str, ']');
384
0
    if (ZEND_HAS_OOM()) {
385
0
      smart_str_free(&host_str);
386
0
      return FAILURE;
387
0
    }
388
389
0
    ZVAL_NEW_STR(retval, smart_str_extract(&host_str));
390
0
  } else if (lexbor_uri->host.type == LXB_URL_HOST_TYPE_EMPTY) {
391
0
    ZVAL_EMPTY_STRING(retval);
392
0
  } else if (lexbor_uri->host.type != LXB_URL_HOST_TYPE__UNDEF) {
393
0
    switch (read_mode) {
394
0
      case PHP_URI_COMPONENT_READ_MODE_NORMALIZED_UNICODE: {
395
0
        smart_str host_str = {0};
396
0
        lxb_url_serialize_host_unicode(&lexbor_idna, &lexbor_uri->host, serialize_to_smart_str_callback, &host_str);
397
0
        lxb_unicode_idna_clean(&lexbor_idna);
398
0
        if (ZEND_HAS_OOM()) {
399
0
          smart_str_free(&host_str);
400
0
          return FAILURE;
401
0
        }
402
403
0
        ZVAL_STR(retval, smart_str_extract(&host_str));
404
0
        break;
405
0
      }
406
0
      case PHP_URI_COMPONENT_READ_MODE_NORMALIZED_ASCII:
407
0
        ZEND_FALLTHROUGH;
408
0
      case PHP_URI_COMPONENT_READ_MODE_RAW:
409
0
        ZVAL_STRINGL(retval, (const char *) lexbor_uri->host.u.domain.data, lexbor_uri->host.u.domain.length);
410
0
        break;
411
0
      default: ZEND_UNREACHABLE();
412
0
    }
413
0
  } else {
414
0
    ZVAL_NULL(retval);
415
0
  }
416
417
0
  return SUCCESS;
418
0
}
419
420
ZEND_ATTRIBUTE_NONNULL void php_uri_parser_whatwg_host_type_read(const lxb_url_t *lexbor_uri, zval *retval)
421
0
{
422
0
  switch (lexbor_uri->host.type) {
423
0
    case LXB_URL_HOST_TYPE_IPV4:
424
0
      ZVAL_OBJ_COPY(retval, zend_enum_get_case_cstr(php_uri_ce_whatwg_url_host_type, "IpV4"));
425
0
      return;
426
0
    case LXB_URL_HOST_TYPE_IPV6:
427
0
      ZVAL_OBJ_COPY(retval, zend_enum_get_case_cstr(php_uri_ce_whatwg_url_host_type, "IpV6"));
428
0
      return;
429
0
    case LXB_URL_HOST_TYPE_DOMAIN:
430
0
      ZVAL_OBJ_COPY(retval, zend_enum_get_case_cstr(php_uri_ce_whatwg_url_host_type, "Domain"));
431
0
      return;
432
0
    case LXB_URL_HOST_TYPE_EMPTY:
433
0
      ZVAL_OBJ_COPY(retval, zend_enum_get_case_cstr(php_uri_ce_whatwg_url_host_type, "Empty"));
434
0
      return;
435
0
    case LXB_URL_HOST_TYPE_OPAQUE:
436
0
      ZVAL_OBJ_COPY(retval, zend_enum_get_case_cstr(php_uri_ce_whatwg_url_host_type, "Opaque"));
437
0
      return;
438
0
    case LXB_URL_HOST_TYPE__UNDEF:
439
0
      ZVAL_NULL(retval);
440
0
      return;
441
0
    default: ZEND_UNREACHABLE();
442
0
  }
443
0
}
444
445
static zend_result php_uri_parser_whatwg_host_write(void *uri, const zval *value, zval *errors)
446
0
{
447
0
  lxb_url_t *lexbor_uri = uri;
448
0
  lexbor_str_t str = {0};
449
450
0
  zval_string_or_null_to_lexbor_str(value, &str);
451
452
0
  lxb_url_parser_clean(&lexbor_parser);
453
454
0
  if (lxb_url_api_hostname_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) {
455
0
    throw_invalid_url_exception_during_write(errors, "host");
456
457
0
    return FAILURE;
458
0
  }
459
460
0
  return SUCCESS;
461
0
}
462
463
static zend_result php_uri_parser_whatwg_port_read(void *uri, const php_uri_component_read_mode read_mode, zval *retval)
464
0
{
465
0
  const lxb_url_t *lexbor_uri = uri;
466
467
0
  if (lexbor_uri->has_port) {
468
0
    ZVAL_LONG(retval, lexbor_uri->port);
469
0
  } else {
470
0
    ZVAL_NULL(retval);
471
0
  }
472
473
0
  return SUCCESS;
474
0
}
475
476
static zend_result php_uri_parser_whatwg_port_write(void *uri, const zval *value, zval *errors)
477
0
{
478
0
  lxb_url_t *lexbor_uri = uri;
479
0
  lexbor_str_t str = {0};
480
481
0
  if (UNEXPECTED(!zval_long_or_null_to_lexbor_str(value, &str))) {
482
0
    ZEND_ASSERT(ZEND_HAS_OOM());
483
0
    zend_handle_delayed_oom();
484
0
    return FAILURE;
485
0
  }
486
487
0
  lxb_url_parser_clean(&lexbor_parser);
488
489
0
  if (lxb_url_api_port_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) {
490
0
    throw_invalid_url_exception_during_write(errors, "port");
491
492
0
    return FAILURE;
493
0
  }
494
495
0
  return SUCCESS;
496
0
}
497
498
static zend_result php_uri_parser_whatwg_path_read(void *uri, const php_uri_component_read_mode read_mode, zval *retval)
499
0
{
500
0
  const lxb_url_t *lexbor_uri = uri;
501
502
0
  if (lexbor_uri->path.str.length > 0) {
503
0
    ZVAL_STRINGL(retval, (const char *) lexbor_uri->path.str.data, lexbor_uri->path.str.length);
504
0
  } else {
505
0
    ZVAL_EMPTY_STRING(retval);
506
0
  }
507
508
0
  return SUCCESS;
509
0
}
510
511
static zend_result php_uri_parser_whatwg_path_write(void *uri, const zval *value, zval *errors)
512
0
{
513
0
  lxb_url_t *lexbor_uri = uri;
514
0
  lexbor_str_t str = {0};
515
516
0
  zval_string_or_null_to_lexbor_str(value, &str);
517
518
0
  lxb_url_parser_clean(&lexbor_parser);
519
520
0
  if (lxb_url_api_pathname_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) {
521
0
    throw_invalid_url_exception_during_write(errors, "path");
522
523
0
    return FAILURE;
524
0
  }
525
526
0
  return SUCCESS;
527
0
}
528
529
static zend_result php_uri_parser_whatwg_query_read(void *uri, const php_uri_component_read_mode read_mode, zval *retval)
530
0
{
531
0
  const lxb_url_t *lexbor_uri = uri;
532
533
0
  if (lexbor_uri->query.data != NULL) {
534
0
    ZVAL_STRINGL(retval, (const char *) lexbor_uri->query.data, lexbor_uri->query.length);
535
0
  } else {
536
0
    ZVAL_NULL(retval);
537
0
  }
538
539
0
  return SUCCESS;
540
0
}
541
542
static zend_result php_uri_parser_whatwg_query_write(void *uri, const zval *value, zval *errors)
543
0
{
544
0
  lxb_url_t *lexbor_uri = uri;
545
0
  lexbor_str_t str = {0};
546
547
0
  zval_string_or_null_to_lexbor_str(value, &str);
548
549
0
  lxb_url_parser_clean(&lexbor_parser);
550
551
0
  if (lxb_url_api_search_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) {
552
0
    throw_invalid_url_exception_during_write(errors, "query string");
553
554
0
    return FAILURE;
555
0
  }
556
557
0
  return SUCCESS;
558
0
}
559
560
static zend_result php_uri_parser_whatwg_fragment_read(void *uri, const php_uri_component_read_mode read_mode, zval *retval)
561
0
{
562
0
  const lxb_url_t *lexbor_uri = uri;
563
564
0
  if (lexbor_uri->fragment.data != NULL) {
565
0
    ZVAL_STRINGL(retval, (const char *) lexbor_uri->fragment.data, lexbor_uri->fragment.length);
566
0
  } else {
567
0
    ZVAL_NULL(retval);
568
0
  }
569
570
0
  return SUCCESS;
571
0
}
572
573
static zend_result php_uri_parser_whatwg_fragment_write(void *uri, const zval *value, zval *errors)
574
0
{
575
0
  lxb_url_t *lexbor_uri = uri;
576
0
  lexbor_str_t str = {0};
577
578
0
  zval_string_or_null_to_lexbor_str(value, &str);
579
580
0
  lxb_url_parser_clean(&lexbor_parser);
581
582
0
  if (lxb_url_api_hash_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) {
583
0
    throw_invalid_url_exception_during_write(errors, "fragment");
584
585
0
    return FAILURE;
586
0
  }
587
588
0
  return SUCCESS;
589
0
}
590
591
PHP_RINIT_FUNCTION(uri_parser_whatwg)
592
306k
{
593
306k
  lxb_status_t status = lexbor_mraw_init(&lexbor_mraw, lexbor_mraw_byte_size);
594
306k
  if (status != LXB_STATUS_OK) {
595
0
    goto fail;
596
0
  }
597
598
306k
  status = lxb_url_parser_init(&lexbor_parser, &lexbor_mraw);
599
306k
  if (status != LXB_STATUS_OK) {
600
0
    goto fail;
601
0
  }
602
603
306k
  status = lxb_unicode_idna_init(&lexbor_idna);
604
306k
  if (status != LXB_STATUS_OK) {
605
0
    goto fail;
606
0
  }
607
608
306k
  memcpy(lexbor_custom_url_map, lxb_url_get_percent_encoding_map(), sizeof(lexbor_custom_url_map));
609
306k
  lexbor_custom_url_map['%'] = -1; /* % is percent-encoded */
610
611
306k
  return SUCCESS;
612
613
0
 fail:
614
615
  /* Unconditionally calling the _destroy() functions is
616
   * safe on a zeroed structure. */
617
0
  lxb_unicode_idna_destroy(&lexbor_idna, false);
618
0
  memset(&lexbor_idna, 0, sizeof(lexbor_idna));
619
0
  lxb_url_parser_destroy(&lexbor_parser, false);
620
0
  memset(&lexbor_parser, 0, sizeof(lexbor_parser));
621
0
  lexbor_mraw_destroy(&lexbor_mraw, false);
622
0
  memset(&lexbor_mraw, 0, sizeof(lexbor_mraw));
623
624
0
  return FAILURE;
625
306k
}
626
627
ZEND_MODULE_POST_ZEND_DEACTIVATE_D(uri_parser_whatwg)
628
306k
{
629
306k
  lxb_unicode_idna_destroy(&lexbor_idna, false);
630
306k
  memset(&lexbor_idna, 0, sizeof(lexbor_idna));
631
306k
  lxb_url_parser_destroy(&lexbor_parser, false);
632
306k
  memset(&lexbor_parser, 0, sizeof(lexbor_parser));
633
306k
  lexbor_mraw_destroy(&lexbor_mraw, false);
634
306k
  memset(&lexbor_mraw, 0, sizeof(lexbor_mraw));
635
636
306k
  return SUCCESS;
637
306k
}
638
639
lxb_url_t *php_uri_parser_whatwg_parse_ex(const char *uri_str, const size_t uri_str_len, const lxb_url_t *lexbor_base_url, zval *errors, const bool silent)
640
0
{
641
0
  lxb_url_parser_clean(&lexbor_parser);
642
643
0
  lxb_url_t *url = lxb_url_parse(&lexbor_parser, lexbor_base_url, (unsigned char *) uri_str, uri_str_len);
644
645
0
  if (ZEND_HAS_OOM()) {
646
0
    zend_handle_delayed_oom();
647
0
    return url;
648
0
  }
649
650
0
  if ((url == NULL && !silent) || errors != NULL) {
651
0
    zval err;
652
0
    const char *reason = fill_errors(&err);
653
0
    if (ZEND_HAS_OOM()) {
654
0
      zval_ptr_dtor(&err);
655
0
      zend_handle_delayed_oom();
656
657
0
      return url;
658
0
    }
659
660
0
    if (url == NULL && !silent) {
661
0
      zend_object *exception = zend_throw_exception_ex(php_uri_ce_whatwg_invalid_url_exception, 0, "The specified URI is malformed%s%s%s", reason ? " (" : "", reason ? reason : "", reason ? ")" : "");
662
0
      zend_update_property(exception->ce, exception, ZEND_STRL("errors"), &err);
663
0
    }
664
0
    if (errors != NULL) {
665
0
      zval_ptr_dtor(errors);
666
0
      ZVAL_COPY_VALUE(errors, &err);
667
0
    } else {
668
0
      zval_ptr_dtor(&err);
669
0
    }
670
0
  }
671
672
0
  return url;
673
0
}
674
675
static void *php_uri_parser_whatwg_parse(const char *uri_str, const size_t uri_str_len, const void *base_url, zval *errors, const bool silent)
676
0
{
677
0
  return php_uri_parser_whatwg_parse_ex(uri_str, uri_str_len, base_url, errors, silent);
678
0
}
679
680
static void *php_uri_parser_whatwg_clone(void *uri)
681
0
{
682
0
  const lxb_url_t *lexbor_uri = uri;
683
684
0
  return lxb_url_clone(lexbor_parser.mraw, lexbor_uri);
685
0
}
686
687
static zend_string *php_uri_parser_whatwg_to_string(void *uri, const php_uri_recomposition_mode recomposition_mode, const bool exclude_fragment)
688
0
{
689
0
  const lxb_url_t *lexbor_uri = uri;
690
0
  smart_str uri_str = {0};
691
692
0
  switch (recomposition_mode) {
693
0
    case PHP_URI_RECOMPOSITION_MODE_RAW_UNICODE:
694
0
      ZEND_FALLTHROUGH;
695
0
    case PHP_URI_RECOMPOSITION_MODE_NORMALIZED_UNICODE:
696
0
      lxb_url_serialize_idna(&lexbor_idna, lexbor_uri, serialize_to_smart_str_callback, &uri_str, exclude_fragment);
697
0
      lxb_unicode_idna_clean(&lexbor_idna);
698
0
      break;
699
0
    case PHP_URI_RECOMPOSITION_MODE_RAW_ASCII:
700
0
      ZEND_FALLTHROUGH;
701
0
    case PHP_URI_RECOMPOSITION_MODE_NORMALIZED_ASCII:
702
0
      lxb_url_serialize(lexbor_uri, serialize_to_smart_str_callback, &uri_str, exclude_fragment);
703
0
      break;
704
0
    default: ZEND_UNREACHABLE();
705
0
  }
706
707
0
  if (ZEND_HAS_OOM()) {
708
0
    smart_str_free(&uri_str);
709
0
    return NULL;
710
0
  }
711
712
0
  return smart_str_extract(&uri_str);
713
0
}
714
715
static zend_string *php_uri_parser_whatwg_percent_encode_component(const char *str, const size_t str_length, const lxb_url_map_type_t map, const bool space_as_plus)
716
0
{
717
0
  lexbor_str_t lexbor_str = {0};
718
719
0
  const lexbor_status_t status = lxb_url_percent_encode_utf_8(
720
0
    (lxb_char_t *) str, str_length, &lexbor_str, lexbor_parser.mraw, lexbor_custom_url_map, map, space_as_plus
721
0
  );
722
723
0
  if (status != LXB_STATUS_OK) {
724
0
    lexbor_str_destroy(&lexbor_str, lexbor_parser.mraw, false);
725
0
    return NULL;
726
0
  }
727
728
0
  zend_string *result = zend_string_init((const char *) lexbor_str.data, lexbor_str.length, false);
729
730
0
  lexbor_str_destroy(&lexbor_str, lexbor_parser.mraw, false);
731
732
0
  return result;
733
0
}
734
735
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_userinfo_percent_encode(const char *str, const size_t str_length)
736
0
{
737
0
  return php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_USERINFO, false);
738
0
}
739
740
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_opaque_host_percent_encode(const char *str, const size_t str_length)
741
0
{
742
0
  return php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_C0, false);
743
0
}
744
745
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_path_percent_encode(const char *str, const size_t str_length)
746
0
{
747
0
  return php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_PATH, false);
748
0
}
749
750
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_opaque_path_percent_encode(const char *str, const size_t str_length)
751
0
{
752
0
  return php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_C0, false);
753
0
}
754
755
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_path_segment_percent_encode(const char *str, const size_t str_length)
756
0
{
757
0
  ZEND_ASSERT((lexbor_custom_url_map['/'] & LXB_URL_MAP_PATH) == 0);
758
759
0
  lexbor_custom_url_map['/'] |= LXB_URL_MAP_PATH;
760
761
0
  zend_string *result = php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_PATH, false);
762
763
0
  lexbor_custom_url_map['/'] &= ~LXB_URL_MAP_PATH;
764
765
0
  return result;
766
0
}
767
768
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_query_percent_encode(const char *str, const size_t str_length)
769
0
{
770
0
  return php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_QUERY, false);
771
0
}
772
773
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_special_query_percent_encode(const char *str, const size_t str_length)
774
0
{
775
0
  return php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_SPECIAL_QUERY, false);
776
0
}
777
778
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_form_query_percent_encode(const char *str, const size_t str_length)
779
0
{
780
0
  return php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_X_WWW_FORM, true);
781
0
}
782
783
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_fragment_percent_encode(const char *str, const size_t str_length)
784
0
{
785
0
  return php_uri_parser_whatwg_percent_encode_component(str, str_length, LXB_URL_MAP_FRAGMENT, false);
786
0
}
787
788
static void php_uri_parser_whatwg_destroy(void *uri)
789
201
{
790
201
  lxb_url_t *lexbor_uri = uri;
791
792
201
  lxb_url_destroy(lexbor_uri);
793
201
}
794
795
ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_throw_exception(const char *message)
796
0
{
797
0
  zend_object *exception = zend_throw_exception(php_uri_ce_whatwg_invalid_url_exception, message, 0);
798
0
  zval errors;
799
0
  ZVAL_EMPTY_ARRAY(&errors);
800
0
  zend_update_property(exception->ce, exception, ZEND_STRL("errors"), &errors);
801
0
}
802
803
ZEND_ATTRIBUTE_NONNULL static zend_always_inline zend_result php_uri_parser_whatwg_component_error(
804
  const char *component_name, const lxb_url_error_type_t error_type
805
0
) {
806
0
  zval errors;
807
0
  array_init(&errors);
808
809
0
  const char *reason = NULL;
810
0
  if (error_type != LXB_URL_ERROR_TYPE__LAST_ENTRY) {
811
0
    append_validation_error(Z_ARRVAL(errors), error_type, "", &reason);
812
0
  }
813
814
0
  zend_object *exception = zend_throw_exception_ex(php_uri_ce_whatwg_invalid_url_exception,
815
0
    0, "The specified %s is malformed%s%s%s", component_name,
816
0
    reason ? " (" : "", reason ? reason : "", reason ? ")" : "");
817
818
0
  zend_update_property(exception->ce, exception, ZEND_STRL("errors"), &errors);
819
0
  zval_ptr_dtor(&errors);
820
821
0
  return FAILURE;
822
0
}
823
824
ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_none_validate(const zend_string *component)
825
0
{
826
0
  return SUCCESS;
827
0
}
828
829
static zend_always_inline bool php_uri_whatwg_is_ascii_tab_or_newline(const unsigned char c)
830
0
{
831
0
  return c == '\t' || c == '\n' || c == '\r';
832
0
}
833
834
static zend_always_inline bool php_uri_parser_whatwg_is_alpha(const unsigned char c)
835
0
{
836
0
  return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
837
0
}
838
839
static zend_always_inline bool php_uri_parser_whatwg_is_digit(const unsigned char c)
840
0
{
841
0
  return c >= '0' && c <= '9';
842
0
}
843
844
static zend_always_inline unsigned char php_uri_ascii_to_lowercase(const unsigned char c)
845
0
{
846
0
  return c >= 'A' && c <= 'Z' ? (unsigned char) (c + ('a' - 'A')) : c;
847
0
}
848
849
ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_scheme_validate(const zend_string *scheme)
850
0
{
851
0
  const char *p = ZSTR_VAL(scheme);
852
0
  const char *end = p + ZSTR_LEN(scheme);
853
0
  bool seen_first = false;
854
855
0
  for (const char *c = p; c < end; c++) {
856
0
    const unsigned char uc = (unsigned char) *c;
857
858
0
    if (php_uri_whatwg_is_ascii_tab_or_newline(uc)) {
859
0
      continue;
860
0
    }
861
862
0
    if (!seen_first) {
863
0
      if (!php_uri_parser_whatwg_is_alpha(uc)) {
864
0
        return php_uri_parser_whatwg_component_error("scheme", LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL);
865
0
      }
866
0
      seen_first = true;
867
0
    } else {
868
0
      if (!php_uri_parser_whatwg_is_alpha(uc) && !php_uri_parser_whatwg_is_digit(uc) && uc != '+' && uc != '-' && uc != '.') {
869
0
        return php_uri_parser_whatwg_component_error("scheme", LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL);
870
0
      }
871
0
    }
872
0
  }
873
874
0
  if (!seen_first) {
875
0
    return php_uri_parser_whatwg_component_error("scheme", LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL);
876
0
  }
877
878
0
  return SUCCESS;
879
0
}
880
881
ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_host_validate(const zend_string *host)
882
0
{
883
0
  if (ZSTR_LEN(host) == 0) {
884
    /* Skip validation - an empty string may or may not be a valid host depending on whether the URL is special */
885
0
    return SUCCESS;
886
0
  }
887
888
0
  const char *first = ZSTR_VAL(host);
889
0
  const char *last = ZSTR_VAL(host) + ZSTR_LEN(host);
890
891
0
  while (first < last && php_uri_whatwg_is_ascii_tab_or_newline((unsigned char) *first)) {
892
0
    first++;
893
0
  }
894
895
  /* Validate the entire host before the hostname setter can stop at a URL delimiter.
896
   * Backslash is a delimiter only for special URLs, but is also forbidden in opaque hosts.
897
   * https://url.spec.whatwg.org/#hostname-state
898
   * https://url.spec.whatwg.org/#opaque-host-parser */
899
0
  for (const char *p = first; p < last; p++) {
900
0
    if (*p == '/' || *p == '?' || *p == '#' || *p == '\\') {
901
0
      return php_uri_parser_whatwg_component_error("host", LXB_URL_ERROR_TYPE_HOST_INVALID_CODE_POINT);
902
0
    }
903
0
  }
904
905
0
  if (*first != '[') {
906
    /* Skip further validation - The host is not an IPv6 address */
907
0
    return SUCCESS;
908
0
  }
909
910
0
  while (first < last && php_uri_whatwg_is_ascii_tab_or_newline((unsigned char) *last)) {
911
0
    last--;
912
0
  }
913
914
0
  lxb_char_t *stripped_host = emalloc(last - first + 1);
915
0
  size_t stripped_host_len = 0;
916
917
0
  for (const char *i = first; i < last; i++) {
918
0
    const unsigned char uc = (unsigned char) *i;
919
920
0
    if (!php_uri_whatwg_is_ascii_tab_or_newline(uc)) {
921
0
      stripped_host[stripped_host_len++] = (lxb_char_t) uc;
922
0
    }
923
0
  }
924
0
  stripped_host[stripped_host_len] = '\0';
925
926
0
  uint16_t ipv6[8];
927
0
  const lxb_status_t result = lxb_url_parse_host_ipv6(
928
0
    &lexbor_parser,
929
0
    stripped_host,
930
0
    stripped_host_len,
931
0
    ipv6
932
0
  );
933
934
0
  efree(stripped_host);
935
936
0
  if (result != LXB_STATUS_OK) {
937
0
    lexbor_plog_entry_t *lxb_error;
938
939
0
    if (lexbor_parser.log != NULL && (lxb_error = lexbor_array_obj_pop(&lexbor_parser.log->list)) != NULL) {
940
0
      return php_uri_parser_whatwg_component_error("host", lxb_error->id);
941
0
    } else {
942
0
      return php_uri_parser_whatwg_component_error("host", LXB_URL_ERROR_TYPE__LAST_ENTRY);
943
0
    }
944
0
  }
945
946
0
  return SUCCESS;
947
0
}
948
949
ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_port_validate(const zend_long port)
950
0
{
951
0
  if (port  < 0) {
952
0
    return php_uri_parser_whatwg_component_error("port", LXB_URL_ERROR_TYPE_PORT_INVALID);
953
0
  }
954
955
0
  if (port > 65535) {
956
0
    return php_uri_parser_whatwg_component_error("port", LXB_URL_ERROR_TYPE_PORT_OUT_OF_RANGE);
957
0
  }
958
959
0
  return SUCCESS;
960
0
}
961
962
ZEND_ATTRIBUTE_NONNULL static lxb_url_scheme_type_t php_uri_parser_whatwg_get_special_scheme(const zend_string *scheme)
963
0
{
964
0
    const char *p = ZSTR_VAL(scheme);
965
0
    const char *end = p + ZSTR_LEN(scheme);
966
967
    /*
968
     * Create a normalized buffer from the scheme by leaving out tab and newline characters.
969
     * The longest special scheme "https" is 5 characters, therefore 6 bytes is enough to store.
970
     */
971
0
    char buf[6];
972
0
    size_t buf_len = 0;
973
974
0
    for (const char *c = p; c < end; c++) {
975
0
        const unsigned char uc = (unsigned char) *c;
976
977
0
        if (php_uri_whatwg_is_ascii_tab_or_newline(uc)) {
978
0
            continue;
979
0
        }
980
981
0
        if (buf_len == sizeof(buf) - 1) {
982
          /* Longer than any special scheme */
983
0
            return false;
984
0
        }
985
986
0
        buf[buf_len++] = (char) php_uri_ascii_to_lowercase(uc);
987
0
    }
988
989
0
    switch (buf_len) {
990
0
      case 2:
991
0
        if (memcmp(buf, "ws", 2) == 0) {
992
0
          return LXB_URL_SCHEMEL_TYPE_WS;
993
0
        }
994
995
0
        break;
996
0
        case 3:
997
0
        if (memcmp(buf, "ftp", 3) == 0) {
998
0
          return LXB_URL_SCHEMEL_TYPE_FTP;
999
0
        }
1000
1001
0
        if (memcmp(buf, "wss", 3) == 0) {
1002
0
          return LXB_URL_SCHEMEL_TYPE_WSS;
1003
0
        }
1004
1005
0
        break;
1006
0
      case 4:
1007
0
        if (memcmp(buf, "http", 4) == 0) {
1008
0
          return LXB_URL_SCHEMEL_TYPE_HTTP;
1009
0
        }
1010
1011
0
        if (memcmp(buf, "file", 4) == 0) {
1012
0
          return LXB_URL_SCHEMEL_TYPE_FILE;
1013
0
        }
1014
1015
0
            break;
1016
0
        case 5:
1017
0
        if (memcmp(buf, "https", 5) == 0) {
1018
0
          return LXB_URL_SCHEMEL_TYPE_HTTPS;
1019
0
        }
1020
1021
0
        break;
1022
0
  }
1023
1024
0
  return LXB_URL_SCHEMEL_TYPE__UNDEF;
1025
0
}
1026
1027
ZEND_ATTRIBUTE_NONNULL static const char *php_uri_parser_whatwg_build_errors(HashTable *errors)
1028
0
{
1029
0
  if (lexbor_parser.log == NULL || lexbor_plog_length(lexbor_parser.log) == 0) {
1030
0
    return NULL;
1031
0
  }
1032
1033
0
  return fill_errors_inner(errors);
1034
0
}
1035
1036
ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_into_exception(HashTable *errors)
1037
0
{
1038
  /* Include errors from earlier components in the exception raised by a later component. */
1039
0
  if (zend_hash_num_elements(errors) > 0 && EG(exception)
1040
0
    && instanceof_function(EG(exception)->ce, php_uri_ce_whatwg_invalid_url_exception)) {
1041
0
    zval rv;
1042
0
    zval *exception_errors = zend_read_property(php_uri_ce_whatwg_invalid_url_exception,
1043
0
      EG(exception), ZEND_STRL("errors"), true, &rv);
1044
0
    ZEND_ASSERT(Z_TYPE_P(exception_errors) == IS_ARRAY);
1045
1046
0
    zval *error;
1047
0
    ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(exception_errors), error) {
1048
0
      Z_TRY_ADDREF_P(error);
1049
0
      zend_hash_next_index_insert(errors, error);
1050
0
    } ZEND_HASH_FOREACH_END();
1051
1052
0
    zval_ptr_dtor(exception_errors);
1053
0
    GC_ADDREF(errors);
1054
0
    ZVAL_ARR(exception_errors, errors);
1055
0
  }
1056
0
}
1057
1058
ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_and_throw(const lxb_status_t status, const char *component, HashTable *errors)
1059
0
{
1060
0
  if (status != LXB_STATUS_OK) {
1061
0
    throw_invalid_url_exception_during_write(NULL, component);
1062
0
  } else {
1063
0
    php_uri_parser_whatwg_build_errors(errors);
1064
0
  }
1065
0
}
1066
1067
ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_resolve_reference_from_zval(
1068
  lxb_url_t *lexbor_base_url, const zval *scheme, const zval *username, const zval *password,
1069
  const zval *host, const zval *port, const zval *path, const zval *query, const zval *fragment,
1070
  zval *soft_errors_zv
1071
0
) {
1072
0
  if (Z_TYPE_P(host) == IS_STRING) {
1073
0
    if (lexbor_base_url->path.opaque) {
1074
0
      php_uri_parser_whatwg_component_error("host", LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL);
1075
0
      return NULL;
1076
0
    }
1077
1078
    /* A new authority inherits only the scheme, not the base URL's other components. */
1079
0
    zval base_scheme;
1080
0
    php_uri_parser_whatwg_scheme_read(lexbor_base_url, PHP_URI_COMPONENT_READ_MODE_NORMALIZED_ASCII, &base_scheme);
1081
0
    lxb_url_t *url = php_uri_parser_whatwg_build_from_zval(NULL, &base_scheme,
1082
0
      username, password, host, port, path, query, fragment, soft_errors_zv);
1083
0
    zval_ptr_dtor(&base_scheme);
1084
0
    return url;
1085
0
  }
1086
1087
  /* Credentials and ports require an authority in the reference itself. */
1088
0
  if (Z_TYPE_P(username) == IS_STRING && Z_STRLEN_P(username) > 0) {
1089
0
    php_uri_parser_whatwg_throw_exception("The specified URL cannot have username");
1090
0
    return NULL;
1091
0
  }
1092
1093
0
  if (Z_TYPE_P(password) == IS_STRING && Z_STRLEN_P(password) > 0) {
1094
0
    php_uri_parser_whatwg_throw_exception("The specified URL cannot have password");
1095
0
    return NULL;
1096
0
  }
1097
1098
0
  if (Z_TYPE_P(port) == IS_LONG) {
1099
0
    php_uri_parser_whatwg_throw_exception("The specified URL cannot have port");
1100
0
    return NULL;
1101
0
  }
1102
1103
0
  const char *first = Z_STRVAL_P(path);
1104
0
  const char *end = first + Z_STRLEN_P(path);
1105
0
  while (first < end && php_uri_whatwg_is_ascii_tab_or_newline(*first)) {
1106
0
    first++;
1107
0
  }
1108
1109
0
  lxb_status_t status;
1110
0
  zval errors;
1111
0
  array_init(&errors);
1112
1113
0
  lxb_url_t *lexbor_url = php_uri_parser_whatwg_clone(lexbor_base_url);
1114
0
  if (lexbor_url == NULL) {
1115
0
    zend_throw_exception(php_uri_ce_error, "Memory allocation error", 0);
1116
0
    goto failure;
1117
0
  }
1118
0
  if (lexbor_base_url->path.opaque && first == end
1119
0
    && (Z_TYPE_P(query) == IS_STRING || Z_TYPE_P(fragment) == IS_NULL)) {
1120
0
    php_uri_parser_whatwg_component_error(Z_TYPE_P(query) == IS_STRING ? "query" : "path",
1121
0
      LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL);
1122
0
    goto failure;
1123
0
  }
1124
1125
  /* Discard the base fragment; the reference fragment is applied below. */
1126
0
  lxb_url_fragment_set_null(lexbor_url);
1127
1128
0
  if (Z_STRLEN_P(path) > 0) {
1129
    /* Resolve relative paths against the base directory. Absolute paths use
1130
     * a path state so leading slashes cannot introduce a new authority. */
1131
0
    lxb_url_state_t state = LXB_URL_STATE_NO_SCHEME_STATE;
1132
0
    if (!lexbor_base_url->path.opaque && first < end
1133
0
      && (*first == '/' || (lxb_url_is_special(lexbor_base_url) && *first == '\\'))) {
1134
      /* A path beginning with // must not replace the authority. */
1135
0
      state = LXB_URL_STATE_PATH_START_STATE;
1136
0
      if (lexbor_base_url->scheme.type == LXB_URL_SCHEMEL_TYPE_FILE) {
1137
0
        const char *second = first + 1;
1138
0
        while (second < end && php_uri_whatwg_is_ascii_tab_or_newline(*second)) {
1139
0
          second++;
1140
0
        }
1141
0
        if (second == end || (*second != '/' && *second != '\\')) {
1142
0
          state = LXB_URL_STATE_FILE_STATE;
1143
0
        }
1144
0
      }
1145
0
    }
1146
1147
    /* Keep delimiters inside the path when entering the reference parser. */
1148
0
    smart_str reference = {0};
1149
0
    for (const char *p = Z_STRVAL_P(path); p < end; p++) {
1150
0
      if (*p == '?') {
1151
0
        smart_str_appends(&reference, "%3F");
1152
0
      } else if (*p == '#') {
1153
0
        smart_str_appends(&reference, "%23");
1154
0
        const char *reason;
1155
0
        append_validation_error(
1156
0
          Z_ARRVAL(errors), LXB_URL_ERROR_TYPE_INVALID_URL_UNIT, p, &reason
1157
0
        );
1158
0
      } else {
1159
0
        smart_str_appendc(&reference, *p);
1160
0
      }
1161
0
    }
1162
1163
    /* After removing tabs and newlines, the parser must see a fragment
1164
     * reference to accept an opaque base. Its actual value is applied below. */
1165
0
    if (lexbor_base_url->path.opaque && first == end
1166
0
      && Z_TYPE_P(query) == IS_NULL && Z_TYPE_P(fragment) != IS_NULL) {
1167
0
      smart_str_appendc(&reference, '#');
1168
0
    }
1169
1170
0
    zend_string *input = smart_str_extract(&reference);
1171
0
    lxb_url_path_set_null(lexbor_url);
1172
0
    lxb_url_parser_clean(&lexbor_parser);
1173
0
    status = lxb_url_parse_basic(&lexbor_parser, lexbor_url, lexbor_base_url,
1174
0
      (const lxb_char_t *) ZSTR_VAL(input), ZSTR_LEN(input), state, LXB_ENCODING_UTF_8);
1175
0
    php_uri_parser_whatwg_build_errors_and_throw(status, "path", Z_ARRVAL(errors));
1176
0
    zend_string_release(input);
1177
0
    if (status != LXB_STATUS_OK) {
1178
0
      goto failure;
1179
0
    }
1180
0
    if (first < end) {
1181
0
      lxb_url_query_set_null(lexbor_url);
1182
0
    }
1183
0
  }
1184
1185
0
  if (Z_TYPE_P(query) == IS_STRING) {
1186
0
    zend_result result = php_uri_parser_whatwg_query_write(lexbor_url, query, NULL);
1187
0
    php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1188
0
    if (result == FAILURE) {
1189
0
      goto failure;
1190
0
    }
1191
0
  }
1192
1193
0
  if (Z_TYPE_P(fragment) == IS_STRING) {
1194
0
    zend_result result = php_uri_parser_whatwg_fragment_write(lexbor_url, fragment, NULL);
1195
0
    php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1196
0
    if (result == FAILURE) {
1197
0
      goto failure;
1198
0
    }
1199
0
  }
1200
1201
0
  if (php_uri_pass_errors_by_ref_and_free(soft_errors_zv, Z_ARRVAL(errors)) == FAILURE) {
1202
    /* The errors array was already consumed; goto failure would destroy it again. */
1203
0
    lxb_url_destroy(lexbor_url);
1204
0
    return NULL;
1205
0
  }
1206
1207
0
  return lexbor_url;
1208
1209
0
failure:
1210
0
  php_uri_parser_whatwg_build_errors_into_exception(Z_ARRVAL(errors));
1211
0
  zval_ptr_dtor(&errors);
1212
0
  lxb_url_destroy(lexbor_url);
1213
0
  return NULL;
1214
0
}
1215
1216
ZEND_ATTRIBUTE_NONNULL static zend_result php_uri_parser_whatwg_build_path(
1217
  lxb_url_t *lexbor_url, const zval *path, const zval *query, const zval *fragment, HashTable *errors
1218
0
) {
1219
0
  zend_result result;
1220
0
  const char *path_start = Z_STRVAL_P(path);
1221
0
  const char *path_end = path_start + Z_STRLEN_P(path);
1222
0
  while (path_start < path_end && php_uri_whatwg_is_ascii_tab_or_newline(*path_start)) {
1223
0
    path_start++;
1224
0
  }
1225
1226
0
  if (lexbor_url->host.type == LXB_URL_HOST_TYPE__UNDEF && (path_start == path_end || *path_start != '/')) {
1227
    /* A pathname setter cannot parse an opaque path. Parse it directly,
1228
     * rejecting delimiters that would start a query or fragment. */
1229
0
    for (const char *p = Z_STRVAL_P(path); p < path_end; p++) {
1230
0
      if (*p == '?' || *p == '#') {
1231
0
        throw_invalid_url_exception_during_write(NULL, "path");
1232
1233
0
        return FAILURE;
1234
0
      }
1235
0
    }
1236
1237
0
    smart_str opaque_path = {0};
1238
0
    smart_str_append(&opaque_path, Z_STR_P(path));
1239
1240
    /* The opaque path parser needs the following delimiter to encode the
1241
     * immediately preceding space, while still reporting all space errors. */
1242
0
    if (Z_TYPE_P(query) == IS_STRING && Z_STRLEN_P(query) > 0) {
1243
0
      smart_str_appendc(&opaque_path, '?');
1244
0
    } else if (Z_TYPE_P(fragment) == IS_STRING && Z_STRLEN_P(fragment) > 0) {
1245
0
      smart_str_appendc(&opaque_path, '#');
1246
0
    }
1247
1248
0
    zend_string *input = smart_str_extract(&opaque_path);
1249
0
    lxb_url_parser_clean(&lexbor_parser);
1250
0
    const lxb_status_t status = lxb_url_parse_basic(&lexbor_parser, lexbor_url, NULL,
1251
0
      (const lxb_char_t *) ZSTR_VAL(input), ZSTR_LEN(input),
1252
0
      LXB_URL_STATE_OPAQUE_PATH_STATE, LXB_ENCODING_UTF_8);
1253
0
    result = status == LXB_STATUS_OK ? SUCCESS : FAILURE;
1254
0
    if (result == FAILURE) {
1255
0
      throw_invalid_url_exception_during_write(NULL, "path");
1256
0
    }
1257
0
    php_uri_parser_whatwg_build_errors(errors);
1258
0
    zend_string_release(input);
1259
0
  } else {
1260
0
    result = php_uri_parser_whatwg_path_write(lexbor_url, path, NULL);
1261
0
  }
1262
1263
0
  return result;
1264
0
}
1265
1266
ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_from_zval(
1267
  lxb_url_t *lexbor_base_url, const zval *scheme, const zval *username, const zval *password,
1268
  const zval *host, const zval *port, const zval *path, const zval *query, const zval *fragment,
1269
  zval *soft_errors_zv
1270
0
) {
1271
0
  lxb_url_parser_clean(&lexbor_parser);
1272
1273
0
  if (lexbor_base_url != NULL && Z_TYPE_P(scheme) == IS_STRING && Z_STRLEN_P(scheme) == 0) {
1274
0
    return php_uri_parser_whatwg_resolve_reference_from_zval(
1275
0
      lexbor_base_url, scheme, username, password, host, port, path, query, fragment, soft_errors_zv
1276
0
    );
1277
0
  }
1278
1279
0
  lxb_url_t *lexbor_url = lexbor_mraw_calloc(lexbor_parser.mraw, sizeof(*lexbor_url));
1280
0
  if (lexbor_url == NULL) {
1281
0
    zend_throw_exception(php_uri_ce_error, "Memory allocation error", 0);
1282
0
    return NULL;
1283
0
  }
1284
1285
0
  lexbor_url->mraw = lexbor_parser.mraw;
1286
1287
  /*
1288
   * The URL is initialized as LXB_URL_SCHEMEL_TYPE__UNDEF but this would prevent the scheme to be updated
1289
   * in case of non-special schemes due to https://github.com/php/php-src/blob/27d7b799c0a13578ee0506b428b8ddc209ffb010/ext/lexbor/lexbor/url/url.c#L1402
1290
   */
1291
0
  if (php_uri_parser_whatwg_get_special_scheme(Z_STR_P(scheme)) == LXB_URL_SCHEMEL_TYPE__UNDEF) {
1292
0
    lexbor_url->scheme.type = LXB_URL_SCHEMEL_TYPE__UNKNOWN;
1293
0
  }
1294
1295
0
  zval errors;
1296
0
  array_init(&errors);
1297
1298
0
  zend_result result = php_uri_parser_whatwg_scheme_write(lexbor_url, scheme, NULL);
1299
0
  php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1300
0
  if (result == FAILURE) {
1301
0
    goto failure;
1302
0
  }
1303
1304
  /* Set the host when provided or required by a special scheme (file allows an empty host).
1305
   * Otherwise, preserve the absent host so the path can be opaque. */
1306
0
  if (Z_TYPE_P(host) == IS_STRING || lxb_url_is_special(lexbor_url)) {
1307
0
    result = php_uri_parser_whatwg_host_write(lexbor_url, host, NULL);
1308
0
    php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1309
0
    if (result == FAILURE) {
1310
0
      goto failure;
1311
0
    }
1312
0
  }
1313
1314
0
  if (lexbor_url->host.type == LXB_URL_HOST_TYPE__UNDEF
1315
0
    || lexbor_url->host.type == LXB_URL_HOST_TYPE_EMPTY
1316
0
    || lexbor_url->scheme.type == LXB_URL_SCHEMEL_TYPE_FILE) {
1317
0
    if (Z_TYPE_P(username) == IS_STRING && Z_STRLEN_P(username) > 0) {
1318
0
      php_uri_parser_whatwg_throw_exception("The specified URL cannot have username");
1319
0
      goto failure;
1320
0
    }
1321
1322
0
    if (Z_TYPE_P(password) == IS_STRING && Z_STRLEN_P(password) > 0) {
1323
0
      php_uri_parser_whatwg_throw_exception("The specified URL cannot have password");
1324
0
      goto failure;
1325
0
    }
1326
1327
0
    if (Z_TYPE_P(port) != IS_NULL) {
1328
0
      php_uri_parser_whatwg_throw_exception("The specified URL cannot have port");
1329
0
      goto failure;
1330
0
    }
1331
0
  }
1332
1333
  /* Intentionally writing username after host to avoid error when the username is set but the host is missing */
1334
0
  result = php_uri_parser_whatwg_username_write(lexbor_url, username, NULL);
1335
0
  php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1336
0
  if (result == FAILURE) {
1337
0
    goto failure;
1338
0
  }
1339
1340
  /* Intentionally writing password after host to avoid error when the password is set but the password is missing */
1341
0
  result = php_uri_parser_whatwg_password_write(lexbor_url, password, NULL);
1342
0
  php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1343
0
  if (result == FAILURE) {
1344
0
    goto failure;
1345
0
  }
1346
1347
  /* Intentionally writing port after host to avoid error when the port is set but the host is missing */
1348
0
  result = php_uri_parser_whatwg_port_write(lexbor_url, port, NULL);
1349
0
  php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1350
0
  if (result == FAILURE) {
1351
0
    goto failure;
1352
0
  }
1353
1354
0
  result = php_uri_parser_whatwg_build_path(lexbor_url, path, query, fragment, Z_ARRVAL(errors));
1355
0
  php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1356
0
  if (result == FAILURE) {
1357
0
    goto failure;
1358
0
  }
1359
1360
0
  result = php_uri_parser_whatwg_query_write(lexbor_url, query, NULL);
1361
0
  php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1362
0
  if (result == FAILURE) {
1363
0
    goto failure;
1364
0
  }
1365
1366
0
  result = php_uri_parser_whatwg_fragment_write(lexbor_url, fragment, NULL);
1367
0
  php_uri_parser_whatwg_build_errors(Z_ARRVAL(errors));
1368
0
  if (result == FAILURE) {
1369
0
    goto failure;
1370
0
  }
1371
1372
0
  if (php_uri_pass_errors_by_ref_and_free(soft_errors_zv, Z_ARRVAL(errors)) == FAILURE) {
1373
    /* The errors array was already consumed; goto failure would destroy it again. */
1374
0
    lxb_url_destroy(lexbor_url);
1375
0
    return NULL;
1376
0
  }
1377
1378
0
  return lexbor_url;
1379
0
failure:
1380
0
  ZEND_ASSERT(EG(exception));
1381
0
  php_uri_parser_whatwg_build_errors_into_exception(Z_ARRVAL(errors));
1382
0
  zval_ptr_dtor(&errors);
1383
0
  lxb_url_destroy(lexbor_url);
1384
  return NULL;
1385
0
}
1386
1387
PHPAPI const php_uri_parser php_uri_parser_whatwg = {
1388
  .name = PHP_URI_PARSER_WHATWG,
1389
  .parse = php_uri_parser_whatwg_parse,
1390
  .clone = php_uri_parser_whatwg_clone,
1391
  .to_string = php_uri_parser_whatwg_to_string,
1392
  .destroy = php_uri_parser_whatwg_destroy,
1393
  {
1394
    .scheme = {.read = php_uri_parser_whatwg_scheme_read, .write = php_uri_parser_whatwg_scheme_write},
1395
    .username = {.read = php_uri_parser_whatwg_username_read, .write = php_uri_parser_whatwg_username_write},
1396
    .password = {.read = php_uri_parser_whatwg_password_read, .write = php_uri_parser_whatwg_password_write},
1397
    .host = {.read = php_uri_parser_whatwg_host_read, .write = php_uri_parser_whatwg_host_write},
1398
    .port = {.read = php_uri_parser_whatwg_port_read, .write = php_uri_parser_whatwg_port_write},
1399
    .path = {.read = php_uri_parser_whatwg_path_read, .write = php_uri_parser_whatwg_path_write},
1400
    .query = {.read = php_uri_parser_whatwg_query_read, .write = php_uri_parser_whatwg_query_write},
1401
    .fragment = {.read = php_uri_parser_whatwg_fragment_read, .write = php_uri_parser_whatwg_fragment_write},
1402
  }
1403
};