Passed
Pull Request — master (#837)
by
unknown
06:31 queued 32s
created

TTemplate::getDirective()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
1
<?php
2
/**
3
 * TTemplateManager and TTemplate class file
4
 *
5
 * @author Qiang Xue <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Web\UI;
11
12
use Prado\Prado;
13
use Prado\TComponent;
14
use Prado\TPropertyValue;
15
use Prado\Web\Javascripts\TJavaScriptLiteral;
16
use Prado\Exceptions\TConfigurationException;
17
use Prado\Exceptions\TException;
18
use Prado\Exceptions\TTemplateException;
19
20
/**
21
 * TTemplate implements PRADO template parsing logic.
22
 * A TTemplate object represents a parsed PRADO control template.
23
 * It can instantiate the template as child controls of a specified control.
24
 * The template format is like HTML, with the following special tags introduced,
25
 * - component tags: a component tag represents the configuration of a component.
26
 * The tag name is in the format of com:ComponentType, where ComponentType is the component
27
 * class name. Component tags must be well-formed. Attributes of the component tag
28
 * are treated as either property initial values, event handler attachment, or regular
29
 * tag attributes.
30
 * - property tags: property tags are used to set large block of attribute values.
31
 * The property tag name is in the format of <prop:AttributeName> where AttributeName
32
 * can be a property name, an event name or a regular tag attribute name.
33
 * - group subproperty tags: subproperties of a common property can be configured using
34
 * <prop:MainProperty SubProperty1="Value1" SubProperty2="Value2" .../>
35
 * - directive: directive specifies the property values for the template owner.
36
 * It is in the format of <%@ property name-value pairs %>;
37
 * - expressions: They are in the format of <%= PHP expression %> and <%% PHP statements %>
38
 * - comments: There are two kinds of comments, regular HTML comments and special template comments.
39
 * The former is in the format of <!-- comments -->, which will be treated as text strings.
40
 * The latter is in the format of <!-- comments --!>, which will be stripped out.
41
 *
42
 * Tags other than the above are not required to be well-formed.
43
 *
44
 * A TTemplate object represents a parsed PRADO template. To instantiate the template
45
 * for a particular control, call {@link instantiateIn($control)}, which
46
 * will create and intialize all components specified in the template and
47
 * set their parent as $control.
48
 *
49
 * @author Qiang Xue <[email protected]>
50
 * @since 3.0
51
 * @method \Prado\Web\Services\TPageService getService()
52
 */
53
class TTemplate extends \Prado\TApplicationComponent implements ITemplate
54
{
55
	/**
56
	 *  '<!--.*?--!>' - template comments
57
	 *  '<!--.*?-->'  - HTML comments
58
	 *	'<\/?com:([\w\.\\\]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/?>' - component tags
59
	 *	'<\/?prop:([\w\.\-]+)\s*>'  - property tags
60
	 *	'<%@\s*((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?")*)\s*%>'  - directives
61
	 *	'<%[%#~\/\\$=\\[](.*?)%>'  - expressions
62
	 *  '<prop:([\w\.\-]+)((?:\s*[\w\.\-]+=\'.*?\'|\s*[\w\.\-]+=".*?"|\s*[\w\.\-]+=<%.*?%>)*)\s*\/>' - group subproperty tags
63
	 */
64
	public const REGEX_RULES = '/<!--.*?--!>|<!---.*?--->|<\/?com:([\w\.\\\]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/?>|<\/?prop:([\w\.\-]+)\s*>|<%@\s*((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?")*)\s*%>|<%[%#~\/\\$=\\[](.*?)%>|<prop:([\w\.\-]+)((?:\s*[\w\.\-]+\s*=\s*\'.*?\'|\s*[\w\.\-]+\s*=\s*".*?"|\s*[\w\.\-]+\s*=\s*<%.*?%>)*)\s*\/>/msS';
65
66
	/**
67
	 * Different configurations of component property/event/attribute
68
	 */
69
	public const CONFIG_DATABIND = 0;
70
	public const CONFIG_EXPRESSION = 1;
71
	public const CONFIG_ASSET = 2;
72
	public const CONFIG_PARAMETER = 3;
73
	public const CONFIG_LOCALIZATION = 4;
74
	public const CONFIG_TEMPLATE = 5;
75
76
	/**
77
	 * @var array list of component tags and strings
78
	 */
79
	private $_tpl = [];
80
	/**
81
	 * @var array list of directive settings
82
	 */
83
	private $_directive = [];
84
	/**
85
	 * @var string context path
86
	 */
87
	private $_contextPath;
88
	/**
89
	 * @var string template file path (if available)
90
	 */
91
	private $_tplFile;
92
	/**
93
	 * @var int the line number that parsing starts from (internal use)
94
	 */
95
	private $_startingLine = 0;
96
	/**
97
	 * @var string template content to be parsed
98
	 */
99
	private $_content;
100
	/**
101
	 * @var bool tells whether the class and attributes should be validated before moving on	 */
102
	private $_attributevalidation = true;
103
	/**
104
	 * @var bool whether this template is a source template
105
	 */
106
	private $_sourceTemplate = true;
107
	/**
108
	 * @var string hash code of the template
109
	 */
110
	private $_hashCode = '';
111
112
	/**
113
	 * @var \Prado\Web\UI\TControl
114
	 */
115
	private $_tplControl;
116
	/**
117
	 * @var array<string>
118
	 */
119
	private $_includedFiles = [];
120
	/**
121
	 * @var array<int>
122
	 */
123
	private $_includeAtLine = [];
124
	/**
125
	 * @var array<int>
126
	 */
127
	private $_includeLines = [];
128
129
130
	/**
131
	 * Constructor.
132
	 * The template will be parsed after construction.
133
	 * @param string $template the template string
134
	 * @param string $contextPath the template context directory
135
	 * @param null|string $tplFile the template file, null if no file
136
	 * @param int $startingLine the line number that parsing starts from (internal use)
137
	 * @param bool $sourceTemplate whether this template is a source template, i.e., this template is loaded from
138
	 * some external storage rather than from within another template.
139
	 */
140
	public function __construct($template, $contextPath, $tplFile = null, $startingLine = 0, $sourceTemplate = true)
141
	{
142
		$this->_sourceTemplate = $sourceTemplate;
143
		$this->_contextPath = $contextPath;
144
		$this->_tplFile = $tplFile;
145
		$this->_startingLine = $startingLine;
146
		$this->_content = $template;
147
		$this->_hashCode = md5($template);
148
		parent::__construct();
149
		$this->parse($template);
150
		$this->_content = null; // reset to save memory
151
	}
152
153
	/**
154
	 * @return string  template file path if available, null otherwise.
155
	 */
156
	public function getTemplateFile()
157
	{
158
		return $this->_tplFile;
159
	}
160
161
	/**
162
	 * @return bool whether this template is a source template, i.e., this template is loaded from
163
	 * some external storage rather than from within another template.
164
	 */
165
	public function getIsSourceTemplate()
166
	{
167
		return $this->_sourceTemplate;
168
	}
169
170
	/**
171
	 * @return string context directory path
172
	 */
173
	public function getContextPath()
174
	{
175
		return $this->_contextPath;
176
	}
177
178
	/**
179
	 * @return array name-value pairs declared in the directive
180
	 */
181
	public function getDirective()
182
	{
183
		return $this->_directive;
184
	}
185
186
	/**
187
	 * @return string hash code that can be used to identify the template
188
	 */
189
	public function getHashCode()
190
	{
191
		return $this->_hashCode;
192
	}
193
194
	/**
195
	 * @return array the parsed template
196
	 */
197
	public function &getItems()
198
	{
199
		return $this->_tpl;
200
	}
201
202
	/**
203
	 * @return bool whether or not validation of the template is active
204
	 * @since 4.2.0
205
	 */
206
	public function getAttributeValidation()
207
	{
208
		return $this->_attributevalidation;
209
	}
210
211
	/**
212
	 * @param bool $value whether or not validation of the template is active
213
	 * @since 4.2.0
214
	 */
215
	public function setAttributeValidation($value)
216
	{
217
		$this->_attributevalidation = $value;
218
	}
219
220
	/**
221
	 * Instantiates the template.
222
	 * Content in the template will be instantiated as components and text strings
223
	 * and passed to the specified parent control.
224
	 * @param \Prado\Web\UI\TControl $tplControl the control who owns the template
225
	 * @param null|TControl $parentControl the control who will become the root parent of the controls on the template. If null, it uses the template control.
226
	 */
227
	public function instantiateIn($tplControl, $parentControl = null)
228
	{
229
		$this->_tplControl = $tplControl;
230
		if ($parentControl === null) {
231
			$parentControl = $tplControl;
232
		}
233
		if (($page = $tplControl->getPage()) === null) {
234
			$page = $this->getService()->getRequestedPage();
235
		}
236
		$controls = [];
237
		$directChildren = [];
238
		foreach ($this->_tpl as $key => $object) {
239
			if ($object[0] === -1) {
240
				$parent = $parentControl;
241
			} elseif (isset($controls[$object[0]])) {
242
				$parent = $controls[$object[0]];
243
			} else {
244
				continue;
245
			}
246
			if (isset($object[2])) {	// component
247
				$component = Prado::createComponent($object[1]);
248
				$properties = &$object[2];
249
				if ($component instanceof TControl) {
250
					if ($component instanceof \Prado\Web\UI\WebControls\TOutputCache) {
251
						$component->setCacheKeyPrefix($this->_hashCode . $key);
0 ignored issues
show
Bug introduced by
The method setCacheKeyPrefix() does not exist on Prado\Web\UI\TControl. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

251
						$component->/** @scrutinizer ignore-call */ 
252
                  setCacheKeyPrefix($this->_hashCode . $key);
Loading history...
252
					}
253
					$component->setTemplateControl($tplControl);
254
					if (isset($properties['id'])) {
255
						if (is_array($properties['id'])) {
256
							if ($properties['id'][0] === self::CONFIG_PARAMETER) {
257
								$properties['id'] = $this->getApplication()->getParameters()->itemAt($properties['id'][1]);
258
							} else {
259
								$properties['id'] = $component->evaluateExpression($properties['id'][1]);
260
							}
261
						}
262
						$tplControl->registerObject($properties['id'], $component);
263
					}
264
					if (isset($properties['skinid'])) {
265
						if (is_array($properties['skinid'])) {
266
							if ($properties['skinid'][0] === self::CONFIG_PARAMETER) {
267
								$properties['skinid'] = $this->getApplication()->getParameters()->itemAt($properties['skinid'][1]);
268
							} else {
269
								$properties['skinid'] = $component->evaluateExpression($properties['skinid'][1]);
270
							}
271
						}
272
						$component->setSkinID($properties['skinid']);
273
						unset($properties['skinid']);
274
					}
275
276
					$component->trackViewState(false);
277
278
					$component->applyStyleSheetSkin($page);
279
					foreach ($properties as $name => $value) {
280
						$this->configureControl($component, $name, $value);
281
					}
282
283
					$component->trackViewState(true);
284
285
					if ($parent === $parentControl) {
286
						$directChildren[] = $component;
287
					} else {
288
						$component->createdOnTemplate($parent);
289
					}
290
					if ($component->getAllowChildControls()) {
291
						$controls[$key] = $component;
292
					}
293
				} elseif ($component instanceof TComponent) {
294
					$controls[$key] = $component;
295
					if (isset($properties['id'])) {
296
						if (is_array($properties['id'])) {
297
							if ($properties['id'][0] === self::CONFIG_PARAMETER) {
298
								$properties['id'] = $this->getApplication()->getParameters()->itemAt($properties['id'][1]);
299
							} else {
300
								$properties['id'] = $component->evaluateExpression($properties['id'][1]);
301
							}
302
						}
303
						$tplControl->registerObject($properties['id'], $component);
304
						if (!$component->hasProperty('id')) {
305
							unset($properties['id']);
306
						}
307
					}
308
					foreach ($properties as $name => $value) {
309
						$this->configureComponent($component, $name, $value);
310
					}
311
					if ($parent === $parentControl) {
312
						$directChildren[] = $component;
313
					} else {
314
						$component->createdOnTemplate($parent);
315
					}
316
				}
317
			} else {
318
				if ($object[1] instanceof TCompositeLiteral) {
319
					// need to clone a new object because the one in template is reused
320
					$o = clone $object[1];
321
					$o->setContainer($tplControl);
322
					if ($parent === $parentControl) {
323
						$directChildren[] = $o;
324
					} else {
325
						$parent->addParsedObject($o);
326
					}
327
				} else {
328
					if ($parent === $parentControl) {
329
						$directChildren[] = $object[1];
330
					} else {
331
						$parent->addParsedObject($object[1]);
332
					}
333
				}
334
			}
335
		}
336
		// delay setting parent till now because the parent may cause
337
		// the child to do lifecycle catchup which may cause problem
338
		// if the child needs its own child controls.
339
		foreach ($directChildren as $control) {
340
			if ($control instanceof TComponent) {
341
				$control->createdOnTemplate($parentControl);
342
			} else {
343
				$parentControl->addParsedObject($control);
344
			}
345
		}
346
	}
347
348
	/**
349
	 * Configures a property/event of a control.
350
	 * @param \Prado\Web\UI\TControl $control control to be configured
351
	 * @param string $name property name
352
	 * @param mixed $value property initial value
353
	 */
354
	protected function configureControl($control, $name, $value)
355
	{
356
		if (strncasecmp($name, 'on', 2) === 0) {		// is an event
357
			$this->configureEvent($control, $name, $value, $control);
358
		} elseif (($pos = strrpos($name, '.')) === false) {	// is a simple property or custom attribute
0 ignored issues
show
Unused Code introduced by
The assignment to $pos is dead and can be removed.
Loading history...
359
			$this->configureProperty($control, $name, $value);
360
		} else {	// is a subproperty
361
			$this->configureSubProperty($control, $name, $value);
362
		}
363
	}
364
365
	/**
366
	 * Configures a property of a non-control component.
367
	 * @param \Prado\TComponent $component component to be configured
368
	 * @param string $name property name
369
	 * @param mixed $value property initial value
370
	 */
371
	protected function configureComponent($component, $name, $value)
372
	{
373
		if (strpos($name, '.') === false) {	// is a simple property or custom attribute
374
			$this->configureProperty($component, $name, $value);
375
		} else {	// is a subproperty
376
			$this->configureSubProperty($component, $name, $value);
377
		}
378
	}
379
380
	/**
381
	 * Configures an event for a control.
382
	 * @param \Prado\Web\UI\TControl $control control to be configured
383
	 * @param string $name event name
384
	 * @param string $value event handler
385
	 * @param \Prado\Web\UI\TControl $contextControl context control
386
	 */
387
	protected function configureEvent($control, $name, $value, $contextControl)
388
	{
389
		if (strpos($value, '.') === false) {
390
			$control->attachEventHandler($name, [$contextControl, 'TemplateControl.' . $value]);
391
		} else {
392
			$control->attachEventHandler($name, [$contextControl, $value]);
393
		}
394
	}
395
396
	/**
397
	 * Configures a simple property for a component.
398
	 * @param \Prado\Web\UI\TControl $component component to be configured
399
	 * @param string $name property name
400
	 * @param mixed $value property initial value
401
	 */
402
	protected function configureProperty($component, $name, $value)
403
	{
404
		if (is_array($value)) {
405
			switch ($value[0]) {
406
				case self::CONFIG_DATABIND:
407
					$component->bindProperty($name, $value[1]);
408
					break;
409
				case self::CONFIG_EXPRESSION:
410
					if ($component instanceof TControl) {
0 ignored issues
show
introduced by
$component is always a sub-type of Prado\Web\UI\TControl.
Loading history...
411
						$component->autoBindProperty($name, $value[1]);
412
					} else {
413
						$setter = 'set' . $name;
414
						$component->$setter($this->_tplControl->evaluateExpression($value[1]));
415
					}
416
					break;
417
				case self::CONFIG_TEMPLATE:
418
					$setter = 'set' . $name;
419
					$component->$setter($value[1]);
420
					break;
421
				case self::CONFIG_ASSET:		// asset URL
422
					$setter = 'set' . $name;
423
					$url = $this->publishFilePath($this->_contextPath . DIRECTORY_SEPARATOR . $value[1]);
424
					$component->$setter($url);
425
					break;
426
				case self::CONFIG_PARAMETER:		// application parameter
427
					$setter = 'set' . $name;
428
					$component->$setter($this->getApplication()->getParameters()->itemAt($value[1]));
429
					break;
430
				case self::CONFIG_LOCALIZATION:
431
					$setter = 'set' . $name;
432
					$component->$setter(Prado::localize($value[1]));
433
					break;
434
				default:	// an error if reaching here
435
					throw new TConfigurationException('template_tag_unexpected', $name, $value[1]);
436
					break;
437
			}
438
		} else {
439
			if (substr($name, 0, 2) == 'js') {
440
				if ($value && !($value instanceof TJavaScriptLiteral)) {
441
					$value = new TJavaScriptLiteral($value);
442
				}
443
			}
444
			$setter = 'set' . $name;
445
			if (method_exists($component, $setter)) {
446
				if ($reflector = new \ReflectionClass($component)) {
447
					try {
448
						$params = $reflector->getMethod($setter)->getParameters();
449
						if (!empty($params) && isset($params[0]) && $type = $params[0]->getType()) {
450
							if ($type instanceof \ReflectionNamedType) {
451
								switch ($type->getName()) {
452
									case 'bool':
453
										$value = TPropertyValue::ensureBoolean($value);
454
										break;
455
									case 'int':
456
										$value = TPropertyValue::ensureInteger($value);
457
										break;
458
									case 'float':
459
										$value = TPropertyValue::ensureFloat($value);
460
										break;
461
									case 'string':
462
										$value = TPropertyValue::ensureString($value);
463
										break;
464
								}
465
							}
466
						}
467
					} catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
468
					}
469
				}
470
			}
471
			$component->$setter($value);
472
		}
473
	}
474
475
	/**
476
	 * Configures a subproperty for a component.
477
	 * @param \Prado\Web\UI\TControl $component component to be configured
478
	 * @param string $name subproperty name
479
	 * @param mixed $value subproperty initial value
480
	 */
481
	protected function configureSubProperty($component, $name, $value)
482
	{
483
		if (is_array($value)) {
484
			switch ($value[0]) {
485
				case self::CONFIG_DATABIND:		// databinding
486
					$component->bindProperty($name, $value[1]);
487
					break;
488
				case self::CONFIG_EXPRESSION:		// expression
489
					if ($component instanceof TControl) {
0 ignored issues
show
introduced by
$component is always a sub-type of Prado\Web\UI\TControl.
Loading history...
490
						$component->autoBindProperty($name, $value[1]);
491
					} else {
492
						$component->setSubProperty($name, $this->_tplControl->evaluateExpression($value[1]));
493
					}
494
					break;
495
				case self::CONFIG_TEMPLATE:
496
					$component->setSubProperty($name, $value[1]);
497
					break;
498
				case self::CONFIG_ASSET:		// asset URL
499
					$url = $this->publishFilePath($this->_contextPath . DIRECTORY_SEPARATOR . $value[1]);
500
					$component->setSubProperty($name, $url);
501
					break;
502
				case self::CONFIG_PARAMETER:		// application parameter
503
					$component->setSubProperty($name, $this->getApplication()->getParameters()->itemAt($value[1]));
504
					break;
505
				case self::CONFIG_LOCALIZATION:
506
					$component->setSubProperty($name, Prado::localize($value[1]));
507
					break;
508
				default:	// an error if reaching here
509
					throw new TConfigurationException('template_tag_unexpected', $name, $value[1]);
510
					break;
511
			}
512
		} else {
513
			$component->setSubProperty($name, $value);
514
		}
515
	}
516
517
	/**
518
	 * Parses a template string.
519
	 *
520
	 * This template parser recognizes five types of data:
521
	 * regular string, well-formed component tags, well-formed property tags, directives, and expressions.
522
	 *
523
	 * The parsing result is returned as an array. Each array element can be of three types:
524
	 * - a string, 0: container index; 1: string content;
525
	 * - a component tag, 0: container index; 1: component type; 2: attributes (name=>value pairs)
526
	 * If a directive is found in the template, it will be parsed and can be
527
	 * retrieved via {@link getDirective}, which returns an array consisting of
528
	 * name-value pairs in the directive.
529
	 *
530
	 * Note, attribute names are treated as case-insensitive and will be turned into lower cases.
531
	 * Component and directive types are case-sensitive.
532
	 * Container index is the index to the array element that stores the container object.
533
	 * If an object has no container, its container index is -1.
534
	 *
535
	 * @param string $input the template string
536
	 * @throws TConfigurationException if a parsing error is encountered
537
	 */
538
	protected function parse($input)
539
	{
540
		$input = $this->preprocess($input);
541
		$tpl = &$this->_tpl;
542
		$n = preg_match_all(self::REGEX_RULES, $input, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
543
		$expectPropEnd = false;
544
		$textStart = 0;
545
		$stack = [];
546
		$container = -1;
547
		$matchEnd = 0;
548
		$c = 0;
549
		$this->_directive = null;
550
		try {
551
			for ($i = 0; $i < $n; ++$i) {
552
				$match = &$matches[$i];
553
				$str = $match[0][0];
554
				$matchStart = $match[0][1];
555
				$matchEnd = $matchStart + strlen($str) - 1;
556
				if (strpos($str, '<com:') === 0) {	// opening component tag
557
					if ($expectPropEnd) {
558
						continue;
559
					}
560
					if ($matchStart > $textStart) {
561
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
562
					}
563
					$textStart = $matchEnd + 1;
564
					$type = $match[1][0];
565
					$attributes = $this->parseAttributes($match[2][0], $match[2][1]);
566
					$class = $this->validateAttributes($type, $attributes);
567
					$tpl[$c++] = [$container, $class, $attributes];
568
					if ($str[strlen($str) - 2] !== '/') {  // open tag
569
						$stack[] = $type;
570
						$container = $c - 1;
571
					}
572
				} elseif (strpos($str, '</com:') === 0) {	// closing component tag
573
					if ($expectPropEnd) {
574
						continue;
575
					}
576
					if ($matchStart > $textStart) {
577
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
578
					}
579
					$textStart = $matchEnd + 1;
580
					$type = $match[1][0];
581
582
					if (empty($stack)) {
583
						throw new TConfigurationException('template_closingtag_unexpected', "</com:$type>");
584
					}
585
586
					$name = array_pop($stack);
587
					if ($name !== $type) {
588
						$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
589
						throw new TConfigurationException('template_closingtag_expected', $tag, "</com:$type>");
590
					}
591
					$container = $tpl[$container][0];
592
				} elseif (strpos($str, '<%@') === 0) {	// directive
593
					if ($expectPropEnd) {
594
						continue;
595
					}
596
					if ($matchStart > $textStart) {
597
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
598
					}
599
					$textStart = $matchEnd + 1;
600
					if (isset($tpl[0]) || $this->_directive !== null) {
601
						throw new TConfigurationException('template_directive_nonunique');
602
					}
603
					$this->_directive = $this->parseAttributes($match[4][0], $match[4][1]);
604
				} elseif (strpos($str, '<%') === 0) {	// expression
605
					if ($expectPropEnd) {
606
						continue;
607
					}
608
					if ($matchStart > $textStart) {
609
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
610
					}
611
					$textStart = $matchEnd + 1;
612
					$literal = trim($match[5][0]);
613
					if ($str[2] === '=') {	// expression
614
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, $literal]];
615
					} elseif ($str[2] === '%') {  // statements
616
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_STATEMENTS, $literal]];
617
					} elseif ($str[2] === '#') {
618
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_DATABINDING, $literal]];
619
					} elseif ($str[2] === '$') {
620
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "\$this->getApplication()->getParameters()->itemAt('$literal')"]];
621
					} elseif ($str[2] === '~') {
622
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "\$this->publishFilePath('$this->_contextPath/$literal')"]];
623
					} elseif ($str[2] === '/') {
624
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "rtrim(dirname(\$this->getApplication()->getRequest()->getApplicationUrl()), '\/').'/$literal'"]];
625
					} elseif ($str[2] === '[') {
626
						$literal = strtr(trim(substr($literal, 0, strlen($literal) - 1)), ["'" => "\'", "\\" => "\\\\"]);
627
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "Prado::localize('$literal')"]];
628
					}
