Completed
Branch master (099915)
by Fabio
08:02
created

TTemplate::preprocess()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

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

198
			$page = $this->getService()->/** @scrutinizer ignore-call */ getRequestedPage();
Loading history...
199
		}
200
		$controls = [];
201
		$directChildren = [];
202
		foreach ($this->_tpl as $key => $object) {
203
			if ($object[0] === -1) {
204
				$parent = $parentControl;
205
			} elseif (isset($controls[$object[0]])) {
206
				$parent = $controls[$object[0]];
207
			} else {
208
				continue;
209
			}
210
			if (isset($object[2])) {	// component
211
				$component = Prado::createComponent($object[1]);
212
				$properties = &$object[2];
213
				if ($component instanceof TControl) {
214
					if ($component instanceof \Prado\Web\UI\WebControls\TOutputCache) {
215
						$component->setCacheKeyPrefix($this->_hashCode . $key);
216
					}
217
					$component->setTemplateControl($tplControl);
0 ignored issues
show
Bug introduced by
The method setTemplateControl() does not exist on Prado\TComponent. 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

217
					$component->/** @scrutinizer ignore-call */ 
218
                 setTemplateControl($tplControl);
Loading history...
218
					if (isset($properties['id'])) {
219
						if (is_array($properties['id'])) {
220
							$properties['id'] = $component->evaluateExpression($properties['id'][1]);
221
						}
222
						$tplControl->registerObject($properties['id'], $component);
223
					}
224
					if (isset($properties['skinid'])) {
225
						if (is_array($properties['skinid'])) {
226
							$component->setSkinID($component->evaluateExpression($properties['skinid'][1]));
0 ignored issues
show
Bug introduced by
The method setSkinID() does not exist on Prado\TComponent. 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

226
							$component->/** @scrutinizer ignore-call */ 
227
                   setSkinID($component->evaluateExpression($properties['skinid'][1]));
Loading history...
227
						} else {
228
							$component->setSkinID($properties['skinid']);
229
						}
230
						unset($properties['skinid']);
231
					}
232
233
					$component->trackViewState(false);
0 ignored issues
show
Bug introduced by
The method trackViewState() does not exist on Prado\TComponent. 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

233
					$component->/** @scrutinizer ignore-call */ 
234
                 trackViewState(false);
Loading history...
234
235
					$component->applyStyleSheetSkin($page);
0 ignored issues
show
Bug introduced by
The method applyStyleSheetSkin() does not exist on Prado\TComponent. 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

235
					$component->/** @scrutinizer ignore-call */ 
236
                 applyStyleSheetSkin($page);
Loading history...
236
					foreach ($properties as $name => $value) {
237
						$this->configureControl($component, $name, $value);
238
					}
239
240
					$component->trackViewState(true);
241
242
					if ($parent === $parentControl) {
243
						$directChildren[] = $component;
244
					} else {
245
						$component->createdOnTemplate($parent);
246
					}
247
					if ($component->getAllowChildControls()) {
0 ignored issues
show
Bug introduced by
The method getAllowChildControls() does not exist on Prado\TComponent. 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

247
					if ($component->/** @scrutinizer ignore-call */ getAllowChildControls()) {
Loading history...
248
						$controls[$key] = $component;
249
					}
250
				} elseif ($component instanceof TComponent) {
251
					$controls[$key] = $component;
252
					if (isset($properties['id'])) {
253
						if (is_array($properties['id'])) {
254
							$properties['id'] = $component->evaluateExpression($properties['id'][1]);
255
						}
256
						$tplControl->registerObject($properties['id'], $component);
257
						if (!$component->hasProperty('id')) {
258
							unset($properties['id']);
259
						}
260
					}
261
					foreach ($properties as $name => $value) {
262
						$this->configureComponent($component, $name, $value);
263
					}
264
					if ($parent === $parentControl) {
265
						$directChildren[] = $component;
266
					} else {
267
						$component->createdOnTemplate($parent);
268
					}
269
				}
270
			} else {
271
				if ($object[1] instanceof TCompositeLiteral) {
272
					// need to clone a new object because the one in template is reused
273
					$o = clone $object[1];
274
					$o->setContainer($tplControl);
275
					if ($parent === $parentControl) {
276
						$directChildren[] = $o;
277
					} else {
278
						$parent->addParsedObject($o);
279
					}
280
				} else {
281
					if ($parent === $parentControl) {
282
						$directChildren[] = $object[1];
283
					} else {
284
						$parent->addParsedObject($object[1]);
285
					}
286
				}
287
			}
288
		}
289
		// delay setting parent till now because the parent may cause
290
		// the child to do lifecycle catchup which may cause problem
291
		// if the child needs its own child controls.
292
		foreach ($directChildren as $control) {
293
			if ($control instanceof TComponent) {
294
				$control->createdOnTemplate($parentControl);
295
			} else {
296
				$parentControl->addParsedObject($control);
297
			}
298
		}
299
	}
