Coverage Report

Created: 2026-06-02 06:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/php-src/ext/standard/html.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: Rasmus Lerdorf <rasmus@php.net>                             |
12
   |          Jaakko Hyvätti <jaakko.hyvatti@iki.fi>                      |
13
   |          Wez Furlong    <wez@thebrainroom.com>                       |
14
   |          Gustavo Lopes  <cataphract@php.net>                         |
15
   +----------------------------------------------------------------------+
16
*/
17
18
/*
19
 * HTML entity resources:
20
 *
21
 * http://www.unicode.org/Public/MAPPINGS/OBSOLETE/UNI2SGML.TXT
22
 *
23
 * XHTML 1.0 DTD
24
 * http://www.w3.org/TR/2002/REC-xhtml1-20020801/dtds.html#h-A2
25
 *
26
 * From HTML 4.01 strict DTD:
27
 * http://www.w3.org/TR/html4/HTMLlat1.ent
28
 * http://www.w3.org/TR/html4/HTMLsymbol.ent
29
 * http://www.w3.org/TR/html4/HTMLspecial.ent
30
 *
31
 * HTML 5:
32
 * http://dev.w3.org/html5/spec/Overview.html#named-character-references
33
 */
34
35
#include "php.h"
36
#ifdef PHP_WIN32
37
#include "config.w32.h"
38
#else
39
#include <php_config.h>
40
#endif
41
#include "php_standard.h"
42
#include "SAPI.h"
43
#include <locale.h>
44
45
#include <zend_hash.h>
46
#include "html_tables.h"
47
48
/* Macro for disabling flag of translation of non-basic entities where this isn't supported.
49
 * Not appropriate for html_entity_decode/htmlspecialchars_decode */
50
611
#define LIMIT_ALL(all, doctype, charset) do { \
51
611
  (all) = (all) && !CHARSET_PARTIAL_SUPPORT((charset)) && ((doctype) != ENT_HTML_DOC_XML1); \
52
611
} while (0)
53
54
56.1k
#define MB_FAILURE(pos, advance) do { \
55
56.1k
  *cursor = pos + (advance); \
56
56.1k
  *status = FAILURE; \
57
56.1k
  return 0; \
58
56.1k
} while (0)
59
60
353k
#define CHECK_LEN(pos, chars_need) ((str_len - (pos)) >= (chars_need))
61
62
/* valid as single byte character or leading byte */
63
8.51k
#define utf8_lead(c)  ((c) < 0x80 || ((c) >= 0xC2 && (c) <= 0xF4))
64
/* whether it's actually valid depends on other stuff;
65
 * this macro cannot check for non-shortest forms, surrogates or
66
 * code points above 0x10FFFF */
67
30.5k
#define utf8_trail(c) ((c) >= 0x80 && (c) <= 0xBF)
68
69
22.3k
#define gb2312_lead(c) ((c) != 0x8E && (c) != 0x8F && (c) != 0xA0 && (c) != 0xFF)
70
573
#define gb2312_trail(c) ((c) >= 0xA1 && (c) <= 0xFE)
71
72
141
#define sjis_lead(c) ((c) != 0x80 && (c) != 0xA0 && (c) < 0xFD)
73
1.08k
#define sjis_trail(c) ((c) >= 0x40  && (c) != 0x7F && (c) < 0xFD)
74
75
/* {{{ get_default_charset */
76
258
static char *get_default_charset(void) {
77
258
  if (PG(internal_encoding) && PG(internal_encoding)[0]) {
78
0
    return PG(internal_encoding);
79
258
  } else if (SG(default_charset) && SG(default_charset)[0] ) {
80
258
    return SG(default_charset);
81
258
  }
82
0
  return NULL;
83
258
}
84
/* }}} */
85
86
/* {{{ get_next_char */
87
static inline unsigned int get_next_char(
88
    enum entity_charset charset,
89
    const unsigned char *str,
90
    size_t str_len,
91
    size_t *cursor,
92
    zend_result *status)
