-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathtemplate.cpp
More file actions
668 lines (545 loc) · 21.5 KB
/
template.cpp
File metadata and controls
668 lines (545 loc) · 21.5 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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
/**
* Template.cpp
*
* Implementation file for the Template class.
*
* @author Emiel Bruijntjes <emiel.bruijntjes@copernica.com>
* @copyright 2025 Copernica BV
*/
/**
* Dependencies
*/
#include "template.h"
#include "core.h"
#include "scope.h"
#include "linker.h"
#include "fromphp.h"
#include "php_variable.h"
#include "fromiterator.h"
#include "php_array.h"
#include "exception.h"
/**
* Begin of namespace
*/
namespace JS {
/**
* Constructor
* Watched out: the passed in PHP-value is only used to decide which handlers to install,
* but the template can after that be used for multiple PHP variables with a similar
* signature (see also Template::apply())
* @param isolate
* @param object
*/
Template::Template(v8::Isolate *isolate, const Php::Value &value) :
_isolate(isolate),
_realarray(value.isArray()),
_arrayaccess(value.instanceOf("ArrayAccess")),
_callable(value.isObject() && Php::call("method_exists", value, "__invoke"))
{
// get the template as local object
v8::Local<v8::ObjectTemplate> tpl(v8::ObjectTemplate::New(isolate));
// register the property handlers for objects and arrays
tpl->SetHandler(v8::NamedPropertyHandlerConfiguration(
&Template::getProperty, // get access to a property
&Template::setProperty, // assign a property
nullptr, // query to check which properties exist
nullptr, // remove a property
&Template::enumerateProperties // enumerate over an object
));
// for ArrayAccess objects we also configure callbacks to get access to properties by their ID
if (_arrayaccess || _realarray) tpl->SetHandler(v8::IndexedPropertyHandlerConfiguration(
&Template::getIndex, // get access to an index
&Template::setIndex, // assign a property by index
nullptr, // query to check which properties exist
nullptr, // remove a property
_realarray ? nullptr : &Template::enumerateIndexes // enumerate over an object based on the index
));
// when object is callable, we need to install a callback too
if (_callable) tpl->SetCallAsFunctionHandler(&Template::call);
// make sure handler is preserved
_template.Reset(isolate, tpl);
}
/**
* Destructor
*/
Template::~Template()
{
// forget handle
_template.Reset();
}
/**
* Is this template useful for a certain object? Does it have the features that are expected?
* @param value
* @return bool
*/
bool Template::matches(const Php::Value &value) const
{
// check whetner the object has certain features
if (_realarray != value.isArray()) return false;
if (_arrayaccess != value.instanceOf("ArrayAccess")) return false;
if (_callable != (value.isObject() && Php::call("method_exists", value, "__invoke"))) return false;
// seems all properties are supported
return true;
}
/**
* Apply the template on a PHP variable, to turn it into a JS object
* @param value
* @return v8::Local<v8::Object>
*/
v8::Local<v8::Value> Template::apply(const Php::Value &value) const
{
// we need the template locally
v8::Local<v8::ObjectTemplate> tpl(_template.Get(_isolate));
// construct a new instance
auto result = tpl->NewInstance(_isolate->GetCurrentContext());
// if not valid
if (result.IsEmpty()) return v8::Undefined(_isolate);
// the actual ojbect
auto object = result.ToLocalChecked();
// arrays cannot be weak-referenced, so we won't link them
if (!value.isObject()) return object;
// object to link the two objects together
Linker linker(_isolate, object);
// attach the objects
linker.attach(value);
// get the object
return result.ToLocalChecked();
}
/**
* Retrieve a property or function from the object
* @param property the property to retrieve
* @param info callback info
*/
v8::Intercepted Template::getProperty(v8::Local<v8::Name> property, const v8::PropertyCallbackInfo<v8::Value> &info)
{
// if the retrieved property is a symbol
if (property->IsSymbol()) return getSymbol(property.As<v8::Symbol>(), info);
// symbol-objects are a specific case that we do not expect to ever see in real life, but we can handle it as symbol like this
if (property->IsSymbolObject()) return getSymbol(v8::Local<v8::Symbol>::Cast(property.As<v8::SymbolObject>()->ValueOf()), info);
// we need the isolate a couple of times
auto *isolate = info.GetIsolate();
// handle-scope
Scope scope(isolate);
// the object that is being accessed
Php::Value object = Linker(isolate, info.This()).value();
// we expect an object or array now
if (!object.isObject() && !object.isArray()) return v8::Intercepted::kNo;
// convert to a utf8value to get the actual c-string
v8::Local<v8::String> prop = property.As<v8::String>();
// for use as c_str
v8::String::Utf8Value name(isolate, prop);
/**
* This is where it gets a little weird.
*
* PHP has the concept of "magic functions", these are sort
* of like operator overloading in C++, only much, much
* clumsier.
*
* The issue we have to work around is that, although it is
* possible to write an __isset method to define which properties
* are possible to retrieve with __get, there is no such sister
* function for __call, which means that as soon as the __call
* function is implemented, every function suddenly becomes
* callable. This is a problem, because we would be creating
* function objects to return to v8 instead of retrieving the
* properties as expected.
*
* Therefore we are using a three-step test. First we see if the
* method exists, and is callable. This filters out __call, so if
* this fails, we check to see if a property exists (or can be
* retrieved with __get). If that fails, we try again if it is
* callable (then it will be a __call for sure!)
*/
// avoid exceptions
try
{
// does the method exist, is it callable or is it a property
bool method_exists = object.isObject() && Php::call("method_exists", object, *name);
bool is_callable = object.isCallable(*name);
bool contains = object.contains(*name, name.length());
// does a property exist by the given name and is it not defined as a method?
if (contains && !method_exists)
{
// get the object property value
FromPhp value(isolate, object.get(*name, name.length()));
// convert it to a javascript handle and return it
info.GetReturnValue().Set(value);
// handled
return v8::Intercepted::kYes;
}
// is it a countable object we want the length off?
else if (std::strcmp(*name, "length") == 0 && (object.instanceOf("Countable") || object.isArray()))
{
// return the count from this object
info.GetReturnValue().Set(FromPhp(isolate, Php::call("count", object)));
// handled
return v8::Intercepted::kYes;
}
else if (object.instanceOf("ArrayAccess") && object.call("offsetExists", *name))
{
// get the object property value
FromPhp value(isolate, object.call("offsetGet", *name));
// use the array access to retrieve the property
info.GetReturnValue().Set(value);
// handled
return v8::Intercepted::kYes;
}
else if (object.isCallable("__toString") && (std::strcmp(*name, "valueOf") == 0 || std::strcmp(*name, "toString") == 0))
{
// handle the to-string conversion
return getString(info);
}
else if (is_callable)
{
// we goong to pass the "this" and method name via data
v8::Local<v8::Array> data = v8::Array::New(isolate, 2);
// store the method name and this-pointer in the data
data->Set(scope, 0, info.This()).Check();
data->Set(scope, 1, prop).Check();
// create a new function object (with the data holding "this" and the name)
auto func = v8::Function::New(scope, &Template::method, data).ToLocalChecked();
// create the function to be called
info.GetReturnValue().Set(func);
// handled
return v8::Intercepted::kYes;
}
else
{
// not handled
return v8::Intercepted::kNo;
}
}
catch (const Php::Exception &exception)
{
// pass on
isolate->ThrowException(Exception(isolate, exception));
// handled
return v8::Intercepted::kYes;
}
}
/**
* Retrieve a property or function from the object
* @param symbol the symbol to retrieve
* @param info callback info
*/
v8::Intercepted Template::getSymbol(v8::Local<v8::Symbol> symbol, const v8::PropertyCallbackInfo<v8::Value> &info)
{
// we need the isolate
auto *isolate = info.GetIsolate();
// create a handlescope
Scope scope(isolate);
// to-string conversions
if (symbol->Equals(scope, v8::Symbol::GetToStringTag(isolate)).FromMaybe(false)) return getString(info);
// to-primitive conversions are also treated as to-strings
if (symbol->Equals(scope, v8::Symbol::GetToPrimitive(isolate)).FromMaybe(false)) return getString(info);
// to-iterator conversions
if (symbol->Equals(scope, v8::Symbol::GetIterator(isolate)).FromMaybe(false)) return getIterator(info);
// not handled
return v8::Intercepted::kNo;
}
/**
* Convert to a string
* @param info callback info
*/
v8::Intercepted Template::getString(const v8::PropertyCallbackInfo<v8::Value> &info)
{
// we need the isolate
auto *isolate = info.GetIsolate();
// catch exceptions
try
{
// the object that is being accessed
Php::Value object = Linker(isolate, info.This()).value();
// only when underlying object can be converted to strings
if (!object.isObject() || !object.isCallable("__toString")) return v8::Intercepted::kNo;
// set the return-value
info.GetReturnValue().Set(FromPhp(isolate, object.call("__toString")));
}
catch (const Php::Exception &exception)
{
// pass the exception on to javascript userspace
isolate->ThrowException(Exception(isolate, exception));
}
// handled
return v8::Intercepted::kYes;
}
/**
* Convert to an iterator
* @param info callback info
*/
v8::Intercepted Template::getIterator(const v8::PropertyCallbackInfo<v8::Value> &info)
{
// we need the isolate
auto *isolate = info.GetIsolate();
// create a handlescope
Scope scope(isolate);
// the object that is being accessed
Php::Value object = Linker(isolate, info.This()).value();
// if it is not iterable
if (!object.instanceOf("Traversable") && !object.isArray()) return v8::Intercepted::kNo;
// an iterator should be a function
auto func = v8::Function::New(scope, [](const v8::FunctionCallbackInfo<v8::Value>& info) {
// we need the isolate
auto *isolate = info.GetIsolate();
// create a handlescope
Scope scope(isolate);
// avoid exceptions
try
{
// get original php object
Php::Value object = Linker(isolate, info.This()).value();
// the retval that needs updating
auto retval = info.GetReturnValue();
// if the object is already traversable
if (object.instanceOf("Iterator")) retval.Set(FromIterator(isolate, object).value());
// of a can indirectly retrieve the iterator?
else if (object.instanceOf("IteratorAggregate")) retval.Set(FromIterator(isolate, object.call("getIterator")).value());
// arrays themselves can be iterated
else if (object.isArray()) retval.Set(FromIterator(isolate, Php::Object("ArrayIterator", object)).value());
// this should not happen
else retval.Set(FromIterator(isolate, Php::Object("EmptyIterator")).value());
}
catch (const Php::Exception &exception)
{
// pass the exception on to javascript userspace
isolate->ThrowException(Exception(isolate, exception));
}
}).ToLocalChecked();
// use the array access to retrieve the property
info.GetReturnValue().Set(func);
// this has been handled by us
return v8::Intercepted::kYes;
}
/**
* Retrieve a property or function from the object
* @param index The index to find the property
* @param info callback info
* @return v8::Intercepted
*/
v8::Intercepted Template::getIndex(unsigned index, const v8::PropertyCallbackInfo<v8::Value> &info)
{
// some variables
auto *isolate = info.GetIsolate();
// handle scope
Scope scope(isolate);
// avoid exceptions
try
{
// the object that is being accessed
Php::Value object = Linker(isolate, info.This()).value();
// is the underlying variable an array?
if (object.isArray() && object.contains(index))
{
// make the call
FromPhp value(isolate, object.get(static_cast<int64_t>(index)));
// set the result
info.GetReturnValue().Set(v8::Global<v8::Value>(isolate, value));
// call was handled
return v8::Intercepted::kYes;
}
// is the underlying variable an ArrayAccess with this property?
if (object.isObject() && object.call("offsetExists", static_cast<int64_t>(index)))
{
// make the call
FromPhp value(isolate, object.call("offsetGet", static_cast<int64_t>(index)));
// set the result
info.GetReturnValue().Set(v8::Global<v8::Value>(isolate, value));
// call was handled
return v8::Intercepted::kYes;
}
// call was not handled
return v8::Intercepted::kNo;
}
catch (const Php::Exception &exception)
{
// pass the exception on to javascript userspace
isolate->ThrowException(Exception(isolate, exception));
// call was handled
return v8::Intercepted::kYes;
}
}
/**
* Set a property or function on the object
* @param property the property to update
* @param input the new property value
* @param info callback info
*/
v8::Intercepted Template::setProperty(v8::Local<v8::Name> property, v8::Local<v8::Value> input, const v8::PropertyCallbackInfo<void>& info)
{
// some variables we need a couple of times
auto isolate = info.GetIsolate();
// handle scope
Scope scope(isolate);
// We are calling into PHP space so we need to catch all exceptions
try
{
// the object that is being accessed
Php::Value object = Linker(isolate, info.This()).value();
// if the underlying variable is an array
if (object.isArray()) object.set(PhpVariable(isolate, input), PhpVariable(isolate, input));
// make the call
else object.call("offsetSet", PhpVariable(isolate, property), PhpVariable(isolate, input));
}
catch (const Php::Exception &exception)
{
// pass the exception on to javascript userspace
isolate->ThrowException(Exception(isolate, exception));
}
// call was handled
return v8::Intercepted::kYes;
}
/**
* Set a property or function on the object
* @param index The index to update
* @param input the new property value
* @param info callback info
*/
v8::Intercepted Template::setIndex(unsigned index, v8::Local<v8::Value> input, const v8::PropertyCallbackInfo<void>& info)
{
// some variables we need a couple of times
auto isolate = info.GetIsolate();
// handle scope
Scope scope(isolate);
// We are calling into PHP space so we need to catch all exceptions
try
{
// the object that is being accessed
Php::Value object = Linker(info.GetIsolate(), info.This()).value();
// the variable to set
PhpVariable value(isolate, input);
// if the underlying variable is an array
if (object.isArray()) object.set(static_cast<int64_t>(index), value);
// make the call
else object.call("offsetSet", static_cast<int64_t>(index), value);
}
catch (const Php::Exception &exception)
{
// pass the exception on to javascript userspace
isolate->ThrowException(Exception(isolate, exception));
}
// call was handled
return v8::Intercepted::kYes;
}
/**
* Retrieve a list of string properties for enumeration
* @param info callback info
*/
void Template::enumerateProperties(const v8::PropertyCallbackInfo<v8::Array> &info)
{
// some variables we need a couple of times
auto isolate = info.GetIsolate();
// handle scope
Scope scope(isolate);
// the object that is being accessed
Php::Value object = Linker(isolate, info.This()).value();
// create a new array to store all the properties
v8::Local<v8::Array> properties(v8::Array::New(isolate));
// there is no 'push' method on v8::Array, so we simply have
// to 'Set' the property with the correct index, declared here
uint32_t index = 0;
// iterate over the properties in the object
for (auto &property : object)
{
// we only care about string indices
if (!property.first.isString()) continue;
// add the property to the list
auto result = properties->Set(scope, index++, FromPhp(isolate, property.first));
// leap out on error
if (!result.IsJust() || !result.FromJust()) return;
}
// set the value as the 'return' parameter
info.GetReturnValue().Set(properties);
}
/**
* Retrieve a list of integer properties for enumeration
* @param info callback info
*/
void Template::enumerateIndexes(const v8::PropertyCallbackInfo<v8::Array> &info)
{
// some variables we need a couple of times
auto isolate = info.GetIsolate();
// handle scope
Scope scope(isolate);
// the object that is being accessed
Php::Value object = Linker(isolate, info.This()).value();
// create a new array to store all the properties
v8::Local<v8::Array> properties(v8::Array::New(isolate));
// there is no 'push' method on v8::Array, so we simply have
// to 'Set' the property with the correct index, declared here
uint32_t index = 0;
// iterate over the properties in the object
for (auto &property : object)
{
// we only care about integer indices
if (!property.first.isNumeric()) continue;
// add the property to the list
auto result = properties->Set(scope, index++, FromPhp(isolate, property.first));
// leap out on error
if (!result.IsJust() || !result.FromJust()) return;
}
// set the value as the 'return' parameter
info.GetReturnValue().Set(properties);
}
/**
* A function is called that happens to be a method
* @param into callback info
*/
void Template::method(const v8::FunctionCallbackInfo<v8::Value>& info)
{
// we need the isolate
auto *isolate = info.GetIsolate();
// we might need a scope
Scope scope(isolate);
// avoid exceptions
try
{
// the data is an array
v8::Local<v8::Array> data(info.Data().As<v8::Array>());
// the "this" object is stored in the data
auto self = data->Get(scope, 0).As<v8::Object>().ToLocalChecked();
auto prop = data->Get(scope, 1).As<v8::String>().ToLocalChecked();
// the object that is being accessed
Php::Value object = Linker(isolate, self).value();
// the callable
Php::Array callable({object, PhpVariable(isolate, prop)});
// call the function
auto result = Php::call("call_user_func_array", callable, PhpArray(info));
// store return value
info.GetReturnValue().Set(FromPhp(isolate, result));
}
catch (const Php::Exception &exception)
{
// pass the exception on to javascript userspace
isolate->ThrowException(Exception(isolate, exception));
}
}
/**
* The object is called as if it was a function
* @param into callback info
*/
void Template::call(const v8::FunctionCallbackInfo<v8::Value>& info)
{
// we need the isolate
auto *isolate = info.GetIsolate();
// create handle-scope
Scope scope(isolate);
// avoid exceptions
try
{
// the object that is being accessed
Php::Value object = Linker(isolate, info.This()).value();
// call the function
auto result = Php::call("call_user_func_array", object, PhpArray(info));
// store return value
info.GetReturnValue().Set(FromPhp(isolate, result));
}
catch (const Php::Exception &exception)
{
// pass the exception on to javascript userspace
isolate->ThrowException(Exception(isolate, exception));
}
}
/**
* End of namespace
*/
}