300
301
	/**
302
	 * Configures a property/event of a control.
303
	 * @param TControl $control control to be configured
304
	 * @param string $name property name
305
	 * @param mixed $value property initial value
306
	 */
307
	protected function configureControl($control, $name, $value)
308
	{
309
		if (strncasecmp($name, 'on', 2) === 0) {		// is an event
310
			$this->configureEvent($control, $name, $value, $control);
311
		} 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...
312
			$this->configureProperty($control, $name, $value);
313
		} else {	// is a subproperty
314
			$this->configureSubProperty($control, $name, $value);
315
		}
316
	}
317
318
	/**
319
	 * Configures a property of a non-control component.
320
	 * @param TComponent $component component to be configured
321
	 * @param string $name property name
322
	 * @param mixed $value property initial value
323
	 */
324
	protected function configureComponent($component, $name, $value)
325
	{
326
		if (strpos($name, '.') === false) {	// is a simple property or custom attribute
327
			$this->configureProperty($component, $name, $value);
328
		} else {	// is a subproperty
329
			$this->configureSubProperty($component, $name, $value);
330
		}
331
	}
332
333
	/**
334
	 * Configures an event for a control.
335
	 * @param TControl $control control to be configured
336
	 * @param string $name event name
337
	 * @param string $value event handler
338
	 * @param TControl $contextControl context control
339
	 */
340
	protected function configureEvent($control, $name, $value, $contextControl)
341
	{
342
		if (strpos($value, '.') === false) {
343
			$control->attachEventHandler($name, [$contextControl, 'TemplateControl.' . $value]);
344
		} else {
345
			$control->attachEventHandler($name, [$contextControl, $value]);
346
		}
347
	}
348
349
	/**
350
	 * Configures a simple property for a component.
351
	 * @param TComponent $component component to be configured
352
	 * @param string $name property name
353
	 * @param mixed $value property initial value
354
	 */
355
	protected function configureProperty($component, $name, $value)
356
	{
357
		if (is_array($value)) {
358
			switch ($value[0]) {
359
				case self::CONFIG_DATABIND:
360
					$component->bindProperty($name, $value[1]);
0 ignored issues
show
Bug introduced by
The method bindProperty() does not exist on Prado\TComponent. 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

360
					$component->/** @scrutinizer ignore-call */ 
361
                 bindProperty($name, $value[1]);
Loading history...
361
					break;
362
				case self::CONFIG_EXPRESSION:
363
					if ($component instanceof TControl) {
364
						$component->autoBindProperty($name, $value[1]);
365
					} else {
366
						$setter = 'set' . $name;
367
						$component->$setter($this->_tplControl->evaluateExpression($value[1]));
368
					}
369
					break;
370
				case self::CONFIG_TEMPLATE:
371
					$setter = 'set' . $name;
372
					$component->$setter($value[1]);
373
					break;
374
				case self::CONFIG_ASSET:		// asset URL
375
					$setter = 'set' . $name;
376
					$url = $this->publishFilePath($this->_contextPath . DIRECTORY_SEPARATOR . $value[1]);
377
					$component->$setter($url);
378
					break;
379
				case self::CONFIG_PARAMETER:		// application parameter
380
					$setter = 'set' . $name;
381
					$component->$setter($this->getApplication()->getParameters()->itemAt($value[1]));
382
					break;
383
				case self::CONFIG_LOCALIZATION:
384
					$setter = 'set' . $name;
385
					$component->$setter(Prado::localize($value[1]));
386
					break;
387
				default:	// an error if reaching here
388
					throw new TConfigurationException('template_tag_unexpected', $name, $value[1]);
389
					break;
390
			}
391
		} else {
392
			if (substr($name, 0, 2) == 'js') {
393
				if ($value && !($value instanceof TJavaScriptLiteral)) {
394
					$value = new TJavaScriptLiteral($value);
395
				}
396
			}
397
			$setter = 'set' . $name;
398
			$component->$setter($value);
399
		}
400
	}