93
340k
{
94
340k
  size_t pos = *cursor;
95
340k
  unsigned int this_char = 0;
96
97
340k
  *status = SUCCESS;
98
340k
  assert(pos <= str_len);
99
100
340k
  if (!CHECK_LEN(pos, 1))
101
0
    MB_FAILURE(pos, 1);
102
103
340k
  switch (charset) {
104
255k
  case cs_utf_8:
105
255k
    {
106
      /* We'll follow strategy 2. from section 3.6.1 of UTR #36:
107
       * "In a reported illegal byte sequence, do not include any
108
       *  non-initial byte that encodes a valid character or is a leading
109
       *  byte for a valid sequence." */
110
255k
      unsigned char c;
111
255k
      c = str[pos];
112
255k
      if (c < 0x80) {
113
200k
        this_char = c;
114
200k
        pos++;
115
200k
      } else if (c < 0xc2) {
116
16.1k
        MB_FAILURE(pos, 1);
117
38.2k
      } else if (c < 0xe0) {
118
11.1k
        if (!CHECK_LEN(pos, 2))
119
16
          MB_FAILURE(pos, 1);
120
121
11.1k
        if (!utf8_trail(str[pos + 1])) {
122
9.39k
          MB_FAILURE(pos, utf8_lead(str[pos + 1]) ? 1 : 2);
123
9.39k
        }
124
1.72k
        this_char = ((c & 0x1f) << 6) | (str[pos + 1] & 0x3f);
125
1.72k
        if (this_char < 0x80) { /* non-shortest form */
126
0
          MB_FAILURE(pos, 2);
127
0
        }
128
1.72k
        pos += 2;
129
27.0k
      } else if (c < 0xf0) {
130
5.09k
        size_t avail = str_len - pos;
131
132
5.09k
        if (avail < 3 ||
133
5.07k
            !utf8_trail(str[pos + 1]) || !utf8_trail(str[pos + 2])) {
134
4.73k
          if (avail < 2 || utf8_lead(str[pos + 1]))
135
3.97k
            MB_FAILURE(pos, 1);
136
761
          else if (avail < 3 || utf8_lead(str[pos + 2]))
137
452
            MB_FAILURE(pos, 2);
138
309
          else
139
309
            MB_FAILURE(pos, 3);
140
4.73k
        }
141
142
361
        this_char = ((c & 0x0f) << 12) | ((str[pos + 1] & 0x3f) << 6) | (str[pos + 2] & 0x3f);
143
361
        if (this_char < 0x800) { /* non-shortest form */
144
12
          MB_FAILURE(pos, 3);
145
349
        } else if (this_char >= 0xd800 && this_char <= 0xdfff) { /* surrogate */
146
6
          MB_FAILURE(pos, 3);
147
6
        }
148
343
        pos += 3;
149
21.9k
      } else if (c < 0xf5) {
150
2.60k
        size_t avail = str_len - pos;
151
152
2.60k
        if (avail < 4 ||
153
2.57k
            !utf8_trail(str[pos + 1]) || !utf8_trail(str[pos + 2]) ||
154
2.40k
            !utf8_trail(str[pos + 3])) {
155
2.40k
          if (avail < 2 || utf8_lead(str[pos + 1]))
156
1.98k
            MB_FAILURE(pos, 1);
157
419
          else if (avail < 3 || utf8_lead(str[pos + 2]))
158
187
            MB_FAILURE(pos, 2);
159
232
          else if (avail < 4 || utf8_lead(str[pos + 3]))
160
99
            MB_FAILURE(pos, 3);
161
133
          else
162
133
            MB_FAILURE(pos, 4);
163
2.40k
        }
164
165
205
        this_char = ((c & 0x07) << 18) | ((str[pos + 1] & 0x3f) << 12) | ((str[pos + 2] & 0x3f) << 6) | (str[pos + 3] & 0x3f);
166
205
        if (this_char < 0x10000 || this_char > 0x10FFFF) { /* non-shortest form or outside range */
167
3
          MB_FAILURE(pos, 4);
168
3
        }
169
202
        pos += 4;
170
19.3k
      } else {
171
19.3k
        MB_FAILURE(pos, 1);
172
19.3k
      }
173
255k
    }
174
203k
    break;
175
176
203k
  case cs_big5:
177
    /* reference http://demo.icu-project.org/icu-bin/convexp?conv=big5 */
178
0
    {
179
0
      unsigned char c = str[pos];
180
0
      if (c >= 0x81 && c <= 0xFE) {
181
0
        unsigned char next;
182
0
        if (!CHECK_LEN(pos, 2))
183
0
          MB_FAILURE(pos, 1);
184
185
0
        next = str[pos + 1];
186
187
0
        if ((next >= 0x40 && next <= 0x7E) ||
188
0
            (next >= 0xA1 && next <= 0xFE)) {
189
0
          this_char = (c << 8) | next;
190
0
        } else {
191
0
          MB_FAILURE(pos, 1);
192
0
        }
193
0
        pos += 2;
194
0
      } else {
195
0
        this_char = c;
196
0
        pos += 1;
197
0
      }
198
0
    }
199
0
    break;
200
201
0
  case cs_big5hkscs:
202
0
    {
203
0
      unsigned char c = str[pos];
204
0
      if (c >= 0x81 && c <= 0xFE) {
205
0
        unsigned char next;
206
0
        if (!CHECK_LEN(pos, 2))
207
0
          MB_FAILURE(pos, 1);
208
209
0
        next = str[pos + 1];
210
211
0
        if ((next >= 0x40 && next <= 0x7E) ||
212
0
            (next >= 0xA1 && next <= 0xFE)) {
213
0
          this_char = (c << 8) | next;
214
0
        } else if (next != 0x80 && next != 0xFF) {
215
0
          MB_FAILURE(pos, 1);
216
0
        } else {
217
0
          MB_FAILURE(pos, 2);
218
0
        }
219
0
        pos += 2;
220
0
      } else {
221
0
        this_char = c;
222
0
        pos += 1;
223
0
      }
224
0
    }
225
0
    break;
226
227
22.4k
  case cs_gb2312: /* EUC-CN */
228
22.4k
    {
229
22.4k
      unsigned char c = str[pos];
230
22.4k
      if (c >= 0xA1 && c <= 0xFE) {
231
618
        unsigned char next;
232
618
        if (!CHECK_LEN(pos, 2))
233
45
          MB_FAILURE(pos, 1);
234
235
573
        next = str[pos + 1];
236
237
573
        if (gb2312_trail(next)) {
238
54
          this_char = (c << 8) | next;
239
519
        } else if (gb2312_lead(next)) {
240
396
          MB_FAILURE(pos, 1);
241
396
        } else {
242
123
          MB_FAILURE(pos, 2);
243
123
        }
244
54
        pos += 2;
245
21.8k
      } else if (gb2312_lead(c)) {
246
20.2k
        this_char = c;
247
20.2k
        pos += 1;
248
20.2k
      } else {
249
1.57k
        MB_FAILURE(pos, 1);
250
1.57k
      }
251
22.4k
    }
252
20.3k
    break;
253
254
20.3k
  case cs_sjis:
255
14.8k
    {
256
14.8k
      unsigned char c = str[pos];
257
14.8k
      if ((c >= 0x81 && c <= 0x9F) || (c >= 0xE0 && c <= 0xFC)) {
258
1.08k
        unsigned char next;
259
1.08k
        if (!CHECK_LEN(pos, 2))
260
0
          MB_FAILURE(pos, 1);
261
262
1.08k
        next = str[pos + 1];
263
264
1.08k
        if (sjis_trail(next)) {
265
948
          this_char = (c << 8) | next;
266
948
        } else if (sjis_lead(next)) {
267
87
          MB_FAILURE(pos, 1);
268
87
        } else {
269
54
          MB_FAILURE(pos, 2);
270
54
        }
271
948
        pos += 2;
272
13.7k
      } else if (c < 0x80 || (c >= 0xA1 && c <= 0xDF)) {
273
12.0k
        this_char = c;
274
12.0k
        pos += 1;
275
12.0k
      } else {
276
1.72k
        MB_FAILURE(pos, 1);
277
1.72k
      }
278
14.8k
    }
279
12.9k
    break;
280
281
12.9k
  case cs_eucjp:
282
0
    {
283
0
      unsigned char c = str[pos];
284
285
0
      if (c >= 0xA1 && c <= 0xFE) {
286
0
        unsigned next;
287
0
        if (!CHECK_LEN(pos, 2))
288
0
          MB_FAILURE(pos, 1);
289
0
        next = str[pos + 1];
290
291
0
        if (next >= 0xA1 && next <= 0xFE) {
292
          /* this a jis kanji char */
293
0
          this_char = (c << 8) | next;
294
0
        } else {
295
0
          MB_FAILURE(pos, (next != 0xA0 && next != 0xFF) ? 1 : 2);
296
0
        }
297
0
        pos += 2;
298
0
      } else if (c == 0x8E) {
299
0
        unsigned next;
300
0
        if (!CHECK_LEN(pos, 2))
301
0
          MB_FAILURE(pos, 1);
302
303
0
        next = str[pos + 1];
304
0
        if (next >= 0xA1 && next <= 0xDF) {
305
          /* JIS X 0201 kana */
306
0
          this_char = (c << 8) | next;
307
0
        } else {
308
0
          MB_FAILURE(pos, (next != 0xA0 && next != 0xFF) ? 1 : 2);
309
0
        }
310
0
        pos += 2;
311
0
      } else if (c == 0x8F) {
312
0
        size_t avail = str_len - pos;
313
314
0
        if (avail < 3 || !(str[pos + 1] >= 0xA1 && str[pos + 1] <= 0xFE) ||
315
0
            !(str[pos + 2] >= 0xA1 && str[pos + 2] <= 0xFE)) {
316
0
          if (avail < 2 || (str[pos + 1] != 0xA0 && str[pos + 1] != 0xFF))
317
0
            MB_FAILURE(pos, 1);
318
0
          else if (avail < 3 || (str[pos + 2] != 0xA0 && str[pos + 2] != 0xFF))
319
0
            MB_FAILURE(pos, 2);
320
0
          else
321
0
            MB_FAILURE(pos, 3);
322
0
        } else {
323
          /* JIS X 0212 hojo-kanji */
324
0
          this_char = (c << 16) | (str[pos + 1] << 8) | str[pos + 2];
325
0
        }
326
0
        pos += 3;
327
0
      } else if (c != 0xA0 && c != 0xFF) {
328
        /* character encoded in 1 code unit */
329
0
        this_char = c;
330
0
        pos += 1;
331
0
      } else {
332
0
        MB_FAILURE(pos, 1);
333
0
      }
334
0
    }
335
0
    break;
336
47.7k
  default:
337
    /* single-byte charsets */
338
47.7k
    this_char = str[pos++];
339
47.7k
    break;
340
340k
  }
341
342
284k
  *cursor = pos;
343
284k
  return this_char;
344
340k
}
345
/* }}} */
346
347
/* {{{ php_next_utf8_char
348
 * Public interface for get_next_char used with UTF-8 */
349
PHPAPI unsigned int php_next_utf8_char(
350
    const unsigned char *str,
351
    size_t str_len,
352
    size_t *cursor,
353
    zend_result *status)
354
13
{
355
13
  return get_next_char(cs_utf_8, str, str_len, cursor, status);
356
13
}
357
/* }}} */
358
359
/* {{{ entity_charset determine_charset
360
 * Returns the charset identifier based on an explicitly provided charset,
361
 * the internal_encoding and default_charset ini settings, or UTF-8 by default. */
362
static enum entity_charset determine_charset(const char *charset_hint, bool quiet)
363
790
{
364
790
  if (!charset_hint || !*charset_hint) {
365
258
    charset_hint = get_default_charset();
366
258
  }
367
368
790
  if (charset_hint && *charset_hint) {
369
790
    size_t len = strlen(charset_hint);
370
    /* now walk the charset map and look for the codeset */
371
16.8k
    for (size_t i = 0; i < sizeof(charset_map)/sizeof(charset_map[0]); i++) {
372
16.5k
      if (len == charset_map[i].codeset_len &&
373
1.95k
          zend_binary_strcasecmp(charset_hint, len, charset_map[i].codeset, len) == 0) {
374
483
        return charset_map[i].charset;
375
483
      }
376
16.5k
    }
377
378
307
    if (!quiet) {
379
307
      php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8",
380
307
          charset_hint);
381
307
    }
382
307
  }
383
384
307
  return cs_utf_8;
385
790
}
386
/* }}} */
387
388
/* {{{ php_utf32_utf8 */
389
static inline size_t php_utf32_utf8(unsigned char *buf, unsigned k)
390
0
{
391
0
  size_t retval = 0;
392
393
  /* assert(0x0 <= k <= 0x10FFFF); */
394
395
0
  if (k < 0x80) {
396
0
    buf[0] = k;
397
0
    retval = 1;
398
0
  } else if (k < 0x800) {
399
0
    buf[0] = 0xc0 | (k >> 6);
400
0
    buf[1] = 0x80 | (k & 0x3f);
401
0
    retval = 2;
402
0
  } else if (k < 0x10000) {
403
0
    buf[0] = 0xe0 | (k >> 12);
404
0
    buf[1] = 0x80 | ((k >> 6) & 0x3f);
405
0
    buf[2] = 0x80 | (k & 0x3f);
406
0
    retval = 3;
407
0
  } else {
408
0
    buf[0] = 0xf0 | (k >> 18);
409
0
    buf[1] = 0x80 | ((k >> 12) & 0x3f);
410
0
    buf[2] = 0x80 | ((k >> 6) & 0x3f);
411
0
    buf[3] = 0x80 | (k & 0x3f);
412
0
    retval = 4;
413
0
  }
414
  /* UTF-8 has been restricted to max 4 bytes since RFC 3629 */
415
416
0
  return retval;
417
0
}
418
/* }}} */
419
420
/* {{{ unimap_bsearc_cmp
421
 * Binary search of unicode code points in unicode <--> charset mapping.
422
 * Returns the code point in the target charset (whose mapping table was given) or 0 if
423
 * the unicode code point is not in the table.
424
 */
