TTemplate::parse()   F
last analyzed

Complexity

Conditions 68
Paths 12512

Size

Total Lines 227
Code Lines 175

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 4692

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 68
eloc 175
c 2
b 0
f 0
nc 12512
nop 1
dl 0
loc 227
ccs 0
cts 212
cp 0
crap 4692
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * TTemplateManager and TTemplate class file
5
 *
6
 * @author Qiang Xue <[email protected]>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
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\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 {@see 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
			$component->$setter($value);
446
		}
447
	}
448
449
	/**
450
	 * Configures a subproperty for a component.
451
	 * @param \Prado\Web\UI\TControl $component component to be configured
452
	 * @param string $name subproperty name
453
	 * @param mixed $value subproperty initial value
454
	 */
455
	protected function configureSubProperty($component, $name, $value)
456
	{
457
		if (is_array($value)) {
458
			switch ($value[0]) {
459
				case self::CONFIG_DATABIND:		// databinding
460
					$component->bindProperty($name, $value[1]);
461
					break;
462
				case self::CONFIG_EXPRESSION:		// expression
463
					if ($component instanceof TControl) {
0 ignored issues
show
introduced by
$component is always a sub-type of Prado\Web\UI\TControl.
Loading history...
464
						$component->autoBindProperty($name, $value[1]);
465
					} else {
466
						$component->setSubProperty($name, $this->_tplControl->evaluateExpression($value[1]));
467
					}
468
					break;
469
				case self::CONFIG_TEMPLATE:
470
					$component->setSubProperty($name, $value[1]);
471
					break;
472
				case self::CONFIG_ASSET:		// asset URL
473
					$url = $this->publishFilePath($this->_contextPath . DIRECTORY_SEPARATOR . $value[1]);
474
					$component->setSubProperty($name, $url);
475
					break;
476
				case self::CONFIG_PARAMETER:		// application parameter
477
					$component->setSubProperty($name, $this->getApplication()->getParameters()->itemAt($value[1]));
478
					break;
479
				case self::CONFIG_LOCALIZATION:
480
					$component->setSubProperty($name, Prado::localize($value[1]));
481
					break;
482
				default:	// an error if reaching here
483
					throw new TConfigurationException('template_tag_unexpected', $name, $value[1]);
484
					break;
485
			}
486
		} else {
487
			$component->setSubProperty($name, $value);
488
		}
489
	}
490
491
	/**
492
	 * Parses a template string.
493
	 *
494
	 * This template parser recognizes five types of data:
495
	 * regular string, well-formed component tags, well-formed property tags, directives, and expressions.
496
	 *
497
	 * The parsing result is returned as an array. Each array element can be of three types:
498
	 * - a string, 0: container index; 1: string content;
499
	 * - a component tag, 0: container index; 1: component type; 2: attributes (name=>value pairs)
500
	 * If a directive is found in the template, it will be parsed and can be
501
	 * retrieved via {@see getDirective}, which returns an array consisting of
502
	 * name-value pairs in the directive.
503
	 *
504
	 * Note, attribute names are treated as case-insensitive and will be turned into lower cases.
505
	 * Component and directive types are case-sensitive.
506
	 * Container index is the index to the array element that stores the container object.
507
	 * If an object has no container, its container index is -1.
508
	 *
509
	 * @param string $input the template string
510
	 * @throws TConfigurationException if a parsing error is encountered
511
	 */
512
	protected function parse($input)