629
				} elseif (strpos($str, '<prop:') === 0) {	// opening property
630
					if (strrpos($str, '/>') === strlen($str) - 2) {  //subproperties
631
						if ($expectPropEnd) {
632
							continue;
633
						}
634
						if ($matchStart > $textStart) {
635
							$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
636
						}
637
						$textStart = $matchEnd + 1;
638
						$prop = strtolower($match[6][0]);
639
						$attrs = $this->parseAttributes($match[7][0], $match[7][1]);
640
						$attributes = [];
641
						foreach ($attrs as $name => $value) {
642
							$attributes[$prop . '.' . $name] = $value;
643
						}
644
						$type = $tpl[$container][1];
645
						$this->validateAttributes($type, $attributes);
646
						foreach ($attributes as $name => $value) {
647
							if (isset($tpl[$container][2][$name])) {
648
								throw new TConfigurationException('template_property_duplicated', $name);
649
							}
650
							$tpl[$container][2][$name] = $value;
651
						}
652
					} else {  // regular property
653
						$prop = strtolower($match[3][0]);
654
						$stack[] = '@' . $prop;
655
						if (!$expectPropEnd) {
656
							if ($matchStart > $textStart) {
657
								$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
658
							}
659
							$textStart = $matchEnd + 1;
660
							$expectPropEnd = true;
661
						}
662
					}
663
				} elseif (strpos($str, '</prop:') === 0) {	// closing property
664
					$prop = strtolower($match[3][0]);
665
					if (empty($stack)) {
666
						throw new TConfigurationException('template_closingtag_unexpected', "</prop:$prop>");
667
					}
668
					$name = array_pop($stack);
669
					if ($name !== '@' . $prop) {
670
						$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
671
						throw new TConfigurationException('template_closingtag_expected', $tag, "</prop:$prop>");
672
					}
673
					if (($last = count($stack)) < 1 || $stack[$last - 1][0] !== '@') {
674
						if ($matchStart > $textStart) {
675
							$value = substr($input, $textStart, $matchStart - $textStart);
676
							if (substr($prop, -8, 8) === 'template') {
677
								$value = $this->parseTemplateProperty($value, $textStart);
678
							} else {
679
								$value = $this->parseAttribute($value);
680
							}
681
							if ($container >= 0) {
682
								$type = $tpl[$container][1];
683
								$this->validateAttributes($type, [$prop => $value]);
684
								if (isset($tpl[$container][2][$prop])) {
685
									throw new TConfigurationException('template_property_duplicated', $prop);
686
								}
687
								$tpl[$container][2][$prop] = $value;
688
							} else {	// a property for the template control
689
								$this->_directive[$prop] = $value;
690
							}
691
							$textStart = $matchEnd + 1;
692
						}
693
						$expectPropEnd = false;
694
					}
695
				} elseif (strpos($str, '<!--') === 0) {	// comments
696
					if ($expectPropEnd) {
697
						throw new TConfigurationException('template_comments_forbidden');
698
					}
699
					if ($matchStart > $textStart) {
700
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
701
					}
702
					$textStart = $matchEnd + 1;
703
				} else {
704
					throw new TConfigurationException('template_matching_unexpected', $match);
705
				}