425
static inline unsigned char unimap_bsearch(const uni_to_enc *table, unsigned code_key_a, size_t num)
426
0
{
427
0
  const uni_to_enc *l = table,
428
0
           *h = &table[num-1],
429
0
           *m;
430
0
  unsigned short code_key;
431
432
  /* we have no mappings outside the BMP */
433
0
  if (code_key_a > 0xFFFFU)
434
0
    return 0;
435
436
0
  code_key = (unsigned short) code_key_a;
437
438
0
  while (l <= h) {
439
0
    m = l + (h - l) / 2;
440
0
    if (code_key < m->un_code_point)
441
0
      h = m - 1;
442
0
    else if (code_key > m->un_code_point)
443
0
      l = m + 1;
444
0
    else
445
0
      return m->cs_code;
446
0
  }
447
0
  return 0;
448
0
}
449
/* }}} */
450
451
/* {{{ map_from_unicode */
452
static inline zend_result map_from_unicode(unsigned code, enum entity_charset charset, unsigned *res)
453
2.46k
{
454
2.46k
  unsigned char found;
455
2.46k
  const uni_to_enc *table;
456
2.46k
  size_t table_size;
457
458
2.46k
  switch (charset) {
459
2.46k
  case cs_8859_1:
460
    /* identity mapping of code points to unicode */
461
2.46k
    if (code > 0xFF) {
462
0
      return FAILURE;
463
0
    }
464
2.46k
    *res = code;
465
2.46k
    break;
466
467
0
  case cs_8859_5:
468
0
    if (code <= 0xA0 || code == 0xAD /* soft hyphen */) {
469
0
      *res = code;
470
0
    } else if (code == 0x2116) {
471
0
      *res = 0xF0; /* numero sign */
472
0
    } else if (code == 0xA7) {
473
0
      *res = 0xFD; /* section sign */
474
0
    } else if (code >= 0x0401 && code <= 0x045F) {
475
0
      if (code == 0x040D || code == 0x0450 || code == 0x045D)
476
0
        return FAILURE;
477
0
      *res = code - 0x360;
478
0
    } else {
479
0
      return FAILURE;
480
0
    }
481
0
    break;
482
483
0
  case cs_8859_15:
484
0
    if (code < 0xA4 || (code > 0xBE && code <= 0xFF)) {
485
0
      *res = code;
486
0
    } else { /* between A4 and 0xBE */
487
0
      found = unimap_bsearch(unimap_iso885915,
488
0
        code, sizeof(unimap_iso885915) / sizeof(*unimap_iso885915));
489
0
      if (found)
490
0
        *res = found;
491
0
      else
492
0
        return FAILURE;
493
0
    }
494
0
    break;
495
496
0
  case cs_cp1252:
497
0
    if (code <= 0x7F || (code >= 0xA0 && code <= 0xFF)) {
498
0
      *res = code;
499
0
    } else {
500
0
      found = unimap_bsearch(unimap_win1252,
501
0
        code, sizeof(unimap_win1252) / sizeof(*unimap_win1252));
502
0
      if (found)
503
0
        *res = found;
504
0
      else
505
0
        return FAILURE;
506
0
    }
507
0
    break;
508
509
0
  case cs_macroman:
510
0
    if (code == 0x7F)
511
0
      return FAILURE;
512
0
    table = unimap_macroman;
513
0
    table_size = sizeof(unimap_macroman) / sizeof(*unimap_macroman);
514
0
    goto table_over_7F;
515
0
  case cs_cp1251:
516
0
    table = unimap_win1251;
517
0
    table_size = sizeof(unimap_win1251) / sizeof(*unimap_win1251);
518
0
    goto table_over_7F;
519
0
  case cs_koi8r:
520
0
    table = unimap_koi8r;
521
0
    table_size = sizeof(unimap_koi8r) / sizeof(*unimap_koi8r);
522
0
    goto table_over_7F;
523
0
  case cs_cp866:
524
0
    table = unimap_cp866;
525
0
    table_size = sizeof(unimap_cp866) / sizeof(*unimap_cp866);
526
527
0
table_over_7F:
528
0
    if (code <= 0x7F) {
529
0
      *res = code;
530
0
    } else {
531
0
      found = unimap_bsearch(table, code, table_size);
532
0
      if (found)
533
0
        *res = found;
534
0
      else
535
0
        return FAILURE;
536
0
    }
537
0
    break;
538
539
  /* from here on, only map the possible characters in the ASCII range.
540
   * to improve support here, it's a matter of building the unicode mappings.
541
   * See <http://www.unicode.org/Public/6.0.0/ucd/Unihan.zip> */
542
0
  case cs_sjis:
543
0
  case cs_eucjp:
544
    /* we interpret 0x5C as the Yen symbol. This is not universal.
545
     * See <http://www.w3.org/Submission/japanese-xml/#ambiguity_of_yen> */
546
0
    if (code >= 0x20 && code <= 0x7D) {
547
0
      if (code == 0x5C)
548
0
        return FAILURE;
549
0
      *res = code;
550
0
    } else {
551
0
      return FAILURE;
552
0
    }
553
0
    break;
554
555
0
  case cs_big5:
556
0
  case cs_big5hkscs:
557
0
  case cs_gb2312:
558
0
    if (code >= 0x20 && code <= 0x7D) {
559
0
      *res = code;
560
0
    } else {
561
0
      return FAILURE;
562
0
    }
563
0
    break;
564
565
0
  default:
566
0
    return FAILURE;
567
2.46k
  }
568
569
2.46k
  return SUCCESS;
570
2.46k
}
571
/* }}} */
572
573
/* {{{ */
574
static inline void map_to_unicode(unsigned code, const enc_to_uni *table, unsigned *res)
575
47.4k
{
576
  /* only single byte encodings are currently supported; assumed code <= 0xFF */
577
47.4k
  *res = table->inner[ENT_ENC_TO_UNI_STAGE1(code)]->uni_cp[ENT_ENC_TO_UNI_STAGE2(code)];
578
47.4k
}
579
/* }}} */
580
581
/* {{{ unicode_cp_is_allowed */
582
static inline int unicode_cp_is_allowed(unsigned uni_cp, int document_type)
583
93.1k
{
584
  /* XML 1.0        HTML 4.01     HTML 5
585
   * 0x09..0x0A     0x09..0x0A      0x09..0x0A
586
   * 0x0D         0x0D        0x0C..0x0D
587
   * 0x0020..0xD7FF   0x20..0x7E      0x20..0x7E
588
   *            0x00A0..0xD7FF    0x00A0..0xD7FF
589
   * 0xE000..0xFFFD   0xE000..0x10FFFF  0xE000..0xFDCF
590
   * 0x010000..0x10FFFF           0xFDF0..0x10FFFF (*)
591
   *
592
   * (*) exclude code points where ((code & 0xFFFF) >= 0xFFFE)
593
   *
594
   * References:
595
   * XML 1.0:   <http://www.w3.org/TR/REC-xml/#charsets>
596
   * HTML 4.01: <http://www.w3.org/TR/1999/PR-html40-19990824/sgml/sgmldecl.html>
597
   * HTML 5:    <http://dev.w3.org/html5/spec/Overview.html#preprocessing-the-input-stream>
598
   *
599
   * Not sure this is the relevant part for HTML 5, though. I opted to
600
   * disallow the characters that would result in a parse error when
601
   * preprocessing of the input stream. See also section 8.1.3.
602
   *
603
   * It's unclear if XHTML 1.0 allows C1 characters. I'll opt to apply to
604
   * XHTML 1.0 the same rules as for XML 1.0.
605
   * See <http://cmsmcq.com/2007/C1.xml>.
606
   */
607
608
93.1k
  switch (document_type) {
609
20.9k
  case ENT_HTML_DOC_HTML401:
610
20.9k
    return (uni_cp >= 0x20 && uni_cp <= 0x7E) ||
611
13.4k
      (uni_cp == 0x0A || uni_cp == 0x09 || uni_cp == 0x0D) ||
612
12.4k
      (uni_cp >= 0xA0 && uni_cp <= 0xD7FF) ||
613
9.14k
      (uni_cp >= 0xE000 && uni_cp <= 0x10FFFF);
614
14.5k
  case ENT_HTML_DOC_HTML5:
615
14.5k
    return (uni_cp >= 0x20 && uni_cp <= 0x7E) ||
616
7.39k
      (uni_cp >= 0x09 && uni_cp <= 0x0D && uni_cp != 0x0B) || /* form feed U+0C allowed */
617
6.99k
      (uni_cp >= 0xA0 && uni_cp <= 0xD7FF) ||
618
6.62k
      (uni_cp >= 0xE000 && uni_cp <= 0x10FFFF &&
619
12
        ((uni_cp & 0xFFFF) < 0xFFFE) && /* last two of each plane (nonchars) disallowed */
620
12
        (uni_cp < 0xFDD0 || uni_cp > 0xFDEF)); /* U+FDD0-U+FDEF (nonchars) disallowed */
621
44.6k
  case ENT_HTML_DOC_XHTML:
622
57.6k
  case ENT_HTML_DOC_XML1:
623
57.6k
    return (uni_cp >= 0x20 && uni_cp <= 0xD7FF) ||
624
31.3k
      (uni_cp == 0x0A || uni_cp == 0x09 || uni_cp == 0x0D) ||
625
29.1k
      (uni_cp >= 0xE000 && uni_cp <= 0x10FFFF && uni_cp != 0xFFFE && uni_cp != 0xFFFF);
626
0
  default:
627
0
    return 1;
628
93.1k
  }
629
93.1k
}
630
/* }}} */
631
632
/* {{{ unicode_cp_is_allowed */
633
static inline int numeric_entity_is_allowed(unsigned uni_cp, int document_type)
634
0
{
635
  /* less restrictive than unicode_cp_is_allowed */
636
0
  switch (document_type) {
637
0
  case ENT_HTML_DOC_HTML401:
638
    /* all non-SGML characters (those marked with UNUSED in DESCSET) should be
639
     * representable with numeric entities */
640
0
    return uni_cp <= 0x10FFFF;
641
0
  case ENT_HTML_DOC_HTML5:
642
    /* 8.1.4. The numeric character reference forms described above are allowed to
643
     * reference any Unicode code point other than U+0000, U+000D, permanently
644
     * undefined Unicode characters (noncharacters), and control characters other
645
     * than space characters (U+0009, U+000A, U+000C and U+000D) */
646
    /* seems to allow surrogate characters, then */
647
0
    return (uni_cp >= 0x20 && uni_cp <= 0x7E) ||
648
0
      (uni_cp >= 0x09 && uni_cp <= 0x0C && uni_cp != 0x0B) || /* form feed U+0C allowed, but not U+0D */
649
0
      (uni_cp >= 0xA0 && uni_cp <= 0x10FFFF &&
650
0
        ((uni_cp & 0xFFFF) < 0xFFFE) && /* last two of each plane (nonchars) disallowed */
651
0
        (uni_cp < 0xFDD0 || uni_cp > 0xFDEF)); /* U+FDD0-U+FDEF (nonchars) disallowed */
652
0
  case ENT_HTML_DOC_XHTML:
653
0
  case ENT_HTML_DOC_XML1:
654
    /* OTOH, XML 1.0 requires "character references to match the production for Char
655
     * See <http://www.w3.org/TR/REC-xml/#NT-CharRef> */
656
0
    return unicode_cp_is_allowed(uni_cp, document_type);
657
0
  default:
658
0
    return 1;
659
0
  }
660
0
}
661
/* }}} */
662
663
/* {{{ process_numeric_entity
664
 * Auxiliary function to traverse_for_entities.
665
 * On input, *buf should point to the first character after # and on output, it's the last
666
 * byte read, no matter if there was success or insuccess.
667
 */
