Coverage Report

Created: 2026-06-02 06:36

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
63
#define LIMIT_ALL(all, doctype, charset) do { \
51
63
  (all) = (all) && !CHARSET_PARTIAL_SUPPORT((charset)) && ((doctype) != ENT_HTML_DOC_XML1); \
52
63
} while (0)
53
54
517
#define MB_FAILURE(pos, advance) do { \
55
517
  *cursor = pos + (advance); \
56
517
  *status = FAILURE; \
57
517
  return 0; \
58
517
} while (0)
59
60
4.73k
#define CHECK_LEN(pos, chars_need) ((str_len - (pos)) >= (chars_need))
61
62
/* valid as single byte character or leading byte */
63
69
#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
215
#define utf8_trail(c) ((c) >= 0x80 && (c) <= 0xBF)
68
69
418
#define gb2312_lead(c) ((c) != 0x8E && (c) != 0x8F && (c) != 0xA0 && (c) != 0xFF)
70
16
#define gb2312_trail(c) ((c) >= 0xA1 && (c) <= 0xFE)
71
72
0
#define sjis_lead(c) ((c) != 0x80 && (c) != 0xA0 && (c) < 0xFD)
73
0
#define sjis_trail(c) ((c) >= 0x40  && (c) != 0x7F && (c) < 0xFD)
74
75
/* {{{ get_default_charset */
76
6
static char *get_default_charset(void) {
77
6
  if (PG(internal_encoding) && PG(internal_encoding)[0]) {
78
0
    return PG(internal_encoding);
79
6
  } else if (SG(default_charset) && SG(default_charset)[0] ) {
80
6
    return SG(default_charset);
81
6
  }
82
0
  return NULL;
83
6
}
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
4.52k
{
94
4.52k
  size_t pos = *cursor;
95
4.52k
  unsigned int this_char = 0;
96
97
4.52k
  *status = SUCCESS;
98
4.52k
  assert(pos <= str_len);
99
100
4.52k
  if (!CHECK_LEN(pos, 1))
101
0
    MB_FAILURE(pos, 1);
102
103
4.52k
  switch (charset) {
104
1.55k
  case cs_utf_8:
105
1.55k
    {
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
1.55k
      unsigned char c;
111
1.55k
      c = str[pos];
112
1.55k
      if (c < 0x80) {
113
1.16k
        this_char = c;
114
1.16k
        pos++;
115
1.16k
      } else if (c < 0xc2) {
116
132
        MB_FAILURE(pos, 1);
117
264
      } else if (c < 0xe0) {
118
89
        if (!CHECK_LEN(pos, 2))
119
1
          MB_FAILURE(pos, 1);
120
121
88
        if (!utf8_trail(str[pos + 1])) {
122
75
          MB_FAILURE(pos, utf8_lead(str[pos + 1]) ? 1 : 2);
123
75
        }
124
13
        this_char = ((c & 0x1f) << 6) | (str[pos + 1] & 0x3f);
125
13
        if (this_char < 0x80) { /* non-shortest form */
126
0
          MB_FAILURE(pos, 2);
127
0
        }
128
13
        pos += 2;
129
175
      } else if (c < 0xf0) {
130
25
        size_t avail = str_len - pos;
131
132
25
        if (avail < 3 ||
133
24
            !utf8_trail(str[pos + 1]) || !utf8_trail(str[pos + 2])) {
134
24
          if (avail < 2 || utf8_lead(str[pos + 1]))
135
16
            MB_FAILURE(pos, 1);
136
8
          else if (avail < 3 || utf8_lead(str[pos + 2]))
137
3
            MB_FAILURE(pos, 2);
138
5
          else
139
5
            MB_FAILURE(pos, 3);
140
24
        }
141
142
1
        this_char = ((c & 0x0f) << 12) | ((str[pos + 1] & 0x3f) << 6) | (str[pos + 2] & 0x3f);
143
1
        if (this_char < 0x800) { /* non-shortest form */
144
0
          MB_FAILURE(pos, 3);
145
1
        } else if (this_char >= 0xd800 && this_char <= 0xdfff) { /* surrogate */
146
0
          MB_FAILURE(pos, 3);
147
0
        }
148
1
        pos += 3;
149
150
      } else if (c < 0xf5) {
150
23
        size_t avail = str_len - pos;
151
152
23
        if (avail < 4 ||
153
19
            !utf8_trail(str[pos + 1]) || !utf8_trail(str[pos + 2]) ||
154
22
            !utf8_trail(str[pos + 3])) {
155
22
          if (avail < 2 || utf8_lead(str[pos + 1]))
156
10
            MB_FAILURE(pos, 1);
157
12
          else if (avail < 3 || utf8_lead(str[pos + 2]))
158
4
            MB_FAILURE(pos, 2);
159
8
          else if (avail < 4 || utf8_lead(str[pos + 3]))
160
7
            MB_FAILURE(pos, 3);
161
1
          else
162
1
            MB_FAILURE(pos, 4);
163
22
        }
164
165
1
        this_char = ((c & 0x07) << 18) | ((str[pos + 1] & 0x3f) << 12) | ((str[pos + 2] & 0x3f) << 6) | (str[pos + 3] & 0x3f);
166
1
        if (this_char < 0x10000 || this_char > 0x10FFFF) { /* non-shortest form or outside range */
167
0
          MB_FAILURE(pos, 4);
168
0
        }
169
1
        pos += 4;
170
127
      } else {
171
127
        MB_FAILURE(pos, 1);
172
127
      }
173
1.55k
    }
174
1.17k
    break;
175
176
1.17k
  case cs_big5:
177
    /* reference http://demo.icu-project.org/icu-bin/convexp?conv=big5 */
178
1.07k
    {
179
1.07k
      unsigned char c = str[pos];
180
1.07k
      if (c >= 0x81 && c <= 0xFE) {
181
104
        unsigned char next;
182
104
        if (!CHECK_LEN(pos, 2))
183
0
          MB_FAILURE(pos, 1);
184
185
104
        next = str[pos + 1];
186
187
104
        if ((next >= 0x40 && next <= 0x7E) ||
188
102
            (next >= 0xA1 && next <= 0xFE)) {
189
9
          this_char = (c << 8) | next;
190
95
        } else {
191
95
          MB_FAILURE(pos, 1);
192
95
        }
193
9
        pos += 2;
194
969
      } else {
195
969
        this_char = c;
196
969
        pos += 1;
197
969
      }
198
1.07k
    }
199
978
    break;
200
201
978
  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
426
  case cs_gb2312: /* EUC-CN */
228
426
    {
229
426
      unsigned char c = str[pos];
230
426
      if (c >= 0xA1 && c <= 0xFE) {
231
16
        unsigned char next;
232
16
        if (!CHECK_LEN(pos, 2))
233
0
          MB_FAILURE(pos, 1);
234
235
16
        next = str[pos + 1];
236
237
16
        if (gb2312_trail(next)) {
238
8
          this_char = (c << 8) | next;
239
8
        } else if (gb2312_lead(next)) {
240
7
          MB_FAILURE(pos, 1);
241
7
        } else {
242
1
          MB_FAILURE(pos, 2);
243
1
        }
244
8
        pos += 2;
245
410
      } else if (gb2312_lead(c)) {
246
377
        this_char = c;
247
377
        pos += 1;
248
377
      } else {
249
33
        MB_FAILURE(pos, 1);
250
33
      }
251
426
    }
252
385
    break;
253
254
385
  case cs_sjis:
255
0
    {
256
0
      unsigned char c = str[pos];
257
0
      if ((c >= 0x81 && c <= 0x9F) || (c >= 0xE0 && c <= 0xFC)) {
258
0
        unsigned char next;
259
0
        if (!CHECK_LEN(pos, 2))
260
0
          MB_FAILURE(pos, 1);
261
262
0
        next = str[pos + 1];
263
264
0
        if (sjis_trail(next)) {
265
0
          this_char = (c << 8) | next;
266
0
        } else if (sjis_lead(next)) {
267
0
          MB_FAILURE(pos, 1);
268
0
        } else {
269
0
          MB_FAILURE(pos, 2);
270
0
        }
271
0
        pos += 2;
272
0
      } else if (c < 0x80 || (c >= 0xA1 && c <= 0xDF)) {
273
0
        this_char = c;
274
0
        pos += 1;
275
0
      } else {
276
0
        MB_FAILURE(pos, 1);
277
0
      }
278
0
    }
279
0
    break;
280
281
0
  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
1.46k
  default:
337
    /* single-byte charsets */
338
1.46k
    this_char = str[pos++];
339
1.46k
    break;
340
4.52k
  }
341
342
4.01k
  *cursor = pos;
343
4.01k
  return this_char;
344
4.52k
}
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
0
{
355
0
  return get_next_char(cs_utf_8, str, str_len, cursor, status);
356
0
}
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
63
{
364
63
  if (!charset_hint || !*charset_hint) {
365
6
    charset_hint = get_default_charset();
366
6
  }
367
368
63
  if (charset_hint && *charset_hint) {
369
63
    size_t len = strlen(charset_hint);
370
    /* now walk the charset map and look for the codeset */
371
1.53k
    for (size_t i = 0; i < sizeof(charset_map)/sizeof(charset_map[0]); i++) {
372
1.51k
      if (len == charset_map[i].codeset_len &&
373
160
          zend_binary_strcasecmp(charset_hint, len, charset_map[i].codeset, len) == 0) {
374
40
        return charset_map[i].charset;
375
40
      }
376
1.51k
    }
377
378
23
    if (!quiet) {
379
23
      php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8",
380
23
          charset_hint);
381
23
    }
382
23
  }