401
402
	/**
403
	 * Configures a subproperty for a component.
404
	 * @param TComponent $component component to be configured
405
	 * @param string $name subproperty name
406
	 * @param mixed $value subproperty initial value
407
	 */
408
	protected function configureSubProperty($component, $name, $value)
409
	{
410
		if (is_array($value)) {
411
			switch ($value[0]) {
412
				case self::CONFIG_DATABIND:		// databinding
413
					$component->bindProperty($name, $value[1]);
414
					break;
415
				case self::CONFIG_EXPRESSION:		// expression
416
					if ($component instanceof TControl) {
417
						$component->autoBindProperty($name, $value[1]);
418
					} else {
419
						$component->setSubProperty($name, $this->_tplControl->evaluateExpression($value[1]));
420
					}
421
					break;
422
				case self::CONFIG_TEMPLATE:
423
					$component->setSubProperty($name, $value[1]);
424
					break;
425
				case self::CONFIG_ASSET:		// asset URL
426
					$url = $this->publishFilePath($this->_contextPath . DIRECTORY_SEPARATOR . $value[1]);
427
					$component->setSubProperty($name, $url);
428
					break;
429
				case self::CONFIG_PARAMETER:		// application parameter
430
					$component->setSubProperty($name, $this->getApplication()->getParameters()->itemAt($value[1]));
431
					break;
432
				case self::CONFIG_LOCALIZATION:
433
					$component->setSubProperty($name, Prado::localize($value[1]));
434
					break;
435
				default:	// an error if reaching here
436
					throw new TConfigurationException('template_tag_unexpected', $name, $value[1]);
437
					break;
438
			}
439
		} else {
440
			$component->setSubProperty($name, $value);
441
		}
442
	}
443
444
	/**
445
	 * Parses a template string.
446
	 *
447
	 * This template parser recognizes five types of data:
448
	 * regular string, well-formed component tags, well-formed property tags, directives, and expressions.
449
	 *
450
	 * The parsing result is returned as an array. Each array element can be of three types:
451
	 * - a string, 0: container index; 1: string content;
452
	 * - a component tag, 0: container index; 1: component type; 2: attributes (name=>value pairs)
453
	 * If a directive is found in the template, it will be parsed and can be
454
	 * retrieved via {@link getDirective}, which returns an array consisting of
455
	 * name-value pairs in the directive.
456
	 *
457
	 * Note, attribute names are treated as case-insensitive and will be turned into lower cases.
458
	 * Component and directive types are case-sensitive.
459
	 * Container index is the index to the array element that stores the container object.
460
	 * If an object has no container, its container index is -1.
461
	 *
462
	 * @param string $input the template string
463
	 * @throws TConfigurationException if a parsing error is encountered
464
	 */
465
	protected function parse($input)