668
static inline zend_result process_numeric_entity(const char **buf, unsigned *code_point)
669
0
{
670
0
  zend_long code_l;
671
0
  int hexadecimal = (**buf == 'x' || **buf == 'X'); /* TODO: XML apparently disallows "X" */
672
0
  char *endptr;
673
674
0
  if (hexadecimal)
675
0
    (*buf)++;
676
677
  /* strtol allows whitespace and other stuff in the beginning
678
    * we're not interested */
679
0
  if ((hexadecimal && !isxdigit((unsigned char)**buf)) ||
680
0
      (!hexadecimal && !isdigit((unsigned char)**buf))) {
681
0
    return FAILURE;
682
0
  }
683
684
0
  code_l = ZEND_STRTOL(*buf, &endptr, hexadecimal ? 16 : 10);
685
  /* we're guaranteed there were valid digits, so *endptr > buf */
686
0
  *buf = endptr;
687
688
0
  if (**buf != ';')
689
0
    return FAILURE;
690
691
  /* many more are invalid, but that depends on whether it's HTML
692
   * (and which version) or XML. */
693
0
  if (code_l > Z_L(0x10FFFF))
694
0
    return FAILURE;
695
696
0
  if (code_point != NULL)
697
0
    *code_point = (unsigned)code_l;
698
699
0
  return SUCCESS;
700
0
}
701
/* }}} */
702
703
/* {{{ process_named_entity */
704
static inline zend_result process_named_entity_html(const char **buf, const char **start, size_t *length)
705
9
{
706
9
  *start = *buf;
707
708
  /* "&" is represented by a 0x26 in all supported encodings. That means
709
   * the byte after represents a character or is the leading byte of a
710
   * sequence of 8-bit code units. If in the ranges below, it represents
711
   * necessarily an alpha character because none of the supported encodings
712
   * has an overlap with ASCII in the leading byte (only on the second one) */
713
18
  while ((**buf >= 'a' && **buf <= 'z') ||
714
9
      (**buf >= 'A' && **buf <= 'Z') ||
715
9
      (**buf >= '0' && **buf <= '9')) {
716
9
    (*buf)++;
717
9
  }
718
719
9
  if (**buf != ';')
720
6
    return FAILURE;
721
722
  /* cast to size_t OK as the quantity is always non-negative */
723
3
  *length = *buf - *start;
724
725
3
  if (*length == 0)
726
0
    return FAILURE;
727
728
3
  return SUCCESS;
729
3
}
730
/* }}} */
731
732
/* {{{ resolve_named_entity_html */
733
static zend_result resolve_named_entity_html(const char *start, size_t length, const entity_ht *ht, unsigned *uni_cp1, unsigned *uni_cp2)
734
2.50k
{
735
2.50k
  const entity_cp_map *s;
736
2.50k
  zend_ulong hash = zend_inline_hash_func(start, length);
737
738
2.50k
  s = ht->buckets[hash % ht->num_elems];
739
2.51k
  while (s->entity) {
740
2.48k
    if (s->entity_len == length) {
741
2.46k
      if (memcmp(start, s->entity, length) == 0) {
742
2.46k
        *uni_cp1 = s->codepoint1;
743
2.46k
        *uni_cp2 = s->codepoint2;
744
2.46k
        return SUCCESS;
745
2.46k
      }
746
2.46k
    }
747
12
    s++;
748
12
  }
749
33
  return FAILURE;
750
2.50k
}
751
/* }}} */
752
753
2.46k
static inline size_t write_octet_sequence(unsigned char *buf, enum entity_charset charset, unsigned code) {
754
  /* code is not necessarily a unicode code point */
755
2.46k
  switch (charset) {
756
0
  case cs_utf_8:
757
0
    return php_utf32_utf8(buf, code);
758
759
2.46k
  case cs_8859_1:
760
2.46k
  case cs_cp1252:
761
2.46k
  case cs_8859_15:
762
2.46k
  case cs_koi8r:
763
2.46k
  case cs_cp1251:
764
2.46k
  case cs_8859_5:
765
2.46k
  case cs_cp866:
766
2.46k
  case cs_macroman:
767
    /* single byte stuff */
768
2.46k
    *buf = code;
769
2.46k
    return 1;
770
771
0
  case cs_big5:
772
0
  case cs_big5hkscs:
773
0
  case cs_sjis:
774
0
  case cs_gb2312:
775
    /* we don't have complete unicode mappings for these yet in entity_decode,
776
     * and we opt to pass through the octet sequences for these in htmlentities
777
     * instead of converting to an int and then converting back. */
778
#if 0
779
    return php_mb2_int_to_char(buf, code);
780
#else
781
0
    ZEND_ASSERT(code <= 0xFFU);
782
0
    *buf = code;
783
0
    return 1;
784
0
#endif
785
786
0
  case cs_eucjp:
787
#if 0 /* idem */
788
    return php_mb2_int_to_char(buf, code);
789
#else
790
0
    ZEND_ASSERT(code <= 0xFFU);
791
0
    *buf = code;
792
0
    return 1;
793
0
#endif
794
795
0
  default:
796
0
    assert(0);
797
0
    return 0;
798
2.46k
  }
799
2.46k
}
800
801
/* {{{ traverse_for_entities
802
 * Auxiliary function to php_unescape_html_entities().
803
 * - The argument "all" determines if all numeric entities are decode or only those
804
 *   that correspond to quotes (depending on quote_style).
805
 */
806
/* maximum expansion (factor 1.2) for HTML 5 with &nGt; and &nLt; */
807
/* +2 is 1 because of rest (probably unnecessary), 1 because of terminating 0 */
808
132
#define TRAVERSE_FOR_ENTITIES_EXPAND_SIZE(oldlen) ((oldlen) + (oldlen) / 5 + 2)
809
static void traverse_for_entities(
810
  const zend_string *input,
811
  zend_string *output, /* should have allocated TRAVERSE_FOR_ENTITIES_EXPAND_SIZE(olden) */
812
  const int all,
813
  const int flags,
814
  const entity_ht *inv_map,
815
  const enum entity_charset charset)