706
			}
707
			if (!empty($stack)) {
708
				$name = array_pop($stack);
709
				$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
710
				throw new TConfigurationException('template_closingtag_expected', $tag, "nothing");
711
			}
712
			if ($textStart < strlen($input)) {
713
				$tpl[$c++] = [$container, substr($input, $textStart)];
714
			}
715
		} catch (\Exception $e) {
716
			if (($e instanceof TException) && ($e instanceof TTemplateException)) {
717
				throw $e;
718
			}
719
			if ($matchEnd === 0) {
720
				$line = $this->_startingLine + 1;
721
			} else {
722
				$line = $this->_startingLine + count(explode("\n", substr($input, 0, $matchEnd + 1)));
723
			}
724
			$this->handleException($e, $line, $input);
725
		}
726
727
		if ($this->_directive === null) {
728
			$this->_directive = [];
729
		}
730
731
		// optimization by merging consecutive strings, expressions, statements and bindings
732
		$objects = [];
733
		$parent = null;
734
		$id = null;
735
		$merged = [];
736
		foreach ($tpl as $id => $object) {
737
			if (isset($object[2]) || $object[0] !== $parent) {
738
				if ($parent !== null) {
739
					if (count($merged[1]) === 1 && is_string($merged[1][0])) {
740
						$objects[$id - 1] = [$merged[0], $merged[1][0]];
741
					} else {
742
						$objects[$id - 1] = [$merged[0], new TCompositeLiteral($merged[1])];
743
					}
744
				}
745
				if (isset($object[2])) {
746
					$parent = null;
747
					$objects[$id] = $object;
748
				} else {
749
					$parent = $object[0];
750
					$merged = [$parent, [$object[1]]];
751
				}
752
			} else {
753
				$merged[1][] = $object[1];
754
			}
755
		}