466
	{
467
		$input = $this->preprocess($input);
468
		$tpl = &$this->_tpl;
469
		$n = preg_match_all(self::REGEX_RULES, $input, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
470
		$expectPropEnd = false;
471
		$textStart = 0;
472
		$stack = [];
473
		$container = -1;
474
		$matchEnd = 0;
475
		$c = 0;
476
		$this->_directive = null;
477
		try {
478
			for ($i = 0; $i < $n; ++$i) {
479
				$match = &$matches[$i];
480
				$str = $match[0][0];
481
				$matchStart = $match[0][1];
482
				$matchEnd = $matchStart + strlen($str) - 1;
483
				if (strpos($str, '<com:') === 0) {	// opening component tag
484
					if ($expectPropEnd) {
485
						continue;
486
					}
487
					if ($matchStart > $textStart) {
488
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
489
					}
490
					$textStart = $matchEnd + 1;
491
					$type = $match[1][0];
492
					$attributes = $this->parseAttributes($match[2][0], $match[2][1]);
493
					$class = $this->validateAttributes($type, $attributes);
494
					$tpl[$c++] = [$container, $class, $attributes];
495
					if ($str[strlen($str) - 2] !== '/') {  // open tag
496
						$stack[] = $type;
497
						$container = $c - 1;
498
					}
499
				} elseif (strpos($str, '</com:') === 0) {	// closing component tag
500
					if ($expectPropEnd) {
501
						continue;
502
					}
503
					if ($matchStart > $textStart) {
504
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
505
					}
506
					$textStart = $matchEnd + 1;
507
					$type = $match[1][0];
508
509
					if (empty($stack)) {
510
						throw new TConfigurationException('template_closingtag_unexpected', "</com:$type>");
511
					}
512
513
					$name = array_pop($stack);
514
					if ($name !== $type) {
515
						$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
516
						throw new TConfigurationException('template_closingtag_expected', $tag, "</com:$type>");
517
					}
518
					$container = $tpl[$container][0];
519
				} elseif (strpos($str, '<%@') === 0) {	// directive
520
					if ($expectPropEnd) {
521
						continue;
522
					}
523
					if ($matchStart > $textStart) {
524
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
525
					}
526
					$textStart = $matchEnd + 1;
527
					if (isset($tpl[0]) || $this->_directive !== null) {
528
						throw new TConfigurationException('template_directive_nonunique');
529
					}
530
					$this->_directive = $this->parseAttributes($match[4][0], $match[4][1]);
531
				} elseif (strpos($str, '<%') === 0) {	// expression
532
					if ($expectPropEnd) {
533
						continue;
534
					}
535
					if ($matchStart > $textStart) {
536
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
537
					}
538
					$textStart = $matchEnd + 1;
539
					$literal = trim($match[5][0]);
540
					if ($str[2] === '=') {	// expression
541
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, $literal]];
542
					} elseif ($str[2] === '%') {  // statements
543
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_STATEMENTS, $literal]];
544
					} elseif ($str[2] === '#') {
545
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_DATABINDING, $literal]];
546
					} elseif ($str[2] === '$') {
547
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "\$this->getApplication()->getParameters()->itemAt('$literal')"]];
548
					} elseif ($str[2] === '~') {
549
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "\$this->publishFilePath('$this->_contextPath/$literal')"]];
550
					} elseif ($str[2] === '/') {
551
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "rtrim(dirname(\$this->getApplication()->getRequest()->getApplicationUrl()), '\/').'/$literal'"]];
552
					} elseif ($str[2] === '[') {
553
						$literal = strtr(trim(substr($literal, 0, strlen($literal) - 1)), ["'" => "\'", "\\" => "\\\\"]);
554
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "Prado::localize('$literal')"]];
555
					}