816
132
{
817
132
  const char *current_ptr = ZSTR_VAL(input);
818
132
  const char *input_end   = current_ptr + ZSTR_LEN(input); /* terminator address */
819
132
  char *output_ptr    = ZSTR_VAL(output);
820
132
  const int doctype    = flags & ENT_HTML_DOC_TYPE_MASK;
821
822
2.64k
  while (current_ptr < input_end) {
823
2.64k
    const char *ampersand_ptr = memchr(current_ptr, '&', input_end - current_ptr);
824
2.64k
    if (!ampersand_ptr) {
825
132
      const size_t tail_len = input_end - current_ptr;
826
132
      if (tail_len > 0) {
827
132
        memcpy(output_ptr, current_ptr, tail_len);
828
132
        output_ptr += tail_len;
829
132
      }
830
132
      break;
831
132
    }
832
833
    /* Copy everything up to the found '&' */
834
2.50k
    const size_t chunk_len = ampersand_ptr - current_ptr;
835
2.50k
    if (chunk_len > 0) {
836
1.57k
      memcpy(output_ptr, current_ptr, chunk_len);
837
1.57k
      output_ptr += chunk_len;
838
1.57k
    }
839
840
    /* Now current_ptr points to the '&' character. */
841
2.50k
    current_ptr = ampersand_ptr;
842
843
    /* If there are less than 4 bytes remaining, there isn't enough for an entity - 
844
     * copy '&' as a normal character. */
845
2.50k
    if (input_end - current_ptr < 4) {
846
0
      const size_t remaining = input_end - current_ptr;
847
0
      memcpy(output_ptr, current_ptr, remaining);
848
0
      output_ptr += remaining;
849
0
      break;
850
0
    }
851
852
2.50k
    unsigned code = 0, code2 = 0;
853
2.50k
    const char *entity_end_ptr = NULL;
854
855
2.50k
    if (current_ptr[1] == '#') {
856
      /* Processing numeric entity */
857
0
      const char *num_start = current_ptr + 2;
858
0
      entity_end_ptr = num_start;
859
0
      if (process_numeric_entity(&entity_end_ptr, &code) == FAILURE) {
860
0
        goto invalid_incomplete_entity;
861
0
      }
862
0
      if (!all && (code > 63U || stage3_table_be_apos_00000[code].data.ent.entity == NULL)) {
863
        /* If we're in htmlspecialchars_decode, we're only decoding entities
864
         * that represent &, <, >, " and '. Is this one of them? */
865
0
        goto invalid_incomplete_entity;
866
0
      } else if (!unicode_cp_is_allowed(code, doctype) ||
867
0
             (doctype == ENT_HTML_DOC_HTML5 && code == 0x0D)) {
868
        /* are we allowed to decode this entity in this document type?
869
         * HTML 5 is the only that has a character that cannot be used in
870
         * a numeric entity but is allowed literally (U+000D). The
871
         * unoptimized version would be ... || !numeric_entity_is_allowed(code) */
872
0
        goto invalid_incomplete_entity;
873
0
      }
874
2.50k
    } else {
875
      /* Processing named entity */
876
2.50k
      const char *name_start = current_ptr + 1;
877
      /* Search for ';' */
878
2.50k
      const size_t max_search_len = MIN(LONGEST_ENTITY_LENGTH + 1, input_end - name_start);
879
2.50k
      const char *semi_colon_ptr = memchr(name_start, ';', max_search_len);
880
2.50k
      if (!semi_colon_ptr) {
881
9
        goto invalid_incomplete_entity;
882
2.49k
      } else {
883
2.49k
        const size_t name_len = semi_colon_ptr - name_start;
884
2.49k
        if (name_len == 0) {
885
0
          goto invalid_incomplete_entity;
886
2.49k
        } else {
887
2.49k
          if (resolve_named_entity_html(name_start, name_len, inv_map, &code, &code2) == FAILURE) {
888
33
            if (doctype == ENT_HTML_DOC_XHTML && name_len == 4 &&
889
0
              name_start[0] == 'a' && name_start[1] == 'p' &&
890
0
              name_start[2] == 'o' && name_start[3] == 's')
891
0
            {
892
              /* uses html4 inv_map, which doesn't include apos;. This is a
893
               * hack to support it */
894
0
              code = (unsigned)'\'';
895
33
            } else {
896
33
              goto invalid_incomplete_entity;
897
33
            }
898
33
          }
899
2.46k
          entity_end_ptr = semi_colon_ptr;
900
2.46k
        }
901
2.49k
      }
902
2.50k
    }
903
904
    /* At this stage the entity_end_ptr should be always set. */
905
2.46k
    ZEND_ASSERT(entity_end_ptr != NULL);
906
907
    /* Check if quotes are allowed for entities representing ' or " */
908
2.46k
    if ((code == '\'' && !(flags & ENT_HTML_QUOTE_SINGLE)) ||
909
2.46k
      (code == '"'  && !(flags & ENT_HTML_QUOTE_DOUBLE)))
910
0
    {
911
0
      goto invalid_complete_entity;
912
0
    }
913
914
    /* UTF-8 doesn't need mapping (ISO-8859-1 doesn't either, but
915
     * the call is needed to ensure the codepoint <= U+00FF)  */
916
2.46k
    if (charset != cs_utf_8) {
917
      /* replace unicode code point */
918
2.46k
      if (map_from_unicode(code, charset, &code) == FAILURE || code2 != 0) {
919
0
        goto invalid_complete_entity;
920
0
      }
921
2.46k
    }
922
923
    /* Write the parsed entity into the output buffer */
924
2.46k
    output_ptr += write_octet_sequence((unsigned char*)output_ptr, charset, code);
925
2.46k
    if (code2) {
926
0
      output_ptr += write_octet_sequence((unsigned char*)output_ptr, charset, code2);
927
0
    }
928
    /* Move current_ptr past the semicolon */
929
2.46k
    current_ptr = entity_end_ptr + 1;
930
2.46k
    continue;
931
932
42
invalid_incomplete_entity:
933
    /* If the entity is invalid at parse stage or entity_end_ptr was never found, copy '&' as normal */
934
42
    *output_ptr++ = *current_ptr++;
935
42
    continue;
936
937
0
invalid_complete_entity:
938
    /* If the entity became invalid after we found entity_end_ptr */
939
0
    if (entity_end_ptr) {
940
0
      const size_t len = entity_end_ptr - current_ptr;
941
0
      memcpy(output_ptr, current_ptr, len);
942
0
      output_ptr += len;
943
0
      current_ptr = entity_end_ptr;
944
0
    } else {
945
0
      *output_ptr++ = *current_ptr++;
946
0
    }
947
0
    continue;
948
2.46k
  }
949
950
132
  *output_ptr = '\0';
951
132
  ZSTR_LEN(output) = (size_t)(output_ptr - ZSTR_VAL(output));
952
132
}
953
/* }}} */
954
955
/* {{{ unescape_inverse_map */
956
static const entity_ht *unescape_inverse_map(int all, int flags)
957
141
{
958
141
  int document_type = flags & ENT_HTML_DOC_TYPE_MASK;
959
960
141
  if (all) {
961
9
    switch (document_type) {
962
9
    case ENT_HTML_DOC_HTML401:
963
9
    case ENT_HTML_DOC_XHTML: /* but watch out for &apos;...*/
964
9
      return &ent_ht_html4;
965
0
    case ENT_HTML_DOC_HTML5:
966
0
      return &ent_ht_html5;
967
0
    default:
968
0
      return &ent_ht_be_apos;
969
9
    }
970
132
  } else {
971
132
    switch (document_type) {
972
132
    case ENT_HTML_DOC_HTML401:
973
132
      return &ent_ht_be_noapos;
974
0
    default:
975
0
      return &ent_ht_be_apos;
976
132
    }
977
132
  }
978
141
}
979
/* }}} */
980
981
/* {{{ determine_entity_table
982
 * Entity table to use. Note that entity tables are defined in terms of
983
 * unicode code points */
984
static entity_table_opt determine_entity_table(int all, int doctype)
985
790
{
986
790
  entity_table_opt retval = {0};
987
988
790
  assert(!(doctype == ENT_HTML_DOC_XML1 && all));
989
990
790
  if (all) {
991
253
    retval.ms_table = (doctype == ENT_HTML_DOC_HTML5) ?
992
184
      entity_ms_table_html5 : entity_ms_table_html4;
993
537
  } else {
994
537
    retval.table = (doctype == ENT_HTML_DOC_HTML401) ?
995
352
      stage3_table_be_noapos_00000 : stage3_table_be_apos_00000;
996
537
  }
997
790
  return retval;
998
790
}
999
/* }}} */
1000
1001
/* {{{ php_unescape_html_entities
1002
 * The parameter "all" should be true to decode all possible entities, false to decode
1003
 * only the basic ones, i.e., those in basic_entities_ex + the numeric entities
1004
 * that correspond to quotes.
1005
 */
