Passed
Push — master ( e877b4...cb3898 )
by Fabio
05:28
created

TTemplate::setAttributeValidation()   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 1
dl 0
loc 3
rs 10
ccs 0
cts 3
cp 0
crap 2
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\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
 * @package Prado\Web\UI
51
 * @since 3.0
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
	private $_tplControl;
112
	private $_includedFiles = [];
113
	private $_includeAtLine = [];
114
	private $_includeLines = [];
115
116
117
	/**
118
	 * Constructor.
119
	 * The template will be parsed after construction.
120
	 * @param string $template the template string
121
	 * @param string $contextPath the template context directory
122
	 * @param null|string $tplFile the template file, null if no file
123
	 * @param int $startingLine the line number that parsing starts from (internal use)
124
	 * @param bool $sourceTemplate whether this template is a source template, i.e., this template is loaded from
125
	 * some external storage rather than from within another template.
126
	 */
127
	public function __construct($template, $contextPath, $tplFile = null, $startingLine = 0, $sourceTemplate = true)
128
	{
129
		$this->_sourceTemplate = $sourceTemplate;
130
		$this->_contextPath = $contextPath;
131
		$this->_tplFile = $tplFile;
132
		$this->_startingLine = $startingLine;
133
		$this->_content = $template;
134
		$this->_hashCode = md5($template);
135
		parent::__construct();
136
		$this->parse($template);
137
		$this->_content = null; // reset to save memory
138
	}
139
140
	/**
141
	 * @return string  template file path if available, null otherwise.
142
	 */
143
	public function getTemplateFile()
144
	{
145
		return $this->_tplFile;
146
	}
147
148
	/**
149
	 * @return bool whether this template is a source template, i.e., this template is loaded from
150
	 * some external storage rather than from within another template.
151
	 */
152
	public function getIsSourceTemplate()
153
	{
154
		return $this->_sourceTemplate;
155
	}
156
157
	/**
158
	 * @return string context directory path
159
	 */
160
	public function getContextPath()
161
	{
162
		return $this->_contextPath;
163
	}
164
165
	/**
166
	 * @return array name-value pairs declared in the directive
167
	 */
168
	public function getDirective()
169
	{
170
		return $this->_directive;
171
	}
172
173
	/**
174
	 * @return string hash code that can be used to identify the template
175
	 */
176
	public function getHashCode()
177
	{
178
		return $this->_hashCode;
179
	}
180
181
	/**
182
	 * @return array the parsed template
183
	 */
184
	public function &getItems()
185
	{
186
		return $this->_tpl;
187
	}
188
	
189
	/**
190
	 * @return bool whether or not validation of the template is active
191
	 * @since 4.2.0
192
	 */
193
	public function getAttributeValidation()
194
	{
195
		return $this->_attributevalidation;
196
	}
197
198
	/**
199
	 * @param bool $value whether or not validation of the template is active
200
	 * @since 4.2.0
201
	 */
202
	public function setAttributeValidation($value)
203
	{
204
		$this->_attributevalidation = $value;
205
	}
206
207
	/**
208
	 * Instantiates the template.
209
	 * Content in the template will be instantiated as components and text strings
210
	 * and passed to the specified parent control.
211
	 * @param \Prado\Web\UI\TControl $tplControl the control who owns the template
212
	 * @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.
213
	 */
214
	public function instantiateIn($tplControl, $parentControl = null)
215
	{
216
		$this->_tplControl = $tplControl;
217
		if ($parentControl === null) {
218
			$parentControl = $tplControl;
219
		}
220
		if (($page = $tplControl->getPage()) === null) {
221
			$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

221
			$page = $this->getService()->/** @scrutinizer ignore-call */ getRequestedPage();
Loading history...
222
		}
223
		$controls = [];
224
		$directChildren = [];
225
		foreach ($this->_tpl as $key => $object) {
226
			if ($object[0] === -1) {
227
				$parent = $parentControl;
228
			} elseif (isset($controls[$object[0]])) {
229
				$parent = $controls[$object[0]];
230
			} else {
231
				continue;
232
			}
233
			if (isset($object[2])) {	// component
234
				$component = Prado::createComponent($object[1]);
235
				$properties = &$object[2];
236
				if ($component instanceof TControl) {
237
					if ($component instanceof \Prado\Web\UI\WebControls\TOutputCache) {
238
						$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

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

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