-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmdparser_options.c
More file actions
307 lines (268 loc) · 11.7 KB
/
mdparser_options.c
File metadata and controls
307 lines (268 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/*
+----------------------------------------------------------------------+
| Copyright (c) 2025-2026, Ilia Alshanetsky |
| Copyright (c) 2025-2026, Advanced Internet Designs Inc. |
+----------------------------------------------------------------------+
| This source file is subject to the BSD 3-Clause license that is |
| bundled with this package in the file LICENSE. |
+----------------------------------------------------------------------+
| Author: Ilia Alshanetsky <ilia@ilia.ws> |
+----------------------------------------------------------------------+
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "zend_exceptions.h"
#include "php_mdparser.h"
#include "mdparser_arginfo.h"
#include "cmark-gfm.h"
zend_class_entry *mdparser_options_ce;
int mdparser_default_cmark_options = 0;
int mdparser_default_extension_mask = 0;
int mdparser_default_postprocess_mask = 0;
/* Each Options property corresponds to either a CMARK_OPT_* flag or a
* GFM extension toggle.
*
* IMPORTANT: default_value MUST match the constructor default in
* mdparser.stub.php. ZPP does not auto-apply arginfo defaults to
* internal methods, so the C-side __construct seeds values[] from this
* table before ZPP runs. If the two drift, `$opts->tagfilter` will
* lie about whether tagfilter is actually enabled. The MINIT init
* step caches the result of walking this table once, so runtime cost
* is zero. */
/* Field indices, used by the preset factories and by __construct to
* address individual entries in the field table without hard-coding
* positions. MUST stay in sync with mdparser_options_fields[] below. */
enum {
MDOPT_SOURCEPOS = 0,
MDOPT_HARDBREAKS,
MDOPT_NOBREAKS,
MDOPT_SMART,
MDOPT_UNSAFE,
MDOPT_VALIDATE_UTF8,
MDOPT_GITHUB_PRE_LANG,
MDOPT_LIBERAL_HTML_TAG,
MDOPT_FOOTNOTES,
MDOPT_STRIKETHROUGH_DOUBLE_TILDE,
MDOPT_TABLE_PREFER_STYLE_ATTRIBUTES,
MDOPT_FULL_INFO_STRING,
MDOPT_TABLES,
MDOPT_STRIKETHROUGH,
MDOPT_TASKLIST,
MDOPT_AUTOLINK,
MDOPT_TAGFILTER,
MDOPT_HEADING_ANCHORS,
MDOPT_NOFOLLOW_LINKS,
MDOPT_COUNT_
};
typedef struct {
const char *name;
size_t name_len;
int cmark_bit; /* nonzero if this maps to a cmark option */
int extension_bit; /* nonzero if this maps to a GFM extension */
int postprocess_bit; /* nonzero if this drives an HTML post-pass */
bool default_value;
} mdparser_options_field;
#define F(name_, cmark_, ext_, pp_, def_) \
{ name_, sizeof(name_) - 1, cmark_, ext_, pp_, def_ }
static const mdparser_options_field mdparser_options_fields[] = {
F("sourcepos", CMARK_OPT_SOURCEPOS, 0, 0, false),
F("hardbreaks", CMARK_OPT_HARDBREAKS, 0, 0, false),
F("nobreaks", CMARK_OPT_NOBREAKS, 0, 0, false),
F("smart", CMARK_OPT_SMART, 0, 0, false),
F("unsafe", CMARK_OPT_UNSAFE, 0, 0, false),
F("validateUtf8", CMARK_OPT_VALIDATE_UTF8, 0, 0, true),
F("githubPreLang", CMARK_OPT_GITHUB_PRE_LANG, 0, 0, true),
F("liberalHtmlTag", CMARK_OPT_LIBERAL_HTML_TAG, 0, 0, false),
F("footnotes", CMARK_OPT_FOOTNOTES, 0, 0, false),
F("strikethroughDoubleTilde", CMARK_OPT_STRIKETHROUGH_DOUBLE_TILDE, 0, 0, false),
F("tablePreferStyleAttributes", CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES, 0, 0, false),
F("fullInfoString", CMARK_OPT_FULL_INFO_STRING, 0, 0, false),
F("tables", 0, MDPARSER_EXT_TABLES, 0, true),
F("strikethrough", 0, MDPARSER_EXT_STRIKETHROUGH, 0, true),
F("tasklist", 0, MDPARSER_EXT_TASKLIST, 0, true),
F("autolink", 0, MDPARSER_EXT_AUTOLINK, 0, true),
F("tagfilter", 0, MDPARSER_EXT_TAGFILTER, 0, true),
F("headingAnchors", 0, 0, MDPARSER_PP_HEADING_ANCHORS, false),
F("nofollowLinks", 0, 0, MDPARSER_PP_NOFOLLOW_LINKS, false),
};
#undef F
#define MDPARSER_OPTIONS_FIELD_COUNT \
(sizeof(mdparser_options_fields) / sizeof(mdparser_options_fields[0]))
/* If a new option is inserted in either the enum or the field table
* without updating its sibling, the preset factories silently target
* the wrong bit. A misaligned MDOPT_UNSAFE would flip the XSS safety
* default for permissive() / strict() / github(); pin the alignment
* at compile time.
*
* MSVC-cl in default C mode rejects _Static_assert (C11) without
* /std:c11, and the PHP Windows build harness doesn't pass that flag,
* so use the portable negative-array-size idiom instead. */
typedef char mdparser_options_field_count_assert[
(MDOPT_COUNT_ == MDPARSER_OPTIONS_FIELD_COUNT) ? 1 : -1];
void mdparser_options_init_defaults(void)
{
int c = 0;
int e = 0;
int p = 0;
for (size_t i = 0; i < MDPARSER_OPTIONS_FIELD_COUNT; i++) {
const mdparser_options_field *f = &mdparser_options_fields[i];
if (!f->default_value) {
continue;
}
if (f->cmark_bit) {
c |= f->cmark_bit;
} else if (f->extension_bit) {
e |= f->extension_bit;
} else {
p |= f->postprocess_bit;
}
}
mdparser_default_cmark_options = c;
mdparser_default_extension_mask = e;
mdparser_default_postprocess_mask = p;
}
/* Write the value vector into a freshly-allocated Options object's
* properties (one bool per MDOPT_* index, sized via
* MDPARSER_OPTIONS_FIELD_COUNT so additions don't drift this
* signature). Used by __construct and by the static preset factories.
* Safe to call only on an object whose properties are still in their
* post-object_init_ex (IS_UNDEF) state, because readonly enforcement
* allows first-writes within the declaring class scope but rejects
* any subsequent assignment. */
static void mdparser_options_populate_object(zend_object *this_obj,
const bool values[MDPARSER_OPTIONS_FIELD_COUNT])
{
for (size_t i = 0; i < MDPARSER_OPTIONS_FIELD_COUNT; i++) {
const mdparser_options_field *f = &mdparser_options_fields[i];
zval tmp;
ZVAL_BOOL(&tmp, values[i]);
zend_update_property(mdparser_options_ce, this_obj,
f->name, f->name_len, &tmp);
}
}
static void mdparser_options_seed_defaults(bool values[MDPARSER_OPTIONS_FIELD_COUNT])
{
for (size_t i = 0; i < MDPARSER_OPTIONS_FIELD_COUNT; i++) {
values[i] = mdparser_options_fields[i].default_value;
}
}
void mdparser_options_read_masks(zval *options_zv, int *cmark_options, int *extension_mask, int *postprocess_mask)
{
int c = 0;
int e = 0;
int p = 0;
zend_object *obj = Z_OBJ_P(options_zv);
for (size_t i = 0; i < MDPARSER_OPTIONS_FIELD_COUNT; i++) {
const mdparser_options_field *f = &mdparser_options_fields[i];
zval *prop;
zval rv;
prop = zend_read_property(mdparser_options_ce, obj,
f->name, f->name_len, 1, &rv);
/* Options is final + readonly with typed bool properties; the
* only way to land here with anything other than IS_TRUE /
* IS_FALSE is to skip __construct (e.g. via
* ReflectionClass::newInstanceWithoutConstructor). Silent
* reads of an uninit typed property return &EG(uninitialized_zval)
* (IS_NULL), so an IS_UNDEF check alone would miss this case.
* Treating uninit as false would silently flip the safety
* defaults (validateUtf8 / tagfilter) off while $parser->options
* remains unreadable. Reject the object outright. */
if (UNEXPECTED(!prop ||
(Z_TYPE_P(prop) != IS_TRUE && Z_TYPE_P(prop) != IS_FALSE))) {
zend_throw_exception_ex(mdparser_exception_ce, 0,
"mdparser: Options::$%s is uninitialized; "
"Options instances must be constructed via __construct() "
"(or one of strict()/github()/permissive())",
f->name);
return;
}
if (Z_TYPE_P(prop) != IS_TRUE) {
continue;
}
if (f->cmark_bit) {
c |= f->cmark_bit;
} else if (f->extension_bit) {
e |= f->extension_bit;
} else {
p |= f->postprocess_bit;
}
}
*cmark_options = c;
*extension_mask = e;
*postprocess_mask = p;
}
void mdparser_options_register_class(void)
{
mdparser_options_ce = register_class_MdParser_Options();
mdparser_options_ce->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE;
}
PHP_METHOD(MdParser_Options, __construct)
{
bool values[MDPARSER_OPTIONS_FIELD_COUNT];
/* Seed with stub defaults; ZPP only overwrites args that were
* actually provided by the caller (Z_PARAM_BOOL is a no-op for
* missing optional args), so unspecified fields keep their
* table-driven default. */
mdparser_options_seed_defaults(values);
ZEND_PARSE_PARAMETERS_START(0, MDPARSER_OPTIONS_FIELD_COUNT)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL(values[MDOPT_SOURCEPOS])
Z_PARAM_BOOL(values[MDOPT_HARDBREAKS])
Z_PARAM_BOOL(values[MDOPT_NOBREAKS])
Z_PARAM_BOOL(values[MDOPT_SMART])
Z_PARAM_BOOL(values[MDOPT_UNSAFE])
Z_PARAM_BOOL(values[MDOPT_VALIDATE_UTF8])
Z_PARAM_BOOL(values[MDOPT_GITHUB_PRE_LANG])
Z_PARAM_BOOL(values[MDOPT_LIBERAL_HTML_TAG])
Z_PARAM_BOOL(values[MDOPT_FOOTNOTES])
Z_PARAM_BOOL(values[MDOPT_STRIKETHROUGH_DOUBLE_TILDE])
Z_PARAM_BOOL(values[MDOPT_TABLE_PREFER_STYLE_ATTRIBUTES])
Z_PARAM_BOOL(values[MDOPT_FULL_INFO_STRING])
Z_PARAM_BOOL(values[MDOPT_TABLES])
Z_PARAM_BOOL(values[MDOPT_STRIKETHROUGH])
Z_PARAM_BOOL(values[MDOPT_TASKLIST])
Z_PARAM_BOOL(values[MDOPT_AUTOLINK])
Z_PARAM_BOOL(values[MDOPT_TAGFILTER])
Z_PARAM_BOOL(values[MDOPT_HEADING_ANCHORS])
Z_PARAM_BOOL(values[MDOPT_NOFOLLOW_LINKS])
ZEND_PARSE_PARAMETERS_END();
mdparser_options_populate_object(Z_OBJ_P(ZEND_THIS), values);
}
PHP_METHOD(MdParser_Options, strict)
{
ZEND_PARSE_PARAMETERS_NONE();
bool v[MDPARSER_OPTIONS_FIELD_COUNT];
mdparser_options_seed_defaults(v);
/* Bare URLs stay inert text instead of becoming live <a> tags. */
v[MDOPT_AUTOLINK] = false;
object_init_ex(return_value, mdparser_options_ce);
mdparser_options_populate_object(Z_OBJ_P(return_value), v);
}
PHP_METHOD(MdParser_Options, github)
{
ZEND_PARSE_PARAMETERS_NONE();
bool v[MDPARSER_OPTIONS_FIELD_COUNT];
mdparser_options_seed_defaults(v);
/* github.com's renderer supports [^ref] / [^ref]: syntax; the
* rest of the default set (tables/strike/tasklist/autolink/
* tagfilter/githubPreLang) already matches github. */
v[MDOPT_FOOTNOTES] = true;
object_init_ex(return_value, mdparser_options_ce);
mdparser_options_populate_object(Z_OBJ_P(return_value), v);
}
PHP_METHOD(MdParser_Options, permissive)
{
ZEND_PARSE_PARAMETERS_NONE();
bool v[MDPARSER_OPTIONS_FIELD_COUNT];
mdparser_options_seed_defaults(v);
/* Trusted-input mode: raw HTML passes through, tagfilter is off,
* liberal tag parsing is on. Explicitly disables the XSS safety
* net -- only for markdown the caller authored themselves. */
v[MDOPT_UNSAFE] = true;
v[MDOPT_TAGFILTER] = false;
v[MDOPT_LIBERAL_HTML_TAG] = true;
object_init_ex(return_value, mdparser_options_ce);
mdparser_options_populate_object(Z_OBJ_P(return_value), v);
}