Coverage Report

Created: 2026-10-28 06:20

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