556
				} elseif (strpos($str, '<prop:') === 0) {	// opening property
557
					if (strrpos($str, '/>') === strlen($str) - 2) {  //subproperties
558
						if ($expectPropEnd) {
559
							continue;
560
						}
561
						if ($matchStart > $textStart) {
562
							$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
563
						}
564
						$textStart = $matchEnd + 1;
565
						$prop = strtolower($match[6][0]);
566
						$attrs = $this->parseAttributes($match[7][0], $match[7][1]);
567
						$attributes = [];
568
						foreach ($attrs as $name => $value) {
569
							$attributes[$prop . '.' . $name] = $value;
570
						}
571
						$type = $tpl[$container][1];
572
						$this->validateAttributes($type, $attributes);
573
						foreach ($attributes as $name => $value) {
574
							if (isset($tpl[$container][2][$name])) {
575
								throw new TConfigurationException('template_property_duplicated', $name);
576
							}
577
							$tpl[$container][2][$name] = $value;
578
						}
579
					} else {  // regular property
580
						$prop = strtolower($match[3][0]);
581
						$stack[] = '@' . $prop;
582
						if (!$expectPropEnd) {
583
							if ($matchStart > $textStart) {
584
								$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
585
							}
586
							$textStart = $matchEnd + 1;
587
							$expectPropEnd = true;
588
						}
589
					}
590
				} elseif (strpos($str, '</prop:') === 0) {	// closing property
591
					$prop = strtolower($match[3][0]);
592
					if (empty($stack)) {
593
						throw new TConfigurationException('template_closingtag_unexpected', "</prop:$prop>");
594
					}
595
					$name = array_pop($stack);
596
					if ($name !== '@' . $prop) {
597
						$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
598
						throw new TConfigurationException('template_closingtag_expected', $tag, "</prop:$prop>");
599
					}
600
					if (($last = count($stack)) < 1 || $stack[$last - 1][0] !== '@') {
601
						if ($matchStart > $textStart) {
602
							$value = substr($input, $textStart, $matchStart - $textStart);
603
							if (substr($prop, -8, 8) === 'template') {
604
								$value = $this->parseTemplateProperty($value, $textStart);
605
							} else {
606
								$value = $this->parseAttribute($value);
607
							}
608
							if ($container >= 0) {
609
								$type = $tpl[$container][1];
610
								$this->validateAttributes($type, [$prop => $value]);
611
								if (isset($tpl[$container][2][$prop])) {
612
									throw new TConfigurationException('template_property_duplicated', $prop);
613
								}
614
								$tpl[$container][2][$prop] = $value;
615
							} else {	// a property for the template control
616
								$this->_directive[$prop] = $value;
617
							}
618
							$textStart = $matchEnd + 1;
619
						}
620
						$expectPropEnd = false;
621
					}
622
				} elseif (strpos($str, '<!--') === 0) {	// comments
623
					if ($expectPropEnd) {
624
						throw new TConfigurationException('template_comments_forbidden');
625
					}
626
					if ($matchStart > $textStart) {
627
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
628
					}
629
					$textStart = $matchEnd + 1;
630
				} else {
631
					throw new TConfigurationException('template_matching_unexpected', $match);
632
				}
633
			}
634
			if (!empty($stack)) {
635
				$name = array_pop($stack);
636
				$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
637
				throw new TConfigurationException('template_closingtag_expected', $tag, "nothing");
638
			}
639
			if ($textStart < strlen($input)) {
640
				$tpl[$c++] = [$container, substr($input, $textStart)];
641
			}
642
		} catch (\Exception $e) {
643
			if (($e instanceof TException) && ($e instanceof TTemplateException)) {
0 ignored issues
show
Bug introduced by
The type Prado\Web\UI\TException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
644
				throw $e;
645
			}
646
			if ($matchEnd === 0) {
647
				$line = $this->_startingLine + 1;
648
			} else {
649
				$line = $this->_startingLine + count(explode("\n", substr($input, 0, $matchEnd + 1)));
650
			}
651
			$this->handleException($e, $line, $input);
652
		}
653
654
		if ($this->_directive === null) {
655
			$this->_directive = [];
656
		}
657
658
		// optimization by merging consecutive strings, expressions, statements and bindings
659
		$objects = [];
660
		$parent = null;
661
		$merged = [];
662
		foreach ($tpl as $id => $object) {
663
			if (isset($object[2]) || $object[0] !== $parent) {
664
				if ($parent !== null) {
665
					if (count($merged[1]) === 1 && is_string($merged[1][0])) {
666
						$objects[$id - 1] = [$merged[0], $merged[1][0]];
667
					} else {
668
						$objects[$id - 1] = [$merged[0], new TCompositeLiteral($merged[1])];
669
					}
670
				}
671
				if (isset($object[2])) {
672
					$parent = null;
673
					$objects[$id] = $object;
674
				} else {
675
					$parent = $object[0];
676
					$merged = [$parent, [$object[1]]];
677
				}
678
			} else {
679
				$merged[1][] = $object[1];
680
			}
681
		}
682
		if ($parent !== null) {
683
			if (count($merged[1]) === 1 && is_string($merged[1][0])) {
684
				$objects[$id] = [$merged[0], $merged[1][0]];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $id seems to be defined by a foreach iteration on line 662. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
685
			} else {
686
				$objects[$id] = [$merged[0], new TCompositeLiteral($merged[1])];
687
			}
688
		}