383
384
23
  return cs_utf_8;
385
63
}
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
0
{
454
0
  unsigned char found;
455
0
  const uni_to_enc *table;
456
0
  size_t table_size;
457
458
0
  switch (charset) {
459
0
  case cs_8859_1:
460
    /* identity mapping of code points to unicode */
461
0
    if (code > 0xFF) {
462
0
      return FAILURE;
463
0
    }
464
0
    *res = code;
465
0
    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
0
  }
568
569
0
  return SUCCESS;
570
0
}
571
/* }}} */
572
573
/* {{{ */
574
static inline void map_to_unicode(unsigned code, const enc_to_uni *table, unsigned *res)
575
1.26k
{
576
  /* only single byte encodings are currently supported; assumed code <= 0xFF */
577
1.26k
  *res = table->inner[ENT_ENC_TO_UNI_STAGE1(code)]->uni_cp[ENT_ENC_TO_UNI_STAGE2(code)];
578
1.26k
}
579
/* }}} */
580
581
/* {{{ unicode_cp_is_allowed */
582
static inline int unicode_cp_is_allowed(unsigned uni_cp, int document_type)
583
2.30k
{
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
2.30k
  switch (document_type) {
609
857
  case ENT_HTML_DOC_HTML401:
610
857
    return (uni_cp >= 0x20 && uni_cp <= 0x7E) ||
611
350
      (uni_cp == 0x0A || uni_cp == 0x09 || uni_cp == 0x0D) ||
612
273
      (uni_cp >= 0xA0 && uni_cp <= 0xD7FF) ||
613
245
      (uni_cp >= 0xE000 && uni_cp <= 0x10FFFF);
614
734
  case ENT_HTML_DOC_HTML5:
615
734
    return (uni_cp >= 0x20 && uni_cp <= 0x7E) ||
616
455
      (uni_cp >= 0x09 && uni_cp <= 0x0D && uni_cp != 0x0B) || /* form feed U+0C allowed */
617
423
      (uni_cp >= 0xA0 && uni_cp <= 0xD7FF) ||
618
407
      (uni_cp >= 0xE000 && uni_cp <= 0x10FFFF &&
619
0
        ((uni_cp & 0xFFFF) < 0xFFFE) && /* last two of each plane (nonchars) disallowed */
620
0
        (uni_cp < 0xFDD0 || uni_cp > 0xFDEF)); /* U+FDD0-U+FDEF (nonchars) disallowed */
621
489
  case ENT_HTML_DOC_XHTML:
622
710
  case ENT_HTML_DOC_XML1:
623
710
    return (uni_cp >= 0x20 && uni_cp <= 0xD7FF) ||
624
387
      (uni_cp == 0x0A || uni_cp == 0x09 || uni_cp == 0x0D) ||
625
326
      (uni_cp >= 0xE000 && uni_cp <= 0x10FFFF && uni_cp != 0xFFFE && uni_cp != 0xFFFF);
626
0
  default:
627
0
    return 1;
628
2.30k
  }
629
2.30k
}
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
1
{
706
1
  *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
3
  while ((**buf >= 'a' && **buf <= 'z') ||
714
1
      (**buf >= 'A' && **buf <= 'Z') ||
715
2
      (**buf >= '0' && **buf <= '9')) {
716
2
    (*buf)++;
717
2
  }
718
719
1
  if (**buf != ';')
720
0
    return FAILURE;
721
722
  /* cast to size_t OK as the quantity is always non-negative */
723
1
  *length = *buf - *start;
724
725
1
  if (*length == 0)
726
0
    return FAILURE;
727
728
1
  return SUCCESS;
729
1
}
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
1
{
735
1
  const entity_cp_map *s;
736
1
  zend_ulong hash = zend_inline_hash_func(start, length);
737
738
1
  s = ht->buckets[hash % ht->num_elems];
739
1
  while (s->entity) {
740
1
    if (s->entity_len == length) {
741
1
      if (memcmp(start, s->entity, length) == 0) {
742
1
        *uni_cp1 = s->codepoint1;
743
1
        *uni_cp2 = s->codepoint2;
744
1
        return SUCCESS;
745
1
      }
746
1
    }
747
0
    s++;
748
0
  }
749
0
  return FAILURE;
750
1
}
751
/* }}} */
752
753
0
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
0
  switch (charset) {
756
0
  case cs_utf_8:
757
0
    return php_utf32_utf8(buf, code);
758
759
0
  case cs_8859_1:
760
0
  case cs_cp1252:
761
0
  case cs_8859_15:
762
0
  case cs_koi8r:
763
0
  case cs_cp1251:
764
0
  case cs_8859_5:
765
0
  case cs_cp866:
766
0
  case cs_macroman:
767
    /* single byte stuff */
768
0
    *buf = code;
769
0
    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
0
  }
