1 /*
2 +----------------------------------------------------------------------+
3 | PHP Version 7 |
4 +----------------------------------------------------------------------+
5 | Copyright (c) The PHP Group |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
15 | Authors: |
16 | Wez Furlong (wez@thebrainroom.com) |
17 | Sara Golemon (pollita@php.net) |
18 +----------------------------------------------------------------------+
19 */
20
21 #include "php.h"
22 #include "php_globals.h"
23 #include "ext/standard/basic_functions.h"
24 #include "ext/standard/file.h"
25
26 #define PHP_STREAM_BRIGADE_RES_NAME "userfilter.bucket brigade"
27 #define PHP_STREAM_BUCKET_RES_NAME "userfilter.bucket"
28 #define PHP_STREAM_FILTER_RES_NAME "userfilter.filter"
29
30 struct php_user_filter_data {
31 zend_class_entry *ce;
32 /* variable length; this *must* be last in the structure */
33 zend_string *classname;
34 };
35
36 /* to provide context for calling into the next filter from user-space */
37 static int le_userfilters;
38 static int le_bucket_brigade;
39 static int le_bucket;
40
41 /* define the base filter class */
42
PHP_FUNCTION(user_filter_nop)43 PHP_FUNCTION(user_filter_nop)
44 {
45 }
46 ZEND_BEGIN_ARG_INFO(arginfo_php_user_filter_filter, 0)
47 ZEND_ARG_INFO(0, in)
48 ZEND_ARG_INFO(0, out)
49 ZEND_ARG_INFO(1, consumed)
50 ZEND_ARG_INFO(0, closing)
51 ZEND_END_ARG_INFO()
52
53 ZEND_BEGIN_ARG_INFO(arginfo_php_user_filter_onCreate, 0)
54 ZEND_END_ARG_INFO()
55
56 ZEND_BEGIN_ARG_INFO(arginfo_php_user_filter_onClose, 0)
57 ZEND_END_ARG_INFO()
58
59 static const zend_function_entry user_filter_class_funcs[] = {
60 PHP_NAMED_FE(filter, PHP_FN(user_filter_nop), arginfo_php_user_filter_filter)
61 PHP_NAMED_FE(onCreate, PHP_FN(user_filter_nop), arginfo_php_user_filter_onCreate)
62 PHP_NAMED_FE(onClose, PHP_FN(user_filter_nop), arginfo_php_user_filter_onClose)
63 PHP_FE_END
64 };
65
66 static zend_class_entry user_filter_class_entry;
67
ZEND_RSRC_DTOR_FUNC(php_bucket_dtor)68 static ZEND_RSRC_DTOR_FUNC(php_bucket_dtor)
69 {
70 php_stream_bucket *bucket = (php_stream_bucket *)res->ptr;
71 if (bucket) {
72 php_stream_bucket_delref(bucket);
73 bucket = NULL;
74 }
75 }
76
PHP_MINIT_FUNCTION(user_filters)77 PHP_MINIT_FUNCTION(user_filters)
78 {
79 zend_class_entry *php_user_filter;
80 /* init the filter class ancestor */
81 INIT_CLASS_ENTRY(user_filter_class_entry, "php_user_filter", user_filter_class_funcs);
82 if ((php_user_filter = zend_register_internal_class(&user_filter_class_entry)) == NULL) {
83 return FAILURE;
84 }
85 zend_declare_property_string(php_user_filter, "filtername", sizeof("filtername")-1, "", ZEND_ACC_PUBLIC);
86 zend_declare_property_string(php_user_filter, "params", sizeof("params")-1, "", ZEND_ACC_PUBLIC);
87
88 /* init the filter resource; it has no dtor, as streams will always clean it up
89 * at the correct time */
90 le_userfilters = zend_register_list_destructors_ex(NULL, NULL, PHP_STREAM_FILTER_RES_NAME, 0);
91
92 if (le_userfilters == FAILURE) {
93 return FAILURE;
94 }
95
96 /* Filters will dispose of their brigades */
97 le_bucket_brigade = zend_register_list_destructors_ex(NULL, NULL, PHP_STREAM_BRIGADE_RES_NAME, module_number);
98 /* Brigades will dispose of their buckets */
99 le_bucket = zend_register_list_destructors_ex(php_bucket_dtor, NULL, PHP_STREAM_BUCKET_RES_NAME, module_number);
100
101 if (le_bucket_brigade == FAILURE) {
102 return FAILURE;
103 }
104
105 REGISTER_LONG_CONSTANT("PSFS_PASS_ON", PSFS_PASS_ON, CONST_CS | CONST_PERSISTENT);
106 REGISTER_LONG_CONSTANT("PSFS_FEED_ME", PSFS_FEED_ME, CONST_CS | CONST_PERSISTENT);
107 REGISTER_LONG_CONSTANT("PSFS_ERR_FATAL", PSFS_ERR_FATAL, CONST_CS | CONST_PERSISTENT);
108
109 REGISTER_LONG_CONSTANT("PSFS_FLAG_NORMAL", PSFS_FLAG_NORMAL, CONST_CS | CONST_PERSISTENT);
110 REGISTER_LONG_CONSTANT("PSFS_FLAG_FLUSH_INC", PSFS_FLAG_FLUSH_INC, CONST_CS | CONST_PERSISTENT);
111 REGISTER_LONG_CONSTANT("PSFS_FLAG_FLUSH_CLOSE", PSFS_FLAG_FLUSH_CLOSE, CONST_CS | CONST_PERSISTENT);
112
113 return SUCCESS;
114 }
115
PHP_RSHUTDOWN_FUNCTION(user_filters)116 PHP_RSHUTDOWN_FUNCTION(user_filters)
117 {
118 if (BG(user_filter_map)) {
119 zend_hash_destroy(BG(user_filter_map));
120 efree(BG(user_filter_map));
121 BG(user_filter_map) = NULL;
122 }
123
124 return SUCCESS;
125 }
126
userfilter_dtor(php_stream_filter * thisfilter)127 static void userfilter_dtor(php_stream_filter *thisfilter)
128 {
129 zval *obj = &thisfilter->abstract;
130 zval func_name;
131 zval retval;
132
133 if (obj == NULL) {
134 /* If there's no object associated then there's nothing to dispose of */
135 return;
136 }
137
138 ZVAL_STRINGL(&func_name, "onclose", sizeof("onclose")-1);
139
140 call_user_function(NULL,
141 obj,
142 &func_name,
143 &retval,
144 0, NULL);
145
146 zval_ptr_dtor(&retval);
147 zval_ptr_dtor(&func_name);
148
149 /* kill the object */
150 zval_ptr_dtor(obj);
151 }
152
userfilter_filter(php_stream * stream,php_stream_filter * thisfilter,php_stream_bucket_brigade * buckets_in,php_stream_bucket_brigade * buckets_out,size_t * bytes_consumed,int flags)153 php_stream_filter_status_t userfilter_filter(
154 php_stream *stream,
155 php_stream_filter *thisfilter,
156 php_stream_bucket_brigade *buckets_in,
157 php_stream_bucket_brigade *buckets_out,
158 size_t *bytes_consumed,
159 int flags
160 )
161 {
162 int ret = PSFS_ERR_FATAL;
163 zval *obj = &thisfilter->abstract;
164 zval func_name;
165 zval retval;
166 zval args[4];
167 zval zpropname;
168 int call_result;
169
170 /* the userfilter object probably doesn't exist anymore */
171 if (CG(unclean_shutdown)) {
172 return ret;
173 }
174
175 /* Make sure the stream is not closed while the filter callback executes. */
176 uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
177 stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
178
179 if (!zend_hash_str_exists_ind(Z_OBJPROP_P(obj), "stream", sizeof("stream")-1)) {
180 zval tmp;
181
182 /* Give the userfilter class a hook back to the stream */
183 php_stream_to_zval(stream, &tmp);
184 Z_ADDREF(tmp);
185 add_property_zval(obj, "stream", &tmp);
186 /* add_property_zval increments the refcount which is unwanted here */
187 zval_ptr_dtor(&tmp);
188 }
189
190 ZVAL_STRINGL(&func_name, "filter", sizeof("filter")-1);
191
192 /* Setup calling arguments */
193 ZVAL_RES(&args[0], zend_register_resource(buckets_in, le_bucket_brigade));
194 ZVAL_RES(&args[1], zend_register_resource(buckets_out, le_bucket_brigade));
195
196 if (bytes_consumed) {
197 ZVAL_LONG(&args[2], *bytes_consumed);
198 } else {
199 ZVAL_NULL(&args[2]);
200 }
201
202 ZVAL_BOOL(&args[3], flags & PSFS_FLAG_FLUSH_CLOSE);
203
204 call_result = call_user_function_ex(NULL,
205 obj,
206 &func_name,
207 &retval,
208 4, args,
209 0, NULL);
210
211 zval_ptr_dtor(&func_name);
212
213 if (call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
214 convert_to_long(&retval);
215 ret = (int)Z_LVAL(retval);
216 } else if (call_result == FAILURE) {
217 php_error_docref(NULL, E_WARNING, "failed to call filter function");
218 }
219
220 if (bytes_consumed) {
221 *bytes_consumed = zval_get_long(&args[2]);
222 }
223
224 if (buckets_in->head) {
225 php_stream_bucket *bucket = buckets_in->head;
226
227 php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade");
228 while ((bucket = buckets_in->head)) {
229 /* Remove unconsumed buckets from the brigade */
230 php_stream_bucket_unlink(bucket);
231 php_stream_bucket_delref(bucket);
232 }
233 }
234 if (ret != PSFS_PASS_ON) {
235 php_stream_bucket *bucket = buckets_out->head;
236 while (bucket != NULL) {
237 php_stream_bucket_unlink(bucket);
238 php_stream_bucket_delref(bucket);
239 bucket = buckets_out->head;
240 }
241 }
242
243 /* filter resources are cleaned up by the stream destructor,
244 * keeping a reference to the stream resource here would prevent it
245 * from being destroyed properly */
246 ZVAL_STRINGL(&zpropname, "stream", sizeof("stream")-1);
247 Z_OBJ_HANDLER_P(obj, unset_property)(obj, &zpropname, NULL);
248 zval_ptr_dtor(&zpropname);
249
250 zval_ptr_dtor(&args[3]);
251 zval_ptr_dtor(&args[2]);
252 zval_ptr_dtor(&args[1]);
253 zval_ptr_dtor(&args[0]);
254
255 stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
256 stream->flags |= orig_no_fclose;
257
258 return ret;
259 }
260
261 static const php_stream_filter_ops userfilter_ops = {
262 userfilter_filter,
263 userfilter_dtor,
264 "user-filter"
265 };
266
user_filter_factory_create(const char * filtername,zval * filterparams,uint8_t persistent)267 static php_stream_filter *user_filter_factory_create(const char *filtername,
268 zval *filterparams, uint8_t persistent)
269 {
270 struct php_user_filter_data *fdat = NULL;
271 php_stream_filter *filter;
272 zval obj, zfilter;
273 zval func_name;
274 zval retval;
275 size_t len;
276
277 /* some sanity checks */
278 if (persistent) {
279 php_error_docref(NULL, E_WARNING,
280 "cannot use a user-space filter with a persistent stream");
281 return NULL;
282 }
283
284 len = strlen(filtername);
285
286 /* determine the classname/class entry */
287 if (NULL == (fdat = zend_hash_str_find_ptr(BG(user_filter_map), (char*)filtername, len))) {
288 char *period;
289
290 /* Userspace Filters using ambiguous wildcards could cause problems.
291 i.e.: myfilter.foo.bar will always call into myfilter.foo.*
292 never seeing myfilter.*
293 TODO: Allow failed userfilter creations to continue
294 scanning through the list */
295 if ((period = strrchr(filtername, '.'))) {
296 char *wildcard = safe_emalloc(len, 1, 3);
297
298 /* Search for wildcard matches instead */
299 memcpy(wildcard, filtername, len + 1); /* copy \0 */
300 period = wildcard + (period - filtername);
301 while (period) {
302 ZEND_ASSERT(period[0] == '.');
303 period[1] = '*';
304 period[2] = '\0';
305 if (NULL != (fdat = zend_hash_str_find_ptr(BG(user_filter_map), wildcard, strlen(wildcard)))) {
306 period = NULL;
307 } else {
308 *period = '\0';
309 period = strrchr(wildcard, '.');
310 }
311 }
312 efree(wildcard);
313 }
314 if (fdat == NULL) {
315 php_error_docref(NULL, E_WARNING,
316 "Err, filter \"%s\" is not in the user-filter map, but somehow the user-filter-factory was invoked for it!?", filtername);
317 return NULL;
318 }
319 }
320
321 /* bind the classname to the actual class */
322 if (fdat->ce == NULL) {
323 if (NULL == (fdat->ce = zend_lookup_class(fdat->classname))) {
324 php_error_docref(NULL, E_WARNING,
325 "user-filter \"%s\" requires class \"%s\", but that class is not defined",
326 filtername, ZSTR_VAL(fdat->classname));
327 return NULL;
328 }
329 }
330
331 /* create the object */
332 if (object_init_ex(&obj, fdat->ce) == FAILURE) {
333 return NULL;
334 }
335
336 filter = php_stream_filter_alloc(&userfilter_ops, NULL, 0);
337 if (filter == NULL) {
338 zval_ptr_dtor(&obj);
339 return NULL;
340 }
341
342 /* filtername */
343 add_property_string(&obj, "filtername", (char*)filtername);
344
345 /* and the parameters, if any */
346 if (filterparams) {
347 add_property_zval(&obj, "params", filterparams);
348 } else {
349 add_property_null(&obj, "params");
350 }
351
352 /* invoke the constructor */
353 ZVAL_STRINGL(&func_name, "oncreate", sizeof("oncreate")-1);
354
355 call_user_function(NULL,
356 &obj,
357 &func_name,
358 &retval,
359 0, NULL);
360
361 if (Z_TYPE(retval) != IS_UNDEF) {
362 if (Z_TYPE(retval) == IS_FALSE) {
363 /* User reported filter creation error "return false;" */
364 zval_ptr_dtor(&retval);
365
366 /* Kill the filter (safely) */
367 ZVAL_UNDEF(&filter->abstract);
368 php_stream_filter_free(filter);
369
370 /* Kill the object */
371 zval_ptr_dtor(&obj);
372
373 /* Report failure to filter_alloc */
374 return NULL;
375 }
376 zval_ptr_dtor(&retval);
377 }
378 zval_ptr_dtor(&func_name);
379
380 /* set the filter property, this will be used during cleanup */
381 ZVAL_RES(&zfilter, zend_register_resource(filter, le_userfilters));
382 ZVAL_OBJ(&filter->abstract, Z_OBJ(obj));
383 add_property_zval(&obj, "filter", &zfilter);
384 /* add_property_zval increments the refcount which is unwanted here */
385 zval_ptr_dtor(&zfilter);
386
387 return filter;
388 }
389
390 static const php_stream_filter_factory user_filter_factory = {
391 user_filter_factory_create
392 };
393
filter_item_dtor(zval * zv)394 static void filter_item_dtor(zval *zv)
395 {
396 struct php_user_filter_data *fdat = Z_PTR_P(zv);
397 zend_string_release_ex(fdat->classname, 0);
398 efree(fdat);
399 }
400
401 /* {{{ proto object stream_bucket_make_writeable(resource brigade)
402 Return a bucket object from the brigade for operating on */
PHP_FUNCTION(stream_bucket_make_writeable)403 PHP_FUNCTION(stream_bucket_make_writeable)
404 {
405 zval *zbrigade, zbucket;
406 php_stream_bucket_brigade *brigade;
407 php_stream_bucket *bucket;
408
409 ZEND_PARSE_PARAMETERS_START(1, 1)
410 Z_PARAM_RESOURCE(zbrigade)
411 ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
412
413 if ((brigade = (php_stream_bucket_brigade*)zend_fetch_resource(
414 Z_RES_P(zbrigade), PHP_STREAM_BRIGADE_RES_NAME, le_bucket_brigade)) == NULL) {
415 RETURN_FALSE;
416 }
417
418 ZVAL_NULL(return_value);
419
420 if (brigade->head && (bucket = php_stream_bucket_make_writeable(brigade->head))) {
421 ZVAL_RES(&zbucket, zend_register_resource(bucket, le_bucket));
422 object_init(return_value);
423 add_property_zval(return_value, "bucket", &zbucket);
424 /* add_property_zval increments the refcount which is unwanted here */
425 zval_ptr_dtor(&zbucket);
426 add_property_stringl(return_value, "data", bucket->buf, bucket->buflen);
427 add_property_long(return_value, "datalen", bucket->buflen);
428 }
429 }
430 /* }}} */
431
432 /* {{{ php_stream_bucket_attach */
php_stream_bucket_attach(int append,INTERNAL_FUNCTION_PARAMETERS)433 static void php_stream_bucket_attach(int append, INTERNAL_FUNCTION_PARAMETERS)
434 {
435 zval *zbrigade, *zobject;
436 zval *pzbucket, *pzdata;
437 php_stream_bucket_brigade *brigade;
438 php_stream_bucket *bucket;
439
440 ZEND_PARSE_PARAMETERS_START(2, 2)
441 Z_PARAM_RESOURCE(zbrigade)
442 Z_PARAM_OBJECT(zobject)
443 ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
444
445 if (NULL == (pzbucket = zend_hash_str_find_deref(Z_OBJPROP_P(zobject), "bucket", sizeof("bucket")-1))) {
446 php_error_docref(NULL, E_WARNING, "Object has no bucket property");
447 RETURN_FALSE;
448 }
449
450 if ((brigade = (php_stream_bucket_brigade*)zend_fetch_resource(
451 Z_RES_P(zbrigade), PHP_STREAM_BRIGADE_RES_NAME, le_bucket_brigade)) == NULL) {
452 RETURN_FALSE;
453 }
454
455 if ((bucket = (php_stream_bucket *)zend_fetch_resource_ex(pzbucket, PHP_STREAM_BUCKET_RES_NAME, le_bucket)) == NULL) {
456 RETURN_FALSE;
457 }
458
459 if (NULL != (pzdata = zend_hash_str_find_deref(Z_OBJPROP_P(zobject), "data", sizeof("data")-1)) && Z_TYPE_P(pzdata) == IS_STRING) {
460 if (!bucket->own_buf) {
461 bucket = php_stream_bucket_make_writeable(bucket);
462 }
463 if (bucket->buflen != Z_STRLEN_P(pzdata)) {
464 bucket->buf = perealloc(bucket->buf, Z_STRLEN_P(pzdata), bucket->is_persistent);
465 bucket->buflen = Z_STRLEN_P(pzdata);
466 }
467 memcpy(bucket->buf, Z_STRVAL_P(pzdata), bucket->buflen);
468 }
469
470 if (append) {
471 php_stream_bucket_append(brigade, bucket);
472 } else {
473 php_stream_bucket_prepend(brigade, bucket);
474 }
475 /* This is a hack necessary to accommodate situations where bucket is appended to the stream
476 * multiple times. See bug35916.phpt for reference.
477 */
478 if (bucket->refcount == 1) {
479 bucket->refcount++;
480 }
481 }
482 /* }}} */
483
484 /* {{{ proto void stream_bucket_prepend(resource brigade, object bucket)
485 Prepend bucket to brigade */
PHP_FUNCTION(stream_bucket_prepend)486 PHP_FUNCTION(stream_bucket_prepend)
487 {
488 php_stream_bucket_attach(0, INTERNAL_FUNCTION_PARAM_PASSTHRU);
489 }
490 /* }}} */
491
492 /* {{{ proto void stream_bucket_append(resource brigade, object bucket)
493 Append bucket to brigade */
PHP_FUNCTION(stream_bucket_append)494 PHP_FUNCTION(stream_bucket_append)
495 {
496 php_stream_bucket_attach(1, INTERNAL_FUNCTION_PARAM_PASSTHRU);
497 }
498 /* }}} */
499
500 /* {{{ proto resource stream_bucket_new(resource stream, string buffer)
501 Create a new bucket for use on the current stream */
PHP_FUNCTION(stream_bucket_new)502 PHP_FUNCTION(stream_bucket_new)
503 {
504 zval *zstream, zbucket;
505 php_stream *stream;
506 char *buffer;
507 char *pbuffer;
508 size_t buffer_len;
509 php_stream_bucket *bucket;
510
511 ZEND_PARSE_PARAMETERS_START(2, 2)
512 Z_PARAM_ZVAL(zstream)
513 Z_PARAM_STRING(buffer, buffer_len)
514 ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
515
516 php_stream_from_zval(stream, zstream);
517
518 pbuffer = pemalloc(buffer_len, php_stream_is_persistent(stream));
519
520 memcpy(pbuffer, buffer, buffer_len);
521
522 bucket = php_stream_bucket_new(stream, pbuffer, buffer_len, 1, php_stream_is_persistent(stream));
523
524 if (bucket == NULL) {
525 RETURN_FALSE;
526 }
527
528 ZVAL_RES(&zbucket, zend_register_resource(bucket, le_bucket));
529 object_init(return_value);
530 add_property_zval(return_value, "bucket", &zbucket);
531 /* add_property_zval increments the refcount which is unwanted here */
532 zval_ptr_dtor(&zbucket);
533 add_property_stringl(return_value, "data", bucket->buf, bucket->buflen);
534 add_property_long(return_value, "datalen", bucket->buflen);
535 }
536 /* }}} */
537
538 /* {{{ proto array stream_get_filters(void)
539 Returns a list of registered filters */
PHP_FUNCTION(stream_get_filters)540 PHP_FUNCTION(stream_get_filters)
541 {
542 zend_string *filter_name;
543 HashTable *filters_hash;
544
545 if (zend_parse_parameters_none() == FAILURE) {
546 return;
547 }
548
549 array_init(return_value);
550
551 filters_hash = php_get_stream_filters_hash();
552
553 if (filters_hash) {
554 ZEND_HASH_FOREACH_STR_KEY(filters_hash, filter_name) {
555 if (filter_name) {
556 add_next_index_str(return_value, zend_string_copy(filter_name));
557 }
558 } ZEND_HASH_FOREACH_END();
559 }
560 /* It's okay to return an empty array if no filters are registered */
561 }
562 /* }}} */
563
564 /* {{{ proto bool stream_filter_register(string filtername, string classname)
565 Registers a custom filter handler class */
PHP_FUNCTION(stream_filter_register)566 PHP_FUNCTION(stream_filter_register)
567 {
568 zend_string *filtername, *classname;
569 struct php_user_filter_data *fdat;
570
571 ZEND_PARSE_PARAMETERS_START(2, 2)
572 Z_PARAM_STR(filtername)
573 Z_PARAM_STR(classname)
574 ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
575
576 RETVAL_FALSE;
577
578 if (!ZSTR_LEN(filtername)) {
579 php_error_docref(NULL, E_WARNING, "Filter name cannot be empty");
580 return;
581 }
582
583 if (!ZSTR_LEN(classname)) {
584 php_error_docref(NULL, E_WARNING, "Class name cannot be empty");
585 return;
586 }
587
588 if (!BG(user_filter_map)) {
589 BG(user_filter_map) = (HashTable*) emalloc(sizeof(HashTable));
590 zend_hash_init(BG(user_filter_map), 8, NULL, (dtor_func_t) filter_item_dtor, 0);
591 }
592
593 fdat = ecalloc(1, sizeof(struct php_user_filter_data));
594 fdat->classname = zend_string_copy(classname);
595
596 if (zend_hash_add_ptr(BG(user_filter_map), filtername, fdat) != NULL &&
597 php_stream_filter_register_factory_volatile(filtername, &user_filter_factory) == SUCCESS) {
598 RETVAL_TRUE;
599 } else {
600 zend_string_release_ex(classname, 0);
601 efree(fdat);
602 }
603 }
604 /* }}} */
605