689
		$tpl = $objects;
690
		return $objects;
691
	}
692
693
	/**
694
	 * Parses the attributes of a tag from a string.
695
	 * @param string $str the string to be parsed.
696
	 * @param mixed $offset
697
	 * @return array attribute values indexed by names.
698
	 */
699
	protected function parseAttributes($str, $offset)
700
	{
701
		if ($str === '') {
702
			return [];
703
		}
704
		$pattern = '/([\w\.\-]+)\s*=\s*(\'.*?\'|".*?"|<%.*?%>)/msS';
705
		$attributes = [];
706
		$n = preg_match_all($pattern, $str, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
707
		for ($i = 0; $i < $n; ++$i) {
708
			$match = &$matches[$i];
709
			$name = strtolower($match[1][0]);
710
			if (isset($attributes[$name])) {
711
				throw new TConfigurationException('template_property_duplicated', $name);
712
			}
713
			$value = $match[2][0];
714
			if (substr($name, -8, 8) === 'template') {
715
				if ($value[0] === '\'' || $value[0] === '"') {
716
					$attributes[$name] = $this->parseTemplateProperty(substr($value, 1, strlen($value) - 2), $match[2][1] + 1);
717
				} else {
718
					$attributes[$name] = $this->parseTemplateProperty($value, $match[2][1]);
719
				}
720
			} else {
721
				if ($value[0] === '\'' || $value[0] === '"') {
722
					$attributes[$name] = $this->parseAttribute(substr($value, 1, strlen($value) - 2));
723
				} else {
724
					$attributes[$name] = $this->parseAttribute($value);
725
				}
726
			}
727
		}
728
		return $attributes;
729
	}
730
731
	protected function parseTemplateProperty($content, $offset)
732
	{
733
		$line = $this->_startingLine + count(explode("\n", substr($this->_content, 0, $offset))) - 1;
734
		return [self::CONFIG_TEMPLATE, new TTemplate($content, $this->_contextPath, $this->_tplFile, $line, false)];
735
	}
736
737
	/**
738
	 * Parses a single attribute.
739
	 * @param string $value the string to be parsed.
740
	 * @return array attribute initialization
741
	 */
742
	protected function parseAttribute($value)
743
	{
744
		if (($n = preg_match_all('/<%[#=].*?%>/msS', $value, $matches, PREG_OFFSET_CAPTURE)) > 0) {
745
			$isDataBind = false;
746
			$textStart = 0;
747
			$expr = '';
748
			for ($i = 0; $i < $n; ++$i) {
749
				$match = $matches[0][$i];
750
				$token = $match[0];
751
				$offset = $match[1];
752
				$length = strlen($token);
753
				if ($token[2] === '#') {
754
					$isDataBind = true;
755
				}
756
				if ($offset > $textStart) {
757
					$expr .= ".'" . strtr(substr($value, $textStart, $offset - $textStart), ["'" => "\\'", "\\" => "\\\\"]) . "'";
758
				}
759
				$expr .= '.(' . substr($token, 3, $length - 5) . ')';
760
				$textStart = $offset + $length;
761
			}
762
			$length = strlen($value);
763
			if ($length > $textStart) {
764
				$expr .= ".'" . strtr(substr($value, $textStart, $length - $textStart), ["'" => "\\'", "\\" => "\\\\"]) . "'";
765
			}
766
			if ($isDataBind) {
767
				return [self::CONFIG_DATABIND, ltrim($expr, '.')];
768
			} else {
769
				return [self::CONFIG_EXPRESSION, ltrim($expr, '.')];
770
			}
771
		} elseif (preg_match('/\\s*(<%~.*?%>|<%\\$.*?%>|<%\\[.*?\\]%>|<%\/.*?%>)\\s*/msS', $value, $matches) && $matches[0] === $value) {
772
			$value = $matches[1];
773
			if ($value[2] === '~') {
774
				return [self::CONFIG_ASSET, trim(substr($value, 3, strlen($value) - 5))];
775
			} elseif ($value[2] === '[') {
776
				return [self::CONFIG_LOCALIZATION, trim(substr($value, 3, strlen($value) - 6))];
777
			} elseif ($value[2] === '$') {
778
				return [self::CONFIG_PARAMETER, trim(substr($value, 3, strlen($value) - 5))];
779
			} elseif ($value[2] === '/') {
780
				$literal = trim(substr($value, 3, strlen($value) - 5));
781
				return [self::CONFIG_EXPRESSION, "rtrim(dirname(\$this->getApplication()->getRequest()->getApplicationUrl()), '\/').'/$literal'"];
782
			}
783
		} else {
784
			return $value;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value returns the type string which is incompatible with the documented return type array.
Loading history...
785
		}
786
	}
787
788
	protected function validateAttributes($type, $attributes)
789
	{
790
		Prado::using($type);
791
		if (($pos = strrpos($type, '.')) !== false) {
792
			$className = substr($type, $pos + 1);
793
		} else {
794
			$className = $type;
795
		}
796
		$class = new \ReflectionClass($className);
797
		if (is_subclass_of($className, '\Prado\Web\UI\TControl') || $className === '\Prado\Web\UI\TControl') {
798
			foreach ($attributes as $name => $att) {
799
				if (($pos = strpos($name, '.')) !== false) {
800
					// a subproperty, so the first segment must be readable
801
					$subname = substr($name, 0, $pos);
802
					if (!$class->hasMethod('get' . $subname)) {
803
						throw new TConfigurationException('template_property_unknown', $type, $subname);
804
					}
805
				} elseif (strncasecmp($name, 'on', 2) === 0) {
806
					// an event
807
					if (!$class->hasMethod($name)) {
808
						throw new TConfigurationException('template_event_unknown', $type, $name);
809
					} elseif (!is_string($att)) {
810
						throw new TConfigurationException('template_eventhandler_invalid', $type, $name);
811
					}
812
				} else {
813
					// a simple property
814
					if (!($class->hasMethod('set' . $name) || $class->hasMethod('setjs' . $name) || $this->isClassBehaviorMethod($class, 'set' . $name))) {
815
						if ($class->hasMethod('get' . $name) || $class->hasMethod('getjs' . $name)) {
816
							throw new TConfigurationException('template_property_readonly', $type, $name);
817
						} else {
818
							throw new TConfigurationException('template_property_unknown', $type, $name);
819
						}
820
					} elseif (is_array($att) && $att[0] !== self::CONFIG_EXPRESSION) {
821
						if (strcasecmp($name, 'id') === 0) {
822
							throw new TConfigurationException('template_controlid_invalid', $type);
823
						} elseif (strcasecmp($name, 'skinid') === 0) {
824
							throw new TConfigurationException('template_controlskinid_invalid', $type);
825
						}
826
					}
827
				}
828
			}
829
		} elseif (is_subclass_of($className, '\Prado\TComponent') || $className === '\Prado\TComponent') {
830
			foreach ($attributes as $name => $att) {
831
				if (is_array($att) && ($att[0] === self::CONFIG_DATABIND)) {
832
					throw new TConfigurationException('template_databind_forbidden', $type, $name);
833
				}
834
				if (($pos = strpos($name, '.')) !== false) {
835
					// a subproperty, so the first segment must be readable
836
					$subname = substr($name, 0, $pos);
837
					if (!$class->hasMethod('get' . $subname)) {
838
						throw new TConfigurationException('template_property_unknown', $type, $subname);
839
					}
840
				} elseif (strncasecmp($name, 'on', 2) === 0) {
841
					throw new TConfigurationException('template_event_forbidden', $type, $name);
842
				} else {
843
					// id is still alowed for TComponent, even if id property doesn't exist
844
					if (strcasecmp($name, 'id') !== 0 && !($class->hasMethod('set' . $name) || $this->isClassBehaviorMethod($class, 'set' . $name))) {
845
						if ($class->hasMethod('get' . $name)) {
846
							throw new TConfigurationException('template_property_readonly', $type, $name);
847
						} else {
848
							throw new TConfigurationException('template_property_unknown', $type, $name);
849
						}
850
					}
851
				}
852
			}
853
		} else {
854
			throw new TConfigurationException('template_component_required', $type);
855
		}
856
		return $class->getName();
857
	}
858
859
	/**
860
	 * @return array list of included external template files
861
	 */
862
	public function getIncludedFiles()
863
	{
864
		return $this->_includedFiles;
865
	}
866
867
	/**
868
	 * Handles template parsing exception.
869
	 * This method rethrows the exception caught during template parsing.
870
	 * It adjusts the error location by giving out correct error line number and source file.
871
	 * @param Exception $e template exception
0 ignored issues
show
Bug introduced by
The type Prado\Web\UI\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
872
	 * @param int $line line number
873
	 * @param null|string $input template string if no source file is used
874
	 */
875
	protected function handleException($e, $line, $input = null)
876
	{
877
		$srcFile = $this->_tplFile;
878
879
		if (($n = count($this->_includedFiles)) > 0) { // need to adjust error row number and file name
880
			for ($i = $n - 1; $i >= 0; --$i) {
881
				if ($this->_includeAtLine[$i] <= $line) {
882
					if ($line < $this->_includeAtLine[$i] + $this->_includeLines[$i]) {
883
						$line = $line - $this->_includeAtLine[$i] + 1;
884
						$srcFile = $this->_includedFiles[$i];
885
						break;
886
					} else {
887
						$line = $line - $this->_includeLines[$i] + 1;
888
					}
889
				}
890
			}
891
		}
892
		$exception = new TTemplateException('template_format_invalid', $e->getMessage());
893
		$exception->setLineNumber($line);
894
		if (!empty($srcFile)) {
895
			$exception->setTemplateFile($srcFile);
896
		} else {
897
			$exception->setTemplateSource($input);
898
		}
899
		throw $exception;
900
	}
901
902
	/**
903
	 * Preprocesses the template string by including external templates
904
	 * @param string $input template string
905
	 * @return string expanded template string
906
	 */
907
	protected function preprocess($input)
908
	{
909
		if ($n = preg_match_all('/<%include(.*?)%>/', $input, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
910
			for ($i = 0; $i < $n; ++$i) {
911
				$filePath = Prado::getPathOfNamespace(trim($matches[$i][1][0]), TTemplateManager::TEMPLATE_FILE_EXT);
912
				if ($filePath !== null && is_file($filePath)) {
913
					$this->_includedFiles[] = $filePath;
914
				} else {
915
					$errorLine = count(explode("\n", substr($input, 0, $matches[$i][0][1] + 1)));
916
					$this->handleException(new TConfigurationException('template_include_invalid', trim($matches[$i][1][0])), $errorLine, $input);
917
				}
918
			}
919
			$base = 0;
920
			for ($i = 0; $i < $n; ++$i) {
921
				$ext = file_get_contents($this->_includedFiles[$i]);
922
				$length = strlen($matches[$i][0][0]);
923
				$offset = $base + $matches[$i][0][1];
924
				$this->_includeAtLine[$i] = count(explode("\n", substr($input, 0, $offset)));
925
				$this->_includeLines[$i] = count(explode("\n", $ext));
926
				$input = substr_replace($input, $ext, $offset, $length);
927
				$base += strlen($ext) - $length;
928
			}
929
		}
930
931
		return $input;
932
	}
933
934
	/**
935
	 * Checks if the given method belongs to a previously attached class behavior.
936
	 * @param ReflectionClass $class
0 ignored issues
show
Bug introduced by
The type Prado\Web\UI\ReflectionClass was not found. Did you mean ReflectionClass? If so, make sure to prefix the type with \.
Loading history...
937
	 * @param string $method
938
	 * @return bool
939
	 */
940
	protected function isClassBehaviorMethod(\ReflectionClass $class, $method)
941
	{
942
		$component = new \ReflectionClass('\Prado\TComponent');
943
		$behaviors = $component->getStaticProperties();
944
		if (!isset($behaviors['_um'])) {
945
			return false;
946
		}
947
		foreach ($behaviors['_um'] as $name => $list) {
948
			if (strtolower($class->getShortName()) !== $name && !$class->isSubclassOf($name)) {
949
				continue;
950
			}
951
			foreach ($list as $param) {
952
				if (method_exists($param->getBehavior(), $method)) {
953
					return true;
954
				}
955
			}
956
		}
957
		return false;
958
	}
959
}
960