799
0
}
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
0
#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
0
{
817
0
  const char *current_ptr = ZSTR_VAL(input);
818
0
  const char *input_end   = current_ptr + ZSTR_LEN(input); /* terminator address */
819
0
  char *output_ptr    = ZSTR_VAL(output);
820
0
  const int doctype    = flags & ENT_HTML_DOC_TYPE_MASK;
821
822
0
  while (current_ptr < input_end) {
823
0
    const char *ampersand_ptr = memchr(current_ptr, '&', input_end - current_ptr);
824
0
    if (!ampersand_ptr) {
825
0
      const size_t tail_len = input_end - current_ptr;
826
0
      if (tail_len > 0) {
827
0
        memcpy(output_ptr, current_ptr, tail_len);
828
0
        output_ptr += tail_len;
829
0
      }
830
0
      break;
831
0
    }
832
833
    /* Copy everything up to the found '&' */
834
0
    const size_t chunk_len = ampersand_ptr - current_ptr;
835
0
    if (chunk_len > 0) {
836
0
      memcpy(output_ptr, current_ptr, chunk_len);
837
0
      output_ptr += chunk_len;
838
0
    }
839
840
    /* Now current_ptr points to the '&' character. */
841
0
    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
0
    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
0
    unsigned code = 0, code2 = 0;
853
0
    const char *entity_end_ptr = NULL;
854
855
0
    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
0
    } else {
875
      /* Processing named entity */
876
0
      const char *name_start = current_ptr + 1;
877
      /* Search for ';' */
878
0
      const size_t max_search_len = MIN(LONGEST_ENTITY_LENGTH + 1, input_end - name_start);
879
0
      const char *semi_colon_ptr = memchr(name_start, ';', max_search_len);
880
0
      if (!semi_colon_ptr) {
881
0
        goto invalid_incomplete_entity;
882
0
      } else {
883
0
        const size_t name_len = semi_colon_ptr - name_start;
884
0
        if (name_len == 0) {
885
0
          goto invalid_incomplete_entity;
886
0
        } else {
887
0
          if (resolve_named_entity_html(name_start, name_len, inv_map, &code, &code2) == FAILURE) {
888
0
            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
0
            } else {
896
0
              goto invalid_incomplete_entity;
897
0
            }
898
0
          }
899
0
          entity_end_ptr = semi_colon_ptr;
900
0
        }
901
0
      }
902
0
    }