756
		if ($parent !== null && $id !== null) {
757
			if (count($merged[1]) === 1 && is_string($merged[1][0])) {
758
				$objects[$id] = [$merged[0], $merged[1][0]];
759
			} else {
760
				$objects[$id] = [$merged[0], new TCompositeLiteral($merged[1])];
761
			}
762
		}
763
		$tpl = $objects;
764
		return $objects;
765
	}
766
767
	/**
768
	 * Parses the attributes of a tag from a string.
769
	 * @param string $str the string to be parsed.
770
	 * @param mixed $offset
771
	 * @return array attribute values indexed by names.
772
	 */
773
	protected function parseAttributes($str, $offset)
774
	{
775
		if ($str === '') {
776
			return [];
777
		}
778
		$pattern = '/([\w\.\-]+)\s*=\s*(\'.*?\'|".*?"|<%.*?%>)/msS';
779
		$attributes = [];
780
		$n = preg_match_all($pattern, $str, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
781
		for ($i = 0; $i < $n; ++$i) {
782
			$match = &$matches[$i];
783
			$name = strtolower($match[1][0]);
784
			if (isset($attributes[$name])) {
785
				throw new TConfigurationException('template_property_duplicated', $name);
786
			}
787
			$value = $match[2][0];
788
			if (substr($name, -8, 8) === 'template') {
789
				if ($value[0] === '\'' || $value[0] === '"') {
790
					$attributes[$name] = $this->parseTemplateProperty(substr($value, 1, strlen($value) - 2), $match[2][1] + 1);
791
				} else {
792
					$attributes[$name] = $this->parseTemplateProperty($value, $match[2][1]);
793
				}
794
			} else {
795
				if ($value[0] === '\'' || $value[0] === '"') {
796
					$attributes[$name] = $this->parseAttribute(substr($value, 1, strlen($value) - 2));
797
				} else {
798
					$attributes[$name] = $this->parseAttribute($value);
799
				}
800
			}
801
		}
802
		return $attributes;
803
	}
804
805
	protected function parseTemplateProperty($content, $offset)
806
	{
807
		$line = $this->_startingLine + count(explode("\n", substr($this->_content, 0, $offset))) - 1;
808
		return [self::CONFIG_TEMPLATE, new TTemplate($content, $this->_contextPath, $this->_tplFile, $line, false)];
809
	}
810
811
	/**
812
	 * Parses a single attribute.
813
	 * @param string $value the string to be parsed.
814
	 * @return array attribute initialization
815
	 */
816
	protected function parseAttribute($value)
817
	{
818
		if (($n = preg_match_all('/<%[#=].*?%>/msS', $value, $matches, PREG_OFFSET_CAPTURE)) > 0) {
819
			$isDataBind = false;
820
			$textStart = 0;
821
			$expr = '';
822
			for ($i = 0; $i < $n; ++$i) {
823
				$match = $matches[0][$i];
824
				$token = $match[0];
825
				$offset = $match[1];
826
				$length = strlen($token);
827
				if ($token[2] === '#') {
828
					$isDataBind = true;
829
				}
830
				if ($offset > $textStart) {
831
					$expr .= ".'" . strtr(substr($value, $textStart, $offset - $textStart), ["'" => "\\'", "\\" => "\\\\"]) . "'";
832
				}
833
				$expr .= '.(' . substr($token, 3, $length - 5) . ')';
834
				$textStart = $offset + $length;
835
			}
836
			$length = strlen($value);
837
			if ($length > $textStart) {
838
				$expr .= ".'" . strtr(substr($value, $textStart, $length - $textStart), ["'" => "\\'", "\\" => "\\\\"]) . "'";
839
			}
840
			if ($isDataBind) {
841
				return [self::CONFIG_DATABIND, ltrim($expr, '.')];
842
			} else {
843
				return [self::CONFIG_EXPRESSION, ltrim($expr, '.')];
844
			}
845
		} elseif (preg_match('/\\s*(<%~.*?%>|<%\\$.*?%>|<%\\[.*?\\]%>|<%\/.*?%>)\\s*/msS', $value, $matches) && $matches[0] === $value) {
846
			$value = $matches[1];
847
			if ($value[2] === '~') {
848
				return [self::CONFIG_ASSET, trim(substr($value, 3, strlen($value) - 5))];
849
			} elseif ($value[2] === '[') {
850
				return [self::CONFIG_LOCALIZATION, trim(substr($value, 3, strlen($value) - 6))];
851
			} elseif ($value[2] === '$') {
852
				return [self::CONFIG_PARAMETER, trim(substr($value, 3, strlen($value) - 5))];
853
			} elseif ($value[2] === '/') {
854
				$literal = trim(substr($value, 3, strlen($value) - 5));
855
				return [self::CONFIG_EXPRESSION, "rtrim(dirname(\$this->getApplication()->getRequest()->getApplicationUrl()), '\/').'/$literal'"];
856
			}
857
		}
858
		return $value;
859
	}
860
861
	protected function validateAttributes($type, $attributes)
862
	{
863
		Prado::using($type);
864
		if (($pos = strrpos($type, '.')) !== false) {
865
			$className = substr($type, $pos + 1);
866
		} else {
867
			$className = $type;
868
		}
869
		$class = new \ReflectionClass($className);
870
		if (!$this->_attributevalidation) {
871
			return $class->getName();
872
		}
873
		if (is_subclass_of($className, '\Prado\Web\UI\TControl') || $className === '\Prado\Web\UI\TControl') {
874
			foreach ($attributes as $name => $att) {
875
				if (($pos = strpos($name, '.')) !== false) {
876
					// a subproperty, so the first segment must be readable
877
					$subname = substr($name, 0, $pos);
878
					if (!$class->hasMethod('get' . $subname)) {
879
						throw new TConfigurationException('template_property_unknown', $type, $subname);
880
					}
881
				} elseif (strncasecmp($name, 'on', 2) === 0) {
882
					// an event
883
					if (!$class->hasMethod($name)) {
884
						throw new TConfigurationException('template_event_unknown', $type, $name);
885
					} elseif (!is_string($att)) {
886
						throw new TConfigurationException('template_eventhandler_invalid', $type, $name);
887
					}
888
				} else {
889
					// a simple property
890
					if (!($class->hasMethod('set' . $name) || $class->hasMethod('setjs' . $name) || $this->isClassBehaviorMethod($class, 'set' . $name))) {
891
						if ($class->hasMethod('get' . $name) || $class->hasMethod('getjs' . $name)) {
892
							throw new TConfigurationException('template_property_readonly', $type, $name);
893
						} else {
894
							throw new TConfigurationException('template_property_unknown', $type, $name);
895
						}
896
					} elseif (is_array($att) && $att[0] !== self::CONFIG_EXPRESSION && $att[0] !== self::CONFIG_PARAMETER) {
897
						if (strcasecmp($name, 'id') === 0) {
898
							throw new TConfigurationException('template_controlid_invalid', $type);
899
						} elseif (strcasecmp($name, 'skinid') === 0) {
900
							throw new TConfigurationException('template_controlskinid_invalid', $type);
901
						}
902
					}
903
				}
904
			}
905
		} elseif (is_subclass_of($className, '\Prado\TComponent') || $className === '\Prado\TComponent') {
906
			foreach ($attributes as $name => $att) {
907
				if (is_array($att) && ($att[0] === self::CONFIG_DATABIND)) {
908
					throw new TConfigurationException('template_databind_forbidden', $type, $name);
909
				}
910
				if (($pos = strpos($name, '.')) !== false) {
911
					// a subproperty, so the first segment must be readable
912
					$subname = substr($name, 0, $pos);
913
					if (!$class->hasMethod('get' . $subname)) {
914
						throw new TConfigurationException('template_property_unknown', $type, $subname);
915
					}
916
				} elseif (strncasecmp($name, 'on', 2) === 0) {
917
					throw new TConfigurationException('template_event_forbidden', $type, $name);
918
				} else {
919
					// id is still allowed for TComponent, even if id property doesn't exist
920
					if (strcasecmp($name, 'id') !== 0 && !($class->hasMethod('set' . $name) || $this->isClassBehaviorMethod($class, 'set' . $name))) {
921
						if ($class->hasMethod('get' . $name)) {
922
							throw new TConfigurationException('template_property_readonly', $type, $name);
923
						} else {
924
							throw new TConfigurationException('template_property_unknown', $type, $name);
925
						}
926
					}
927
				}
928
			}
929
		} else {
930
			throw new TConfigurationException('template_component_required', $type);
931
		}
932
		return $class->getName();
933
	}
934
935
	/**
936
	 * @return array list of included external template files
937
	 */
938
	public function getIncludedFiles()
939
	{
940
		return $this->_includedFiles;
941
	}
942
943
	/**
944
	 * Handles template parsing exception.
945
	 * This method rethrows the exception caught during template parsing.
946
	 * It adjusts the error location by giving out correct error line number and source file.
947
	 * @param \Exception $e template exception
948
	 * @param int $line line number
949
	 * @param null|string $input template string if no source file is used
950
	 */
951
	protected function handleException($e, $line, $input = null)
952
	{
953
		$srcFile = $this->_tplFile;
954
955
		if (($n = count($this->_includedFiles)) > 0) { // need to adjust error row number and file name
956
			for ($i = $n - 1; $i >= 0; --$i) {
957
				if ($this->_includeAtLine[$i] <= $line) {
958
					if ($line < $this->_includeAtLine[$i] + $this->_includeLines[$i]) {
959
						$line = $line - $this->_includeAtLine[$i] + 1;
960
						$srcFile = $this->_includedFiles[$i];
961
						break;
962
					} else {
963
						$line = $line - $this->_includeLines[$i] + 1;
964
					}
965
				}
966
			}
967
		}
968
		$exception = new TTemplateException('template_format_invalid', $e->getMessage());
969
		$exception->setLineNumber($line);
970
		if (!empty($srcFile)) {
971
			$exception->setTemplateFile($srcFile);
972
		} else {
973
			$exception->setTemplateSource($input);
974
		}
975
		throw $exception;
976
	}
977
978
	/**
979
	 * Preprocesses the template string by including external templates
980
	 * @param string $input template string
981
	 * @return string expanded template string
982
	 */
983
	protected function preprocess($input)
984
	{
985
		if ($n = preg_match_all('/<%include(.*?)%>/', $input, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
986
			for ($i = 0; $i < $n; ++$i) {
987
				$filePath = Prado::getPathOfNamespace(trim($matches[$i][1][0]), TTemplateManager::TEMPLATE_FILE_EXT);
988
				if ($filePath !== null && is_file($filePath)) {
989
					$this->_includedFiles[] = $filePath;
990
				} else {
991
					$errorLine = count(explode("\n", substr($input, 0, $matches[$i][0][1] + 1)));
992
					$this->handleException(new TConfigurationException('template_include_invalid', trim($matches[$i][1][0])), $errorLine, $input);
993
				}
994
			}
995
			$base = 0;
996
			for ($i = 0; $i < $n; ++$i) {
997
				$ext = file_get_contents($this->_includedFiles[$i]);
998
				$length = strlen($matches[$i][0][0]);
999
				$offset = $base + $matches[$i][0][1];
1000
				$this->_includeAtLine[$i] = count(explode("\n", substr($input, 0, $offset)));
1001
				$this->_includeLines[$i] = count(explode("\n", $ext));
1002
				$input = substr_replace($input, $ext, $offset, $length);
1003
				$base += strlen($ext) - $length;
1004
			}
1005
		}
1006
1007
		return $input;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $input also could return the type array which is incompatible with the documented return type string.
Loading history...
1008
	}
1009
1010
	/**
1011
	 * Checks if the given method belongs to a previously attached class behavior.
1012
	 * @param \ReflectionClass $class
1013
	 * @param string $method
1014
	 * @return bool
1015
	 */
1016
	protected function isClassBehaviorMethod(\ReflectionClass $class, $method)
1017
	{
1018
		$component = new \ReflectionClass('\Prado\TComponent');
1019
		$behaviors = $component->getStaticProperties();
1020
		if (!isset($behaviors['_um'])) {
1021
			return false;
1022
		}
1023
		foreach ($behaviors['_um'] as $name => $list) {
1024
			if (strtolower($class->getShortName()) !== $name && !$class->isSubclassOf($name)) {
1025
				continue;
1026
			}
1027
			foreach ($list as $param) {
1028
				$behavior = $param->getBehavior();
1029
				if (is_array($behavior)) {
1030
					if (method_exists($behavior['class'], $method)) {
1031
						return true;
1032
					}
1033
				} elseif (method_exists($behavior, $method)) {
1034
					return true;
1035
				}
1036
			}
1037
		}
1038
		return false;
1039
	}
1040
}
1041