Passed
Branch php-cs-fixer (b9836a)
by Fabio
15:02
created

TTemplate::getItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * TTemplateManager and TTemplate class file
4
 *
5
 * @author Qiang Xue <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @copyright Copyright &copy; 2005-2016 The PRADO Group
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 * @package Prado\Web\UI
10
 */
11
12
namespace Prado\Web\UI;
13
14
use Prado\Prado;
15
use Prado\TComponent;
16
use Prado\Web\Javascripts\TJavaScriptLiteral;
17
use Prado\Exceptions\TConfigurationException;
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
	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
	const CONFIG_DATABIND = 0;
70
	const CONFIG_EXPRESSION = 1;
71
	const CONFIG_ASSET = 2;
72
	const CONFIG_PARAMETER = 3;
73
	const CONFIG_LOCALIZATION = 4;
74
	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 integer 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 boolean whether this template is a source template
102
	 */
103
	private $_sourceTemplate = true;
104
	/**
105
	 * @var string hash code of the template
106
	 */
107
	private $_hashCode = '';
108
	private $_tplControl;
109
	private $_includedFiles = [];
110
	private $_includeAtLine = [];
111
	private $_includeLines = [];
112
113
114
	/**
115
	 * Constructor.
116
	 * The template will be parsed after construction.
117
	 * @param string the template string
0 ignored issues
show
Bug introduced by
The type Prado\Web\UI\the 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...
118
	 * @param string the template context directory
119
	 * @param string the template file, null if no file
120
	 * @param integer the line number that parsing starts from (internal use)
121
	 * @param boolean whether this template is a source template, i.e., this template is loaded from
0 ignored issues
show
Bug introduced by
The type Prado\Web\UI\whether 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...
122
	 * some external storage rather than from within another template.
123
	 */
124
	public function __construct($template, $contextPath, $tplFile = null, $startingLine = 0, $sourceTemplate = true)
125
	{
126
		$this->_sourceTemplate = $sourceTemplate;
127
		$this->_contextPath = $contextPath;
128
		$this->_tplFile = $tplFile;
129
		$this->_startingLine = $startingLine;
130
		$this->_content = $template;
131
		$this->_hashCode = md5($template);
132
		$this->parse($template);
133
		$this->_content = null; // reset to save memory
134
	}
135
136
	/**
137
	 * @return string  template file path if available, null otherwise.
138
	 */
139
	public function getTemplateFile()
140
	{
141
		return $this->_tplFile;
142
	}
143
144
	/**
145
	 * @return boolean whether this template is a source template, i.e., this template is loaded from
146
	 * some external storage rather than from within another template.
147
	 */
148
	public function getIsSourceTemplate()
149
	{
150
		return $this->_sourceTemplate;
151
	}
152
153
	/**
154
	 * @return string context directory path
155
	 */
156
	public function getContextPath()
157
	{
158
		return $this->_contextPath;
159
	}
160
161
	/**
162
	 * @return array name-value pairs declared in the directive
163
	 */
164
	public function getDirective()
165
	{
166
		return $this->_directive;
167
	}
168
169
	/**
170
	 * @return string hash code that can be used to identify the template
171
	 */
172
	public function getHashCode()
173
	{
174
		return $this->_hashCode;
175
	}
176
177
	/**
178
	 * @return array the parsed template
179
	 */
180
	public function &getItems()
181
	{
182
		return $this->_tpl;
183
	}
184
185
	/**
186
	 * Instantiates the template.
187
	 * Content in the template will be instantiated as components and text strings
188
	 * and passed to the specified parent control.
189
	 * @param TControl the control who owns the template
190
	 * @param TControl the control who will become the root parent of the controls on the template. If null, it uses the template control.
191
	 */
192
	public function instantiateIn($tplControl, $parentControl = null)
193
	{
194
		$this->_tplControl = $tplControl;
195
		if($parentControl === null)
196
			$parentControl = $tplControl;
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
		$controls = [];
200
		$directChildren = [];
201
		foreach($this->_tpl as $key => $object)
202
		{
203
			if($object[0] === -1)
204
				$parent = $parentControl;
205
			elseif(isset($controls[$object[0]]))
206
				$parent = $controls[$object[0]];
207
			else
208
				continue;
209
			if(isset($object[2]))	// component
210
			{
211
				$component = Prado::createComponent($object[1]);
212
				$properties = &$object[2];
213
				if($component instanceof TControl)
214
				{
215
					if($component instanceof \Prado\Web\UI\WebControls\TOutputCache)
216
						$component->setCacheKeyPrefix($this->_hashCode . $key);
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
					{
220
						if(is_array($properties['id']))
221
							$properties['id'] = $component->evaluateExpression($properties['id'][1]);
222
						$tplControl->registerObject($properties['id'], $component);
223
					}
224
					if(isset($properties['skinid']))
225
					{
226
						if(is_array($properties['skinid']))
227
							$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

227
							$component->/** @scrutinizer ignore-call */ 
228
                   setSkinID($component->evaluateExpression($properties['skinid'][1]));
Loading history...
228
						else
229
							$component->setSkinID($properties['skinid']);
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
					$component->trackViewState(true);
240
241
					if($parent === $parentControl)
242
						$directChildren[] = $component;
243
					else
244
						$component->createdOnTemplate($parent);
245
					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

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