903
904
    /* At this stage the entity_end_ptr should be always set. */
905
0
    ZEND_ASSERT(entity_end_ptr != NULL);
906
907
    /* Check if quotes are allowed for entities representing ' or " */
908
0
    if ((code == '\'' && !(flags & ENT_HTML_QUOTE_SINGLE)) ||
909
0
      (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
0
    if (charset != cs_utf_8) {
917
      /* replace unicode code point */
918
0
      if (map_from_unicode(code, charset, &code) == FAILURE || code2 != 0) {
919
0
        goto invalid_complete_entity;
920
0
      }
921
0
    }
922
923
    /* Write the parsed entity into the output buffer */
924
0
    output_ptr += write_octet_sequence((unsigned char*)output_ptr, charset, code);
925
0
    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
0
    current_ptr = entity_end_ptr + 1;
930
0
    continue;
931
932
0
invalid_incomplete_entity:
933
    /* If the entity is invalid at parse stage or entity_end_ptr was never found, copy '&' as normal */
934
0
    *output_ptr++ = *current_ptr++;
935
0
    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
0
  }
949
950
0
  *output_ptr = '\0';
951
0
  ZSTR_LEN(output) = (size_t)(output_ptr - ZSTR_VAL(output));
952
0
}
953
/* }}} */
954
955
/* {{{ unescape_inverse_map */
956
static const entity_ht *unescape_inverse_map(int all, int flags)
957
1
{
958
1
  int document_type = flags & ENT_HTML_DOC_TYPE_MASK;
959
960
1
  if (all) {
961
1
    switch (document_type) {
962
1
    case ENT_HTML_DOC_HTML401:
963
1
    case ENT_HTML_DOC_XHTML: /* but watch out for &apos;...*/
964
1
      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
1
    }
970
1
  } else {
971
0
    switch (document_type) {
972
0
    case ENT_HTML_DOC_HTML401:
973
0
      return &ent_ht_be_noapos;
974
0
    default:
975
0
      return &ent_ht_be_apos;
976
0
    }
977
0
  }
978
1
}
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
63
{
986
63
  entity_table_opt retval = {0};
987
988
63
  assert(!(doctype == ENT_HTML_DOC_XML1 && all));
989
990
63
  if (all) {
991
40
    retval.ms_table = (doctype == ENT_HTML_DOC_HTML5) ?
992
27
      entity_ms_table_html5 : entity_ms_table_html4;
993
40
  } else {
994
23
    retval.table = (doctype == ENT_HTML_DOC_HTML401) ?
995
15
      stage3_table_be_noapos_00000 : stage3_table_be_apos_00000;
996
23
  }
997
63
  return retval;
998
63
}
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
0
{
1008
0
  zend_string *ret;
1009
0
  enum entity_charset charset;
1010
0
  const entity_ht *inverse_map;
1011
0
  size_t new_size;
1012
1013
0
  if (!memchr(ZSTR_VAL(str), '&', ZSTR_LEN(str))) {
1014
0
    return zend_string_copy(str);
1015
0
  }
1016
1017
0
  if (all) {
1018
0
    charset = determine_charset(hint_charset, /* quiet */ false);
1019
0
  } else {
1020
0
    charset = cs_8859_1; /* charset shouldn't matter, use ISO-8859-1 for performance */
1021
0
  }
1022
1023
  /* don't use LIMIT_ALL! */
1024
1025
0
  new_size = TRAVERSE_FOR_ENTITIES_EXPAND_SIZE(ZSTR_LEN(str));
1026
0
  if (ZSTR_LEN(str) > new_size) {
1027
    /* overflow, refuse to do anything */
1028
0
    return zend_string_copy(str);
1029
0
  }
1030
1031
0
  ret = zend_string_alloc(new_size, 0);
1032
1033
0
  inverse_map = unescape_inverse_map(all, flags);
1034
1035
  /* replace numeric entities */
1036
0
  traverse_for_entities(str, ret, all, flags, inverse_map, charset);
1037
1038
0
  return ret;
1039
0
}
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
2.34k
{
1058
2.34k
  unsigned stage1_idx = ENT_STAGE1_INDEX(k);
1059
2.34k
  const entity_stage3_row *c;
1060
1061
2.34k
  if (stage1_idx > 0x1D) {
1062
0
    *entity     = NULL;
1063
0
    *entity_len = 0;
1064
0
    return;
1065
0
  }
1066
1067
2.34k
  c = &table[stage1_idx][ENT_STAGE2_INDEX(k)][ENT_STAGE3_INDEX(k)];
1068
1069
2.34k
  if (!c->ambiguous) {
1070
2.34k
    *entity     = (const unsigned char *)c->data.ent.entity;
1071
2.34k
    *entity_len = c->data.ent.entity_len;
1072
2.34k
  } else {
1073
    /* peek at next char */
1074
4
    size_t cursor_before = *cursor;
1075
4
    zend_result status = SUCCESS;
1076
4
    unsigned next_char;
1077
1078
4
    if (!(*cursor < oldlen))
1079
0
      goto no_suitable_2nd;
1080
1081
4
    next_char = get_next_char(charset, old, oldlen, cursor, &status);
1082
1083
4
    if (status == FAILURE)
1084
2
      goto no_suitable_2nd;
1085
1086
2
    {
1087
2
      const entity_multicodepoint_row *s, *e;
1088
1089
2
      s = &c->data.multicodepoint_table[1];
1090
2
      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
4
      for ( ; s <= e; s++) {
1094
2
        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
2
      }
1100
2
    }
1101
4
no_suitable_2nd:
1102
4
    *cursor = cursor_before;
1103
4
    *entity = (const unsigned char *)
1104
4
      c->data.multicodepoint_table[0].leading_entry.default_entity;
1105
4
    *entity_len = c->data.multicodepoint_table[0].leading_entry.default_entity_len;
1106
4
  }
1107
2.34k
}
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
1.60k
{
1117
1.60k
  if (k >= 64U) {
1118
979
    *entity     = NULL;
1119
979
    *entity_len = 0;
1120
979
    return;
1121
979
  }
1122
1123
626
  *entity     = (const unsigned char *) table[k].data.ent.entity;
1124
626
  *entity_len = table[k].data.ent.entity_len;
1125
626
}
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
63
{
1131
63
  size_t cursor, maxlen, len;
1132
63
  zend_string *replaced;
1133
63
  enum entity_charset charset = determine_charset(hint_charset, quiet);
1134
63
  int doctype = flags & ENT_HTML_DOC_TYPE_MASK;
1135
63
  entity_table_opt entity_table;
1136
63
  const enc_to_uni *to_uni_table = NULL;
1137
63
  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
63
  const unsigned char *replacement = NULL;
1140
63
  size_t replacement_len = 0;
1141
1142
63
  if (all) { /* replace with all named entities */
1143
63
    if (!quiet && CHARSET_PARTIAL_SUPPORT(charset)) {
1144
16
      php_error_docref(NULL, E_NOTICE, "Only basic entities "
1145
16
        "substitution is supported for multi-byte encodings other than UTF-8; "
1146
16
        "functionality is equivalent to htmlspecialchars");
1147
16
    }
1148
63
    LIMIT_ALL(all, doctype, charset);
1149
63
  }
1150
63
  entity_table = determine_entity_table(all, doctype);
1151
63
  if (all && !CHARSET_UNICODE_COMPAT(charset)) {
1152
15
    to_uni_table = enc_to_uni_index[charset];
1153
15
  }
1154
1155
63
  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
1
    inv_map = unescape_inverse_map(1, flags);
1159
1
  }