1006
PHPAPI zend_string *php_unescape_html_entities(zend_string *str, int all, int flags, const char *hint_charset)
1007
195
{
1008
195
  zend_string *ret;
1009
195
  enum entity_charset charset;
1010
195
  const entity_ht *inverse_map;
1011
195
  size_t new_size;
1012
1013
195
  if (!memchr(ZSTR_VAL(str), '&', ZSTR_LEN(str))) {
1014
63
    return zend_string_copy(str);
1015
63
  }
1016
1017
132
  if (all) {
1018
0
    charset = determine_charset(hint_charset, /* quiet */ false);
1019
132
  } else {
1020
132
    charset = cs_8859_1; /* charset shouldn't matter, use ISO-8859-1 for performance */
1021
132
  }
1022
1023
  /* don't use LIMIT_ALL! */
1024
1025
132
  new_size = TRAVERSE_FOR_ENTITIES_EXPAND_SIZE(ZSTR_LEN(str));
1026
132
  if (ZSTR_LEN(str) > new_size) {
1027
    /* overflow, refuse to do anything */
1028
0
    return zend_string_copy(str);
1029
0
  }
1030
1031
132
  ret = zend_string_alloc(new_size, 0);
1032
1033
132
  inverse_map = unescape_inverse_map(all, flags);
1034
1035
  /* replace numeric entities */
1036
132
  traverse_for_entities(str, ret, all, flags, inverse_map, charset);
1037
1038
132
  return ret;
1039
132
}
1040
/* }}} */
1041
1042
PHPAPI zend_string *php_escape_html_entities(const unsigned char *old, size_t oldlen, int all, int flags, const char *hint_charset)
1043
0
{
1044
0
  return php_escape_html_entities_ex(old, oldlen, all, flags, hint_charset, true, /* quiet */ false);
1045
0
}
1046
1047
/* {{{ find_entity_for_char */
1048
static inline void find_entity_for_char(
1049
  unsigned int k,
1050
  enum entity_charset charset,
1051
  const entity_stage1_row *table,
1052
  const unsigned char **entity,
1053
  size_t *entity_len,
1054
  const unsigned char *old,
1055
  size_t oldlen,
1056
  size_t *cursor)
1057
66.2k
{
1058
66.2k
  unsigned stage1_idx = ENT_STAGE1_INDEX(k);
1059
66.2k
  const entity_stage3_row *c;
1060
1061
66.2k
  if (stage1_idx > 0x1D) {
1062
97
    *entity     = NULL;
1063
97
    *entity_len = 0;
1064
97
    return;
1065
97
  }
1066
1067
66.1k
  c = &table[stage1_idx][ENT_STAGE2_INDEX(k)][ENT_STAGE3_INDEX(k)];
1068
1069
66.1k
  if (!c->ambiguous) {
1070
65.7k
    *entity     = (const unsigned char *)c->data.ent.entity;
1071
65.7k
    *entity_len = c->data.ent.entity_len;
1072
65.7k
  } else {
1073
    /* peek at next char */
1074
363
    size_t cursor_before = *cursor;
1075
363
    zend_result status = SUCCESS;
1076
363
    unsigned next_char;
1077
1078
363
    if (!(*cursor < oldlen))
1079
21
      goto no_suitable_2nd;
1080
1081
342
    next_char = get_next_char(charset, old, oldlen, cursor, &status);
1082
1083
342
    if (status == FAILURE)
1084
33
      goto no_suitable_2nd;
1085
1086
309
    {
1087
309
      const entity_multicodepoint_row *s, *e;
1088
1089
309
      s = &c->data.multicodepoint_table[1];
1090
309
      e = s - 1 + c->data.multicodepoint_table[0].leading_entry.size;
1091
      /* we could do a binary search but it's not worth it since we have
1092
       * at most two entries... */
1093
618
      for ( ; s <= e; s++) {
1094
309
        if (s->normal_entry.second_cp == next_char) {
1095
0
          *entity     = (const unsigned char *) s->normal_entry.entity;
1096
0
          *entity_len = s->normal_entry.entity_len;
1097
0
          return;
1098
0
        }
1099
309
      }
1100
309
    }
1101
363
no_suitable_2nd:
1102
363
    *cursor = cursor_before;
1103
363
    *entity = (const unsigned char *)
1104
363
      c->data.multicodepoint_table[0].leading_entry.default_entity;
1105
363
    *entity_len = c->data.multicodepoint_table[0].leading_entry.default_entity_len;
1106
363
  }
1107
66.1k
}
1108
/* }}} */
1109
1110
/* {{{ find_entity_for_char_basic */
1111
static inline void find_entity_for_char_basic(
1112
  unsigned int k,
1113
  const entity_stage3_row *table,
1114
  const unsigned char **entity,
1115
  size_t *entity_len)