513
	{
514
		$input = $this->preprocess($input);
515
		$tpl = &$this->_tpl;
516
		$n = preg_match_all(self::REGEX_RULES, $input, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
517
		$expectPropEnd = false;
518
		$textStart = 0;
519
		$stack = [];
520
		$container = -1;
521
		$matchEnd = 0;
522
		$c = 0;
523
		$this->_directive = null;
524
		try {
525
			for ($i = 0; $i < $n; ++$i) {
526
				$match = &$matches[$i];
527
				$str = $match[0][0];
528
				$matchStart = $match[0][1];
529
				$matchEnd = $matchStart + strlen($str) - 1;
530
				if (strpos($str, '<com:') === 0) {	// opening component tag
531
					if ($expectPropEnd) {
532
						continue;
533
					}
534
					if ($matchStart > $textStart) {
535
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
536
					}
537
					$textStart = $matchEnd + 1;
538
					$type = $match[1][0];
539
					$attributes = $this->parseAttributes($match[2][0], $match[2][1]);
540
					$class = $this->validateAttributes($type, $attributes);
541
					$tpl[$c++] = [$container, $class, $attributes];
542
					if ($str[strlen($str) - 2] !== '/') {  // open tag
543
						$stack[] = $type;
544
						$container = $c - 1;
545
					}
546
				} elseif (strpos($str, '</com:') === 0) {	// closing component tag
547
					if ($expectPropEnd) {
548
						continue;
549
					}
550
					if ($matchStart > $textStart) {
551
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
552
					}
553
					$textStart = $matchEnd + 1;
554
					$type = $match[1][0];
555
556
					if (empty($stack)) {
557
						throw new TConfigurationException('template_closingtag_unexpected', "</com:$type>");
558
					}
559
560
					$name = array_pop($stack);
561
					if ($name !== $type) {
562
						$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
563
						throw new TConfigurationException('template_closingtag_expected', $tag, "</com:$type>");
564
					}
565
					$container = $tpl[$container][0];
566
				} elseif (strpos($str, '<%@') === 0) {	// directive
567
					if ($expectPropEnd) {
568
						continue;
569
					}
570
					if ($matchStart > $textStart) {
571
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
572
					}
573
					$textStart = $matchEnd + 1;
574
					if (isset($tpl[0]) || $this->_directive !== null) {
575
						throw new TConfigurationException('template_directive_nonunique');
576
					}
577
					$this->_directive = $this->parseAttributes($match[4][0], $match[4][1]);
578
				} elseif (strpos($str, '<%') === 0) {	// expression
579
					if ($expectPropEnd) {
580
						continue;
581
					}
582
					if ($matchStart > $textStart) {
583
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
584
					}
585
					$textStart = $matchEnd + 1;
586
					$literal = trim($match[5][0]);
587
					if ($str[2] === '=') {	// expression
588
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, $literal]];
589
					} elseif ($str[2] === '%') {  // statements
590
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_STATEMENTS, $literal]];
591
					} elseif ($str[2] === '#') {
592
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_DATABINDING, $literal]];
593
					} elseif ($str[2] === '$') {
594
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "\$this->getApplication()->getParameters()->itemAt('$literal')"]];
595
					} elseif ($str[2] === '~') {
596
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "\$this->publishFilePath('$this->_contextPath/$literal')"]];
597
					} elseif ($str[2] === '/') {
598
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "rtrim(dirname(\$this->getApplication()->getRequest()->getApplicationUrl()), '\/').'/$literal'"]];
599
					} elseif ($str[2] === '[') {
600
						$literal = strtr(trim(substr($literal, 0, strlen($literal) - 1)), ["'" => "\'", "\\" => "\\\\"]);
601
						$tpl[$c++] = [$container, [TCompositeLiteral::TYPE_EXPRESSION, "Prado::localize('$literal')"]];
602
					}