1160
1161
63
  if (flags & (ENT_HTML_SUBSTITUTE_ERRORS | ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS)) {
1162
52
    if (charset == cs_utf_8) {
1163
23
      replacement = (const unsigned char*)"\xEF\xBF\xBD";
1164
23
      replacement_len = sizeof("\xEF\xBF\xBD") - 1;
1165
29
    } else {
1166
29
      replacement = (const unsigned char*)"&#xFFFD;";
1167
29
      replacement_len = sizeof("&#xFFFD;") - 1;
1168
29
    }
1169
52
  }
1170
1171
  /* initial estimate */
1172
63
  if (oldlen < 64) {
1173
28
    maxlen = 128;
1174
35
  } else {
1175
35
    maxlen = zend_safe_addmult(oldlen, 2, 0, "html_entities");
1176
35
  }
1177
1178
63
  replaced = zend_string_alloc(maxlen, 0);
1179
63
  len = 0;
1180
63
  cursor = 0;
1181
4.57k
  while (cursor < oldlen) {
1182
4.52k
    const unsigned char *mbsequence = NULL;
1183
4.52k
    size_t mbseqlen         = 0,
1184
4.52k
           cursor_before      = cursor;
1185
4.52k
    zend_result status        = SUCCESS;
1186
4.52k
    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
4.52k
    if (len > maxlen - 40) { /* maxlen can never be smaller than 128 */
1191
57
      replaced = zend_string_safe_realloc(replaced, maxlen, 1, 128, 0);
1192
57
      maxlen += 128;
1193
57
    }
1194
1195
4.52k
    if (status == FAILURE) {
1196
      /* invalid MB sequence */
1197
515
      if (flags & ENT_HTML_IGNORE_ERRORS) {
1198
387
        continue;
1199
387
      } else if (flags & ENT_HTML_SUBSTITUTE_ERRORS) {
1200
119
        memcpy(&ZSTR_VAL(replaced)[len], replacement, replacement_len);
1201
119
        len += replacement_len;
1202
119
        continue;
1203
119
      } else {
1204
9
        zend_string_efree(replaced);
1205
9
        return ZSTR_EMPTY_ALLOC();
1206
9
      }
1207
4.00k
    } else { /* SUCCESS */
1208
4.00k
      mbsequence = &old[cursor_before];
1209
4.00k
      mbseqlen = cursor - cursor_before;
1210
4.00k
    }
1211
1212
4.00k
    if (this_char != '&') { /* no entity on this position */
1213
3.96k
      const unsigned char *rep  = NULL;
1214
3.96k
      size_t        rep_len = 0;
1215
1216
3.96k
      if (((this_char == '\'' && !(flags & ENT_HTML_QUOTE_SINGLE)) ||
1217
3.95k
          (this_char == '"' && !(flags & ENT_HTML_QUOTE_DOUBLE))))
1218
18
        goto pass_char_through;
1219
1220
3.95k
      if (all) { /* false that CHARSET_PARTIAL_SUPPORT(charset) */
1221
2.34k
        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
1.26k
          map_to_unicode(this_char, to_uni_table, &this_char);
1226
1.26k
          if (this_char == 0xFFFF) /* no mapping; pass through */
1227
0
            goto pass_char_through;
1228
1.26k
        }
1229
        /* the cursor may advance */
1230
2.34k
        find_entity_for_char(this_char, charset, entity_table.ms_table, &rep,
1231
2.34k
          &rep_len, old, oldlen, &cursor);
1232
2.34k
      } else {
1233
1.60k
        find_entity_for_char_basic(this_char, entity_table.table, &rep, &rep_len);
1234
1.60k
      }
1235
1236
3.95k
      if (rep != NULL) {
1237
401
        ZSTR_VAL(replaced)[len++] = '&';
1238
401
        memcpy(&ZSTR_VAL(replaced)[len], rep, rep_len);
1239
401
        len += rep_len;
1240
401
        ZSTR_VAL(replaced)[len++] = ';';
1241
3.54k
      } else {
1242
        /* we did not find an entity for this char.
1243
         * check for its validity, if its valid pass it unchanged */
1244
3.54k
        if (flags & ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS) {
1245
2.90k
          if (CHARSET_UNICODE_COMPAT(charset)) {
1246
784
            if (!unicode_cp_is_allowed(this_char, doctype)) {
1247
311
              mbsequence = replacement;
1248
311
              mbseqlen = replacement_len;
1249
311
            }
1250
2.12k
          } else if (to_uni_table) {
1251
965
            if (!all) /* otherwise we already did this */
1252
0
              map_to_unicode(this_char, to_uni_table, &this_char);
1253
965
            if (!unicode_cp_is_allowed(this_char, doctype)) {
1254
400
              mbsequence = replacement;
1255
400
              mbseqlen = replacement_len;
1256
400
            }
1257
1.15k
          } 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
1.15k
            if (this_char <= 0x7D &&
1266
552
                !unicode_cp_is_allowed(this_char, doctype)) {
1267
266
              mbsequence = replacement;
1268
266
              mbseqlen = replacement_len;
1269
266
            }
1270
1.15k
          }
1271
2.90k
        }
1272
3.56k
pass_char_through:
1273
3.56k
        if (mbseqlen > 1) {
1274
1.00k
          memcpy(ZSTR_VAL(replaced) + len, mbsequence, mbseqlen);
1275
1.00k
          len += mbseqlen;
1276
2.55k
        } else {
1277
2.55k
          ZSTR_VAL(replaced)[len++] = mbsequence[0];
1278
2.55k
        }
1279
3.56k
      }