1116
209k
{
1117
209k
  if (k >= 64U) {
1118
95.4k
    *entity     = NULL;
1119
95.4k
    *entity_len = 0;
1120
95.4k
    return;
1121
95.4k
  }
1122
1123
114k
  *entity     = (const unsigned char *) table[k].data.ent.entity;
1124
114k
  *entity_len = table[k].data.ent.entity_len;
1125
114k
}
1126
/* }}} */
1127
1128
/* {{{ php_escape_html_entities */
1129
PHPAPI zend_string *php_escape_html_entities_ex(const unsigned char *old, size_t oldlen, int all, int flags, const char *hint_charset, bool double_encode, bool quiet)
1130
790
{
1131
790
  size_t cursor, maxlen, len;
1132
790
  zend_string *replaced;
1133
790
  enum entity_charset charset = determine_charset(hint_charset, quiet);
1134
790
  int doctype = flags & ENT_HTML_DOC_TYPE_MASK;
1135
790
  entity_table_opt entity_table;
1136
790
  const enc_to_uni *to_uni_table = NULL;
1137
790
  const entity_ht *inv_map = NULL; /* used for !double_encode */
1138
  /* only used if flags includes ENT_HTML_IGNORE_ERRORS or ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS */
1139
790
  const unsigned char *replacement = NULL;
1140
790
  size_t replacement_len = 0;
1141
1142
790
  if (all) { /* replace with all named entities */
1143
611
    if (!quiet && CHARSET_PARTIAL_SUPPORT(charset)) {
1144
108
      php_error_docref(NULL, E_NOTICE, "Only basic entities "
1145
108
        "substitution is supported for multi-byte encodings other than UTF-8; "
1146
108
        "functionality is equivalent to htmlspecialchars");
1147
108
    }
1148
611
    LIMIT_ALL(all, doctype, charset);
1149
611
  }
1150
790
  entity_table = determine_entity_table(all, doctype);
1151
790
  if (all && !CHARSET_UNICODE_COMPAT(charset)) {
1152
117
    to_uni_table = enc_to_uni_index[charset];
1153
117
  }
1154
1155
790
  if (!double_encode) {
1156
    /* first arg is 1 because we want to identify valid named entities
1157
     * even if we are only encoding the basic ones */
1158
9
    inv_map = unescape_inverse_map(1, flags);
1159
9
  }
1160
1161
790
  if (flags & (ENT_HTML_SUBSTITUTE_ERRORS | ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS)) {
1162
772
    if (charset == cs_utf_8) {
1163
550
      replacement = (const unsigned char*)"\xEF\xBF\xBD";
1164
550
      replacement_len = sizeof("\xEF\xBF\xBD") - 1;
1165
550
    } else {
1166
222
      replacement = (const unsigned char*)"&#xFFFD;";
1167
222
      replacement_len = sizeof("&#xFFFD;") - 1;
1168
222
    }
1169
772
  }
1170
1171
  /* initial estimate */
1172
790
  if (oldlen < 64) {
1173
66
    maxlen = 128;
1174
724
  } else {
1175
724
    maxlen = zend_safe_addmult(oldlen, 2, 0, "html_entities");
1176
724
  }
1177
1178
790
  replaced = zend_string_alloc(maxlen, 0);
1179
790
  len = 0;
1180
790
  cursor = 0;
1181
340k
  while (cursor < oldlen) {
1182
339k
    const unsigned char *mbsequence = NULL;
1183
339k
    size_t mbseqlen         = 0,
1184
339k
           cursor_before      = cursor;
1185
339k
    zend_result status        = SUCCESS;
1186
339k
    unsigned int this_char      = get_next_char(charset, old, oldlen, &cursor, &status);
1187
1188
    /* guarantee we have at least 40 bytes to write.
1189
     * In HTML5, entities may take up to 33 bytes */
1190
339k
    if (len > maxlen - 40) { /* maxlen can never be smaller than 128 */
1191
1.97k
      replaced = zend_string_safe_realloc(replaced, maxlen, 1, 128, 0);
1192
1.97k
      maxlen += 128;
1193
1.97k
    }
1194
1195
339k
    if (status == FAILURE) {
1196
      /* invalid MB sequence */
1197
56.0k
      if (flags & ENT_HTML_IGNORE_ERRORS) {
1198
12.2k
        continue;
1199
43.7k
      } else if (flags & ENT_HTML_SUBSTITUTE_ERRORS) {
1200
43.5k
        memcpy(&ZSTR_VAL(replaced)[len], replacement, replacement_len);
1201
43.5k
        len += replacement_len;
1202
43.5k
        continue;
1203
43.5k
      } else {
1204
235
        zend_string_efree(replaced);
1205
235
        return ZSTR_EMPTY_ALLOC();
1206
235
      }
1207
283k
    } else { /* SUCCESS */
1208
283k
      mbsequence = &old[cursor_before];
1209
283k
      mbseqlen = cursor - cursor_before;
1210
283k
    }
1211
1212
283k
    if (this_char != '&') { /* no entity on this position */
1213
276k
      const unsigned char *rep  = NULL;
1214
276k
      size_t        rep_len = 0;
1215
1216
276k
      if (((this_char == '\'' && !(flags & ENT_HTML_QUOTE_SINGLE)) ||
1217
275k
          (this_char == '"' && !(flags & ENT_HTML_QUOTE_DOUBLE))))
1218
333
        goto pass_char_through;
1219
1220
275k
      if (all) { /* false that CHARSET_PARTIAL_SUPPORT(charset) */
1221
66.2k
        if (to_uni_table != NULL) {
1222
          /* !CHARSET_UNICODE_COMPAT therefore not UTF-8; since UTF-8
1223
           * is the only multibyte encoding with !CHARSET_PARTIAL_SUPPORT,
1224
           * we're using a single byte encoding */
1225
47.4k
          map_to_unicode(this_char, to_uni_table, &this_char);
1226
47.4k
          if (this_char == 0xFFFF) /* no mapping; pass through */
1227
0
            goto pass_char_through;
1228
47.4k
        }
1229
        /* the cursor may advance */
1230
66.2k
        find_entity_for_char(this_char, charset, entity_table.ms_table, &rep,
1231
66.2k
          &rep_len, old, oldlen, &cursor);
1232
209k
      } else {
1233
209k
        find_entity_for_char_basic(this_char, entity_table.table, &rep, &rep_len);
1234
209k
      }
1235
1236
275k
      if (rep != NULL) {
1237
16.0k
        ZSTR_VAL(replaced)[len++] = '&';
1238
16.0k
        memcpy(&ZSTR_VAL(replaced)[len], rep, rep_len);
1239
16.0k
        len += rep_len;
1240
16.0k
        ZSTR_VAL(replaced)[len++] = ';';
1241
259k
      } else {
1242
        /* we did not find an entity for this char.
1243
         * check for its validity, if its valid pass it unchanged */
1244
259k
        if (flags & ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS) {
1245
95.3k
          if (CHARSET_UNICODE_COMPAT(charset)) {
1246
27.9k
            if (!unicode_cp_is_allowed(this_char, doctype)) {
1247
11.5k
              mbsequence = replacement;
1248
11.5k
              mbseqlen = replacement_len;
1249
11.5k
            }
1250
67.4k
          } else if (to_uni_table) {
1251
35.1k
            if (!all) /* otherwise we already did this */
1252
0
              map_to_unicode(this_char, to_uni_table, &this_char);
1253
35.1k
            if (!unicode_cp_is_allowed(this_char, doctype)) {
1254
12.9k
              mbsequence = replacement;
1255
12.9k
              mbseqlen = replacement_len;
1256
12.9k
            }
1257
35.1k
          } else {
1258
            /* not a unicode code point, unless, coincidentally, it's in
1259
             * the 0x20..0x7D range (except 0x5C in sjis). We know nothing
1260
             * about other code points, because we have no tables. Since
1261
             * Unicode code points in that range are not disallowed in any
1262
             * document type, we could do nothing. However, conversion
1263
             * tables frequently map 0x00-0x1F to the respective C0 code
1264
             * points. Let's play it safe and admit that's the case */
1265
32.2k
            if (this_char <= 0x7D &&
1266
30.0k
                !unicode_cp_is_allowed(this_char, doctype)) {
1267
20.3k
              mbsequence = replacement;
1268
20.3k
              mbseqlen = replacement_len;
1269
20.3k
            }
1270
32.2k
          }
1271
95.3k
        }
1272
260k
pass_char_through:
1273
260k
        if (mbseqlen > 1) {
1274
48.0k
          memcpy(ZSTR_VAL(replaced) + len, mbsequence, mbseqlen);
1275
48.0k
          len += mbseqlen;
1276
212k
        } else {
1277
212k
          ZSTR_VAL(replaced)[len++] = mbsequence[0];
1278
212k
        }
1279
260k
      }
1280
275k
    } else { /* this_char == '&' */
1281
7.66k
      if (double_encode) {
1282
7.66k
encode_amp:
1283
7.66k
        memcpy(&ZSTR_VAL(replaced)[len], "&amp;", sizeof("&amp;") - 1);
1284
7.66k
        len += sizeof("&amp;") - 1;
1285
7.66k
      } else { /* no double encode */
1286
        /* check if entity is valid */
1287
9
        size_t ent_len; /* not counting & or ; */
1288
        /* peek at next char */
1289
9
        if (old[cursor] == '#') { /* numeric entity */
1290
0
          unsigned code_point;
1291
0
          int valid;
1292
0
          char *pos = (char*)&old[cursor+1];
1293
0
          valid = process_numeric_entity((const char **)&pos, &code_point);
1294
0
          if (valid == FAILURE)
1295
0
            goto encode_amp;
1296
0
          if (flags & ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS) {
1297
0
            if (!numeric_entity_is_allowed(code_point, doctype))
1298
0
              goto encode_amp;
1299
0
          }
1300
0
          ent_len = pos - (char*)&old[cursor];
1301
9
        } else { /* named entity */
1302
          /* check for vality of named entity */
1303
9
          const char *start = (const char *) &old[cursor],
1304
9
                 *next = start;
1305
9
          unsigned   dummy1, dummy2;
1306
1307
9
          if (process_named_entity_html(&next, &start, &ent_len) == FAILURE)
1308
6
            goto encode_amp;
1309
3
          if (resolve_named_entity_html(start, ent_len, inv_map, &dummy1, &dummy2) == FAILURE) {
1310
0
            if (!(doctype == ENT_HTML_DOC_XHTML && ent_len == 4 && start[0] == 'a'
1311
0
                  && start[1] == 'p' && start[2] == 'o' && start[3] == 's')) {
1312
              /* uses html4 inv_map, which doesn't include apos;. This is a
1313
               * hack to support it */
1314
0
              goto encode_amp;
1315
0
            }
1316
0
          }
1317
3
        }
1318
        /* checks passed; copy entity to result */
1319
        /* entity size is unbounded, we may need more memory */
1320
        /* at this point maxlen - len >= 40 */
1321
3
        if (maxlen - len < ent_len + 2 /* & and ; */) {
1322
          /* ent_len < oldlen, which is certainly <= SIZE_MAX/2 */
1323
0
          replaced = zend_string_safe_realloc(replaced, maxlen, 1, ent_len + 128, 0);
1324
0
          maxlen += ent_len + 128;
1325
0
        }
1326
3
        ZSTR_VAL(replaced)[len++] = '&';
1327
3
        memcpy(&ZSTR_VAL(replaced)[len], &old[cursor], ent_len);
1328
3
        len += ent_len;
1329
3
        ZSTR_VAL(replaced)[len++] = ';';
1330
3
        cursor += ent_len + 1;
1331
3
      }
1332
7.66k
    }
1333
283k
  }
1334
555
  ZSTR_VAL(replaced)[len] = '\0';
1335
555
  ZSTR_LEN(replaced) = len;
1336
1337
555
  return replaced;
1338
790
}
1339
/* }}} */
1340
1341
/* {{{ php_html_entities */
1342
static void php_html_entities(INTERNAL_FUNCTION_PARAMETERS, int all)
1343
790
{
1344
790
  zend_string *str, *hint_charset = NULL;
1345
790
  zend_long flags = ENT_QUOTES|ENT_SUBSTITUTE;
1346
790
  zend_string *replaced;
1347
790
  bool double_encode = 1;
1348
1349
2.37k
  ZEND_PARSE_PARAMETERS_START(1, 4)
1350
3.16k
    Z_PARAM_STR(str)
1351
790
    Z_PARAM_OPTIONAL
1352
2.80k
    Z_PARAM_LONG(flags)
1353
2.92k
    Z_PARAM_STR_OR_NULL(hint_charset)
1354
1.66k
    Z_PARAM_BOOL(double_encode);
1355
790
  ZEND_PARSE_PARAMETERS_END();
1356
1357
790
  if (ZSTR_LEN(str) == 0) {
1358
0
    RETURN_EMPTY_STRING();
1359
0
  }
1360
790
  replaced = php_escape_html_entities_ex(
1361
790
    (unsigned char*)ZSTR_VAL(str), ZSTR_LEN(str), all, (int) flags,
1362
790
    hint_charset ? ZSTR_VAL(hint_charset) : NULL, double_encode, /* quiet */ 0);
1363
790
  RETVAL_STR(replaced);
1364
790
}
1365
/* }}} */
1366
1367
/* {{{ Convert special characters to HTML entities */
1368
PHP_FUNCTION(htmlspecialchars)
1369
179
{
1370
179
  php_html_entities(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
1371
179
}
1372
/* }}} */
1373
1374
/* {{{ Convert special HTML entities back to characters */
1375
PHP_FUNCTION(htmlspecialchars_decode)
1376
195
{
1377
195
  zend_string *str;
1378
195
  zend_long quote_style = ENT_QUOTES|ENT_SUBSTITUTE;
1379
195
  zend_string *replaced;
1380
1381
585
  ZEND_PARSE_PARAMETERS_START(1, 2)
1382
780
    Z_PARAM_STR(str)
1383
195
    Z_PARAM_OPTIONAL
1384
390
    Z_PARAM_LONG(quote_style)
1385
195
  ZEND_PARSE_PARAMETERS_END();
1386
1387
195
  replaced = php_unescape_html_entities(str, 0 /*!all*/, (int)quote_style, NULL);
1388
195
  RETURN_STR(replaced);
1389
195
}
1390
/* }}} */
1391
1392
/* {{{ Convert all HTML entities to their applicable characters */
1393
PHP_FUNCTION(html_entity_decode)
1394
0
{
1395
0
  zend_string *str, *hint_charset = NULL;
1396
0
  zend_long quote_style = ENT_QUOTES|ENT_SUBSTITUTE;
1397
0
  zend_string *replaced;
1398
1399
0
  ZEND_PARSE_PARAMETERS_START(1, 3)
1400
0
    Z_PARAM_STR(str)
1401
0
    Z_PARAM_OPTIONAL
1402
0
    Z_PARAM_LONG(quote_style)
1403
0
    Z_PARAM_STR_OR_NULL(hint_charset)
1404
0
  ZEND_PARSE_PARAMETERS_END();
1405
1406
0
  replaced = php_unescape_html_entities(
1407
0
    str, 1 /*all*/, (int)quote_style, hint_charset ? ZSTR_VAL(hint_charset) : NULL);
1408
0
  RETURN_STR(replaced);
1409
0
}
1410
/* }}} */
1411
1412
1413
/* {{{ Convert all applicable characters to HTML entities */
1414
PHP_FUNCTION(htmlentities)
1415
611
{
1416
611
  php_html_entities(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
1417
611
}
1418
/* }}} */
1419
1420
/* {{{ write_s3row_data */
1421
static inline void write_s3row_data(
1422
  const entity_stage3_row *r,
1423
  unsigned orig_cp,
1424
  enum entity_charset charset,
1425
  zval *arr)
1426
0
{
1427
0
  char key[9] = ""; /* two unicode code points in UTF-8 */
1428
0
  char entity[LONGEST_ENTITY_LENGTH + 2] = {'&'};
1429
0
  size_t written_k1;
1430
1431
0
  written_k1 = write_octet_sequence((unsigned char*)key, charset, orig_cp);
1432
1433
0
  if (!r->ambiguous) {
1434
0
    size_t l = r->data.ent.entity_len;
1435
0
    memcpy(&entity[1], r->data.ent.entity, l);
1436
0
    entity[l + 1] = ';';
1437
0
    add_assoc_stringl_ex(arr, key, written_k1, entity, l + 2);
1438
0
  } else {
1439
0
    unsigned i,
1440
0
           num_entries;
1441
0
    const entity_multicodepoint_row *mcpr = r->data.multicodepoint_table;
1442
1443
0
    if (mcpr[0].leading_entry.default_entity != NULL) {
1444
0
      size_t l = mcpr[0].leading_entry.default_entity_len;
1445
0
      memcpy(&entity[1], mcpr[0].leading_entry.default_entity, l);
1446
0
      entity[l + 1] = ';';
1447
0
      add_assoc_stringl_ex(arr, key, written_k1, entity, l + 2);
1448
0
    }
1449
0
    num_entries = mcpr[0].leading_entry.size;
1450
0
    for (i = 1; i <= num_entries; i++) {
1451
0
      size_t   l,
1452
0
             written_k2;
1453
0
      unsigned uni_cp,
1454
0
           spe_cp;
1455
1456
0
      uni_cp = mcpr[i].normal_entry.second_cp;
1457
0
      l = mcpr[i].normal_entry.entity_len;
1458
1459
0
      if (!CHARSET_UNICODE_COMPAT(charset)) {
1460
0
        if (map_from_unicode(uni_cp, charset, &spe_cp) == FAILURE)
1461
0
          continue; /* non representable in this charset */
1462
0
      } else {
1463
0
        spe_cp = uni_cp;
1464
0
      }
1465
1466
0
      written_k2 = write_octet_sequence((unsigned char*)&key[written_k1], charset, spe_cp);
1467
0
      memcpy(&entity[1], mcpr[i].normal_entry.entity, l);
1468
0
      entity[l + 1] = ';';
1469
0
      add_assoc_stringl_ex(arr, key, written_k1 + written_k2, entity, l + 2);
1470
0
    }
1471
0
  }
1472
0
}
1473
/* }}} */
1474
1475
/* {{{ Returns the internal translation table used by htmlspecialchars and htmlentities */
1476
PHP_FUNCTION(get_html_translation_table)
1477
0
{
1478
0
  zend_long all = PHP_HTML_SPECIALCHARS,
1479
0
     flags = ENT_QUOTES|ENT_SUBSTITUTE;
1480
0
  int doctype;
1481
0
  entity_table_opt entity_table;
1482
0
  const enc_to_uni *to_uni_table = NULL;
1483
0
  char *charset_hint = NULL;
1484
0
  size_t charset_hint_len;
1485
0
  enum entity_charset charset;
1486
1487
  /* in this function we have to jump through some loops because we're
1488
   * getting the translated table from data structures that are optimized for
1489
   * random access, not traversal */
1490
1491
0
  ZEND_PARSE_PARAMETERS_START(0, 3)
1492
0
    Z_PARAM_OPTIONAL
1493
0
    Z_PARAM_LONG(all)
1494
0
    Z_PARAM_LONG(flags)
1495
0
    Z_PARAM_STRING(charset_hint, charset_hint_len)
1496
0
  ZEND_PARSE_PARAMETERS_END();
1497
1498
0
  charset = determine_charset(charset_hint, /* quiet */ 0);
1499
0
  doctype = flags & ENT_HTML_DOC_TYPE_MASK;
1500
0
  LIMIT_ALL(all, doctype, charset);
1501
1502
0
  array_init(return_value);
1503
1504
0
  entity_table = determine_entity_table((int)all, doctype);
1505
0
  if (all && !CHARSET_UNICODE_COMPAT(charset)) {
1506
0
    to_uni_table = enc_to_uni_index[charset];
1507
0
  }
1508
1509
0
  if (all) { /* PHP_HTML_ENTITIES (actually, any non-zero value for 1st param) */
1510
0
    const entity_stage1_row *ms_table = entity_table.ms_table;
1511
1512
0
    if (CHARSET_UNICODE_COMPAT(charset)) {
1513
0
      unsigned i, j, k,
1514
0
           max_i, max_j, max_k;
1515
      /* no mapping to unicode required */
1516
0
      if (CHARSET_SINGLE_BYTE(charset)) { /* ISO-8859-1 */
1517
0
        max_i = 1; max_j = 4; max_k = 64;
1518
0
      } else {
1519
0
        max_i = 0x1E; max_j = 64; max_k = 64;
1520
0
      }
1521
1522
0
      for (i = 0; i < max_i; i++) {
1523
0
        if (ms_table[i] == empty_stage2_table)
1524
0
          continue;
1525
0
        for (j = 0; j < max_j; j++) {
1526
0
          if (ms_table[i][j] == empty_stage3_table)
1527
0
            continue;
1528
0
          for (k = 0; k < max_k; k++) {
1529
0
            const entity_stage3_row *r = &ms_table[i][j][k];
1530
0
            unsigned code;
1531
1532
0
            if (r->data.ent.entity == NULL)
1533
0
              continue;
1534
1535
0
            code = ENT_CODE_POINT_FROM_STAGES(i, j, k);
1536
0
            if (((code == '\'' && !(flags & ENT_HTML_QUOTE_SINGLE)) ||
1537
0
                (code == '"' && !(flags & ENT_HTML_QUOTE_DOUBLE))))
1538
0
              continue;
1539
0
            write_s3row_data(r, code, charset, return_value);
1540
0
          }
1541
0
        }
1542
0
      }
1543
0
    } else {
1544
      /* we have to iterate through the set of code points for this
1545
       * encoding and map them to unicode code points */
1546
0
      unsigned i;
1547
0
      for (i = 0; i <= 0xFF; i++) {
1548
0
        const entity_stage3_row *r;
1549
0
        unsigned uni_cp;
1550
1551
        /* can be done before mapping, they're invariant */
1552
0
        if (((i == '\'' && !(flags & ENT_HTML_QUOTE_SINGLE)) ||
1553
0
            (i == '"' && !(flags & ENT_HTML_QUOTE_DOUBLE))))
1554
0
          continue;
1555
1556
0
        map_to_unicode(i, to_uni_table, &uni_cp);
1557
0
        r = &ms_table[ENT_STAGE1_INDEX(uni_cp)][ENT_STAGE2_INDEX(uni_cp)][ENT_STAGE3_INDEX(uni_cp)];
1558
0
        if (r->data.ent.entity == NULL)
1559
0
          continue;
1560
1561
0
        write_s3row_data(r, i, charset, return_value);
1562
0
      }
1563
0
    }
1564
0
  } else {
1565
    /* we could use sizeof(stage3_table_be_apos_00000) as well */
1566
0
    unsigned    j,
1567
0
            numelems = sizeof(stage3_table_be_noapos_00000) /
1568
0
              sizeof(*stage3_table_be_noapos_00000);
1569
1570
0
    for (j = 0; j < numelems; j++) {
1571
0
      const entity_stage3_row *r = &entity_table.table[j];
1572
0
      if (r->data.ent.entity == NULL)
1573
0
        continue;
1574
1575
0
      if (((j == '\'' && !(flags & ENT_HTML_QUOTE_SINGLE)) ||
1576
0
          (j == '"' && !(flags & ENT_HTML_QUOTE_DOUBLE))))
1577
0
        continue;
1578
1579
      /* charset is indifferent, used cs_8859_1 for efficiency */
1580
0
      write_s3row_data(r, j, cs_8859_1, return_value);
1581
0
    }
1582
0
  }
1583
0
}
1584
/* }}} */