603
				} elseif (strpos($str, '<prop:') === 0) {	// opening property
604
					if (strrpos($str, '/>') === strlen($str) - 2) {  //subproperties
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
						$prop = strtolower($match[6][0]);
613
						$attrs = $this->parseAttributes($match[7][0], $match[7][1]);
614
						$attributes = [];
615
						foreach ($attrs as $name => $value) {
616
							$attributes[$prop . '.' . $name] = $value;
617
						}
618
						$type = $tpl[$container][1];
619
						$this->validateAttributes($type, $attributes);
620
						foreach ($attributes as $name => $value) {
621
							if (isset($tpl[$container][2][$name])) {
622
								throw new TConfigurationException('template_property_duplicated', $name);
623
							}
624
							$tpl[$container][2][$name] = $value;
625
						}
626
					} else {  // regular property
627
						$prop = strtolower($match[3][0]);
628
						$stack[] = '@' . $prop;
629
						if (!$expectPropEnd) {
630
							if ($matchStart > $textStart) {
631
								$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
632
							}
633
							$textStart = $matchEnd + 1;
634
							$expectPropEnd = true;
635
						}
636
					}
637
				} elseif (strpos($str, '</prop:') === 0) {	// closing property
638
					$prop = strtolower($match[3][0]);
639
					if (empty($stack)) {
640
						throw new TConfigurationException('template_closingtag_unexpected', "</prop:$prop>");
641
					}
642
					$name = array_pop($stack);
643
					if ($name !== '@' . $prop) {
644
						$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
645
						throw new TConfigurationException('template_closingtag_expected', $tag, "</prop:$prop>");
646
					}
647
					if (($last = count($stack)) < 1 || $stack[$last - 1][0] !== '@') {
648
						if ($matchStart > $textStart) {
649
							$value = substr($input, $textStart, $matchStart - $textStart);
650
							if (substr($prop, -8, 8) === 'template') {
651
								$value = $this->parseTemplateProperty($value, $textStart);
652
							} else {
653
								$value = $this->parseAttribute($value);
654
							}
655
							if ($container >= 0) {
656
								$type = $tpl[$container][1];
657
								$this->validateAttributes($type, [$prop => $value]);
658
								if (isset($tpl[$container][2][$prop])) {
659
									throw new TConfigurationException('template_property_duplicated', $prop);
660
								}
661
								$tpl[$container][2][$prop] = $value;
662
							} else {	// a property for the template control
663
								$this->_directive[$prop] = $value;
664
							}
665
							$textStart = $matchEnd + 1;
666
						}
667
						$expectPropEnd = false;
668
					}
669
				} elseif (strpos($str, '<!--') === 0) {	// comments
670
					if ($expectPropEnd) {
671
						throw new TConfigurationException('template_comments_forbidden');
672
					}
673
					if ($matchStart > $textStart) {
674
						$tpl[$c++] = [$container, substr($input, $textStart, $matchStart - $textStart)];
675
					}
676
					$textStart = $matchEnd + 1;
677
				} else {
678
					throw new TConfigurationException('template_matching_unexpected', $match);
679
				}
680
			}
681
			if (!empty($stack)) {
682
				$name = array_pop($stack);
683
				$tag = $name[0] === '@' ? '</prop:' . substr($name, 1) . '>' : "</com:$name>";
684
				throw new TConfigurationException('template_closingtag_expected', $tag, "nothing");
685
			}
686
			if ($textStart < strlen($input)) {
687
				$tpl[$c++] = [$container, substr($input, $textStart)];
688
			}
689
		} catch (\Exception $e) {
690
			if (($e instanceof TException) && ($e instanceof TTemplateException)) {
691
				throw $e;
692
			}
693
			if ($matchEnd === 0) {
694
				$line = $this->_startingLine + 1;
695
			} else {
696
				$line = $this->_startingLine + count(explode("\n", substr($input, 0, $matchEnd + 1)));
697
			}
698
			$this->handleException($e, $line, $input);
699
		}
700
701
		if ($this->_directive === null) {
702
			$this->_directive = [];
703
		}
704
705
		// optimization by merging consecutive strings, expressions, statements and bindings
706
		$objects = [];
707
		$parent = null;
708
		$id = null;
709
		$merged = [];
710
		foreach ($tpl as $id => $object) {
711
			if (isset($object[2]) || $object[0] !== $parent) {
712
				if ($parent !== null) {
713
					if (count($merged[1]) === 1 && is_string($merged[1][0])) {
714
						$objects[$id - 1] = [$merged[0], $merged[1][0]];
715
					} else {
716
						$objects[$id - 1] = [$merged[0], new TCompositeLiteral($merged[1])];
717
					}
718
				}
719
				if (isset($object[2])) {
720
					$parent = null;
721
					$objects[$id] = $object;
722
				} else {
723
					$parent = $object[0];
724
					$merged = [$parent, [$object[1]]];
725
				}
726
			} else {
727
				$merged[1][] = $object[1];
728
			}
729
		}
730
		if ($parent !== null && $id !== null) {
731
			if (count($merged[1]) === 1 && is_string($merged[1][0])) {
732
				$objects[$id] = [$merged[0], $merged[1][0]];
733
			} else {
734
				$objects[$id] = [$merged[0], new TCompositeLiteral($merged[1])];
735
			}
736
		}