1280
3.95k
    } else { /* this_char == '&' */
1281
40
      if (double_encode) {
1282
39
encode_amp:
1283
39
        memcpy(&ZSTR_VAL(replaced)[len], "&amp;", sizeof("&amp;") - 1);
1284
39
        len += sizeof("&amp;") - 1;
1285
39
      } else { /* no double encode */
1286
        /* check if entity is valid */
1287
1
        size_t ent_len; /* not counting & or ; */
1288
        /* peek at next char */
1289
1
        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
1
        } else { /* named entity */
1302
          /* check for vality of named entity */
1303
1
          const char *start = (const char *) &old[cursor],
1304
1
                 *next = start;
1305
1
          unsigned   dummy1, dummy2;
1306
1307
1
          if (process_named_entity_html(&next, &start, &ent_len) == FAILURE)
1308
0
            goto encode_amp;
1309
1
          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
1
        }
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
1
        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
1
        ZSTR_VAL(replaced)[len++] = '&';
1327
1
        memcpy(&ZSTR_VAL(replaced)[len], &old[cursor], ent_len);
1328
1
        len += ent_len;
1329
1
        ZSTR_VAL(replaced)[len++] = ';';
1330
1
        cursor += ent_len + 1;
1331
1
      }
1332
40
    }
1333
4.00k
  }
1334
54
  ZSTR_VAL(replaced)[len] = '\0';
1335
54
  ZSTR_LEN(replaced) = len;
1336
1337
54
  return replaced;
1338
63
}
1339
/* }}} */
1340
1341
/* {{{ php_html_entities */
1342
static void php_html_entities(INTERNAL_FUNCTION_PARAMETERS, int all)
1343
64
{
1344
64
  zend_string *str, *hint_charset = NULL;
1345
64
  zend_long flags = ENT_QUOTES|ENT_SUBSTITUTE;
1346
64
  zend_string *replaced;
1347
64
  bool double_encode = 1;
1348
1349
192
  ZEND_PARSE_PARAMETERS_START(1, 4)
1350
256
    Z_PARAM_STR(str)
1351
64
    Z_PARAM_OPTIONAL
1352
254
    Z_PARAM_LONG(flags)
1353
302
    Z_PARAM_STR_OR_NULL(hint_charset)
1354
204
    Z_PARAM_BOOL(double_encode);
1355
64
  ZEND_PARSE_PARAMETERS_END();
1356
1357
63
  if (ZSTR_LEN(str) == 0) {
1358
0
    RETURN_EMPTY_STRING();
1359
0
  }
1360
63
  replaced = php_escape_html_entities_ex(
1361
63
    (unsigned char*)ZSTR_VAL(str), ZSTR_LEN(str), all, (int) flags,
1362
63
    hint_charset ? ZSTR_VAL(hint_charset) : NULL, double_encode, /* quiet */ 0);
1363
63
  RETVAL_STR(replaced);
1364
63
}
1365
/* }}} */
1366
1367
/* {{{ Convert special characters to HTML entities */
1368
PHP_FUNCTION(htmlspecialchars)
1369
0
{
1370
0
  php_html_entities(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
1371
0
}
1372
/* }}} */
1373
1374
/* {{{ Convert special HTML entities back to characters */
1375
PHP_FUNCTION(htmlspecialchars_decode)
1376
0
{
1377
0
  zend_string *str;
1378
0
  zend_long quote_style = ENT_QUOTES|ENT_SUBSTITUTE;
1379
0
  zend_string *replaced;
1380
1381
0
  ZEND_PARSE_PARAMETERS_START(1, 2)
1382
0
    Z_PARAM_STR(str)
1383
0
    Z_PARAM_OPTIONAL
1384
0
    Z_PARAM_LONG(quote_style)
1385
0
  ZEND_PARSE_PARAMETERS_END();
1386
1387
0
  replaced = php_unescape_html_entities(str, 0 /*!all*/, (int)quote_style, NULL);
1388
0
  RETURN_STR(replaced);
1389
0
}
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
64
{
1416
64
  php_html_entities(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
1417
64
}
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
/* }}} */