737
		$tpl = $objects;
738
		return $objects;
739
	}
740
741
	/**
742
	 * Parses the attributes of a tag from a string.
743
	 * @param string $str the string to be parsed.
744
	 * @param mixed $offset
745
	 * @return array attribute values indexed by names.
746
	 */
747
	protected function parseAttributes($str, $offset)
748
	{
749
		if ($str === '') {
750
			return [];
751
		}
752
		$pattern = '/([\w\.\-]+)\s*=\s*(\'.*?\'|".*?"|<%.*?%>)/msS';
753
		$attributes = [];
754
		$n = preg_match_all($pattern, $str, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
755
		for ($i = 0; $i < $n; ++$i) {
756
			$match = &$matches[$i];
757
			$name = strtolower($match[1][0]);
758
			if (isset($attributes[$name])) {
759
				throw new TConfigurationException('template_property_duplicated', $name);
760
			}
761
			$value = $match[2][0];
762
			if (substr($name, -8, 8) === 'template') {
763
				if ($value[0] === '\'' || $value[0] === '"') {
764
					$attributes[$name] = $this->parseTemplateProperty(substr($value, 1, strlen($value) - 2), $match[2][1] + 1);
765
				} else {
766
					$attributes[$name] = $this->parseTemplateProperty($value, $match[2][1]);
767
				}
768
			} else {
769
				if ($value[0] === '\'' || $value[0] === '"') {
770
					$attributes[$name] = $this->parseAttribute(substr($value, 1, strlen($value) - 2));
771
				} else {
772
					$attributes[$name] = $this->parseAttribute($value);
773
				}
774
			}
775
		}
776
		return $attributes;
777
	}
778
779
	protected function parseTemplateProperty($content, $offset)
780
	{
781
		$line = $this->_startingLine + count(explode("\n", substr($this->_content, 0, $offset))) - 1;
782
		return [self::CONFIG_TEMPLATE, new TTemplate($content, $this->_contextPath, $this->_tplFile, $line, false)];
783
	}
784
785
	/**
786
	 * Parses a single attribute.
787
	 * @param string $value the string to be parsed.
788
	 * @return array attribute initialization
789
	 */
790
	protected function parseAttribute($value)
791
	{
792
		if (($n = preg_match_all('/<%[#=].*?%>/msS', $value, $matches, PREG_OFFSET_CAPTURE)) > 0) {
793
			$isDataBind = false;
794
			$textStart = 0;
795
			$expr = '';
796
			for ($i = 0; $i < $n; ++$i) {
797
				$match = $matches[0][$i];
798
				$token = $match[0];
799
				$offset = $match[1];
800
				$length = strlen($token);
801
				if ($token[2] === '#') {
802
					$isDataBind = true;
803
				}
804
				if ($offset > $textStart) {
805
					$expr .= ".'" . strtr(substr($value, $textStart, $offset - $textStart), ["'" => "\\'", "\\" => "\\\\"]) . "'";
806
				}
807
				$expr .= '.(' . substr($token, 3, $length - 5) . ')';
808
				$textStart = $offset + $length;
809
			}
810
			$length = strlen($value);
811
			if ($length > $textStart) {
812
				$expr .= ".'" . strtr(substr($value, $textStart, $length - $textStart), ["'" => "\\'", "\\" => "\\\\"]) . "'";
813
			}
814
			if ($isDataBind) {
815
				return [self::CONFIG_DATABIND, ltrim($expr, '.')];
816
			} else {
817
				return [self::CONFIG_EXPRESSION, ltrim($expr, '.')];
818
			}
819
		} elseif (preg_match('/\\s*(<%~.*?%>|<%\\$.*?%>|<%\\[.*?\\]%>|<%\/.*?%>)\\s*/msS', $value, $matches) && $matches[0] === $value) {
820
			$value = $matches[1];
821
			if ($value[2] === '~') {
822
				return [self::CONFIG_ASSET, trim(substr($value, 3, strlen($value) - 5))];
823
			} elseif ($value[2] === '[') {
824
				return [self::CONFIG_LOCALIZATION, trim(substr($value, 3, strlen($value) - 6))];
825
			} elseif ($value[2] === '$') {
826
				return [self::CONFIG_PARAMETER, trim(substr($value, 3, strlen($value) - 5))];
827
			} elseif ($value[2] === '/') {
828
				$literal = trim(substr($value, 3, strlen($value) - 5));
829
				return [self::CONFIG_EXPRESSION, "rtrim(dirname(\$this->getApplication()->getRequest()->getApplicationUrl()), '\/').'/$literal'"];
830
			}
831
		}
832
		return $value;
833
	}
834
835
	protected function validateAttributes($type, $attributes)
836
	{
837
		Prado::using($type);
838
		if (($pos = strrpos($type, '.')) !== false) {
839
			$className = substr($type, $pos + 1);
840
		} else {
841
			$className = $type;
842
		}
843
		$class = new \ReflectionClass($className);
844
		if (!$this->_attributevalidation) {
845
			return $class->getName();
846
		}
847
		if (is_subclass_of($className, TControl::class) || $className === TControl::class) {
848
			foreach ($attributes as $name => $att) {
849
				if (($pos = strpos($name, '.')) !== false) {
850
					// a subproperty, so the first segment must be readable
851
					$subname = substr($name, 0, $pos);
852
					if (!$class->hasMethod('get' . $subname)) {
853
						throw new TConfigurationException('template_property_unknown', $type, $subname);
854
					}
855
				} elseif (strncasecmp($name, 'on', 2) === 0) {
856
					// an event
857
					if (!$class->hasMethod($name)) {
858
						throw new TConfigurationException('template_event_unknown', $type, $name);
859
					} elseif (!is_string($att)) {
860
						throw new TConfigurationException('template_eventhandler_invalid', $type, $name);
861
					}
862
				} else {
863
					// a simple property
864
					if (!($class->hasMethod('set' . $name) || $class->hasMethod('setjs' . $name) || $this->isClassBehaviorMethod($class, 'set' . $name))) {
865
						if ($class->hasMethod('get' . $name) || $class->hasMethod('getjs' . $name)) {
866
							throw new TConfigurationException('template_property_readonly', $type, $name);
867
						} else {
868
							throw new TConfigurationException('template_property_unknown', $type, $name);
869
						}
870
					} elseif (is_array($att) && $att[0] !== self::CONFIG_EXPRESSION && $att[0] !== self::CONFIG_PARAMETER) {
871
						if (strcasecmp($name, 'id') === 0) {
872
							throw new TConfigurationException('template_controlid_invalid', $type);
873
						} elseif (strcasecmp($name, 'skinid') === 0) {
874
							throw new TConfigurationException('template_controlskinid_invalid', $type);
875
						}
876
					}
877
				}
878
			}
879
		} elseif (is_subclass_of($className, TComponent::class) || $className === TComponent::class) {
880
			foreach ($attributes as $name => $att) {
881
				if (is_array($att) && ($att[0] === self::CONFIG_DATABIND)) {
882
					throw new TConfigurationException('template_databind_forbidden', $type, $name);
883
				}
884
				if (($pos = strpos($name, '.')) !== false) {
885
					// a subproperty, so the first segment must be readable
886
					$subname = substr($name, 0, $pos);
887
					if (!$class->hasMethod('get' . $subname)) {
888
						throw new TConfigurationException('template_property_unknown', $type, $subname);
889
					}
890
				} elseif (strncasecmp($name, 'on', 2) === 0) {
891
					throw new TConfigurationException('template_event_forbidden', $type, $name);
892
				} else {
893
					// id is still allowed for TComponent, even if id property doesn't exist
894
					if (strcasecmp($name, 'id') !== 0 && !($class->hasMethod('set' . $name) || $this->isClassBehaviorMethod($class, 'set' . $name))) {
895
						if ($class->hasMethod('get' . $name)) {
896
							throw new TConfigurationException('template_property_readonly', $type, $name);
897
						} else {
898
							throw new TConfigurationException('template_property_unknown', $type, $name);
899
						}
900
					}
901
				}
902
			}
903
		} else {
904
			throw new TConfigurationException('template_component_required', $type);
905
		}
906
		return $class->getName();
907
	}
908
909
	/**
910
	 * @return array list of included external template files
911
	 */
912
	public function getIncludedFiles()
913
	{
914
		return $this->_includedFiles;
915
	}
916
917
	/**
918
	 * Handles template parsing exception.
919
	 * This method rethrows the exception caught during template parsing.
920
	 * It adjusts the error location by giving out correct error line number and source file.
921
	 * @param \Exception $e template exception
922
	 * @param int $line line number
923
	 * @param null|string $input template string if no source file is used
924
	 */
925
	protected function handleException($e, $line, $input = null)
926
	{
927
		$srcFile = $this->_tplFile;
928
929
		if (($n = count($this->_includedFiles)) > 0) { // need to adjust error row number and file name
930
			for ($i = $n - 1; $i >= 0; --$i) {
931
				if ($this->_includeAtLine[$i] <= $line) {
932
					if ($line < $this->_includeAtLine[$i] + $this->_includeLines[$i]) {
933
						$line = $line - $this->_includeAtLine[$i] + 1;
934
						$srcFile = $this->_includedFiles[$i];
935
						break;
936
					} else {
937
						$line = $line - $this->_includeLines[$i] + 1;
938
					}
939
				}
940
			}
941
		}
942
		$exception = new TTemplateException('template_format_invalid', $e->getMessage());
943
		$exception->setLineNumber($line);
944
		if (!empty($srcFile)) {
945
			$exception->setTemplateFile($srcFile);
946
		} else {
947
			$exception->setTemplateSource($input);
948
		}
949
		throw $exception;
950
	}
951
952
	/**
953
	 * Preprocesses the template string by including external templates
954
	 * @param string $input template string
955
	 * @return string expanded template string
956
	 */
957
	protected function preprocess($input)
958
	{
959
		if ($n = preg_match_all('/<%include(.*?)%>/', $input, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
960
			for ($i = 0; $i < $n; ++$i) {
961
				$filePath = Prado::getPathOfNamespace(trim($matches[$i][1][0]), TTemplateManager::TEMPLATE_FILE_EXT);
962
				if ($filePath !== null && is_file($filePath)) {
963
					$this->_includedFiles[] = $filePath;
964
				} else {
965
					$errorLine = count(explode("\n", substr($input, 0, $matches[$i][0][1] + 1)));
966
					$this->handleException(new TConfigurationException('template_include_invalid', trim($matches[$i][1][0])), $errorLine, $input);
967
				}
968
			}
969
			$base = 0;
970
			for ($i = 0; $i < $n; ++$i) {
971
				$ext = file_get_contents($this->_includedFiles[$i]);
972
				$length = strlen($matches[$i][0][0]);
973
				$offset = $base + $matches[$i][0][1];
974
				$this->_includeAtLine[$i] = count(explode("\n", substr($input, 0, $offset)));
975
				$this->_includeLines[$i] = count(explode("\n", $ext));
976
				$input = substr_replace($input, $ext, $offset, $length);
977
				$base += strlen($ext) - $length;
978
			}
979
		}
980
981
		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...
982
	}
983
984
	/**
985
	 * Checks if the given method belongs to a previously attached class behavior.
986
	 * @param \ReflectionClass $class
987
	 * @param string $method
988
	 * @return bool
989
	 */
990
	protected function isClassBehaviorMethod(\ReflectionClass $class, $method)
991
	{
992
		$component = new \ReflectionClass(TComponent::class);
993
		$behaviors = $component->getStaticProperties();
994
		if (!isset($behaviors['_um'])) {
995
			return false;
996
		}
997
		foreach ($behaviors['_um'] as $name => $list) {
998
			if (strtolower($class->getShortName()) !== $name && !$class->isSubclassOf($name)) {
999
				continue;
1000
			}
1001
			foreach ($list as $param) {
1002
				$behavior = $param->getBehavior();
1003
				if (is_array($behavior)) {
1004
					if (method_exists($behavior['class'], $method)) {
1005
						return true;
1006
					}
1007
				} elseif (method_exists($behavior, $method)) {
1008
					return true;
1009
				}
1010
			}
1011
		}
1012
		return false;
1013
	}
1014
}
1015