Completed
Push — master ( a86e58...ac11d6 )
by Fabio
06:43
created

TTemplate::validateAttributes()   D

Complexity

Conditions 31
Paths 40

Size

Total Lines 79
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 31
eloc 48
nc 40
nop 2
dl 0
loc 79
rs 4.9882
c 0
b 0
f 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
 * TTemplateManager and TTemplate class file
4
 *
5
 * @author Qiang Xue <[email protected]>
6
 * @link https://github.com/pradosoft/prado4
7
 * @copyright Copyright &copy; 2005-2016 The PRADO Group
8
 * @license https://github.com/pradosoft/prado4/blob/master/LICENSE
9
 * @package Prado\Web\UI
10
 */
11
12
namespace Prado\Web\UI;
13
use Prado\Prado;
14
use Prado\TComponent;
15
use Prado\Web\Javascripts\TJavaScriptLiteral;
16
use Prado\Exceptions\TConfigurationException;
17
use Prado\Exceptions\TTemplateException;
18
use Prado\Exceptions\TException;
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=array();
80
	/**
81
	 * @var array list of directive settings
82
	 */
83
	private $_directive=array();
84
	/**
85
	 * @var string context path
86
	 */
87
	private $_contextPath;
88
	/**
89
	 * @var string template file path (if available)
90
	 */
91
	private $_tplFile=null;
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=null;
109
	private $_includedFiles=array();
110
	private $_includeAtLine=array();
111
	private $_includeLines=array();
112
113
114
	/**
115
	 * Constructor.
116
	 * The template will be parsed after construction.
117
	 * @param string the template string
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
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
It seems like you code against a concrete implementation and not the interface Prado\IService as the method getRequestedPage() does only exist in the following implementations of said interface: Prado\Web\Services\TPageService, Prado\Wsat\TWsatService.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
199
		$controls=array();
200
		$directChildren=array();
201
		foreach($this->_tpl as $key=>$object)
202
		{
203
			if($object[0]===-1)
204
				$parent=$parentControl;
205
			else if(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);
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]));
228
						else
229
							$component->setSkinID($properties['skinid']);
230
						unset($properties['skinid']);
231
					}
232
233
					$component->trackViewState(false);
234
235
					$component->applyStyleSheetSkin($page);
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())
246
						$controls[$key]=$component;
247
				}
248
				else if($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.
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
		else if(($pos=strrpos($name,'.'))===false)	// is a simple property or custom attribute
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.
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.
332
	 * @param TControl control to be configured
333
	 * @param string event name
334
	 * @param string event handler
335
	 * @param TControl context control
336
	 */
337
	protected function configureEvent($control,$name,$value,$contextControl)
338
	{
339
		if(strpos($value,'.')===false)
340
			$control->attachEventHandler($name,array($contextControl,'TemplateControl.'.$value));
341
		else
342
			$control->attachEventHandler($name,array($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;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
389
			}
390
		}
391
		else
392
		{
393
			if (substr($name,0,2)=='js')
394
				if ($value and !($value instanceof TJavaScriptLiteral))
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
395
					$value = new TJavaScriptLiteral($value);
396
			$setter='set'.$name;
397
			$component->$setter($value);
398
		}
399
	}
400
401
	/**
402
	 * Configures a subproperty for a component.
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;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
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=array();
473
		$container=-1;
474
		$matchEnd=0;
0 ignored issues
show
Unused Code introduced by
$matchEnd is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
475
		$c=0;
476
		$this->_directive=null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $_directive.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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++]=array($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++]=array($container,$class,$attributes);
496
					if($str[strlen($str)-2]!=='/')  // open tag
497
					{
498
						$stack[] = $type;
499
						$container=$c-1;
500
					}
501
				}
502
				else if(strpos($str,'</com:')===0)	// closing component tag
503
				{
504
					if($expectPropEnd)
505
						continue;
506
					if($matchStart>$textStart)
507
						$tpl[$c++]=array($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);
519
					}
520
					$container=$tpl[$container][0];
521
				}
522
				else if(strpos($str,'<%@')===0)	// directive
523
				{
524
					if($expectPropEnd)
525
						continue;
526
					if($matchStart>$textStart)
527
						$tpl[$c++]=array($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
				else if(strpos($str,'<%')===0)	// expression
534
				{
535
					if($expectPropEnd)
536
						continue;
537
					if($matchStart>$textStart)
538
						$tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
539
					$textStart=$matchEnd+1;
540
					$literal=trim($match[5][0]);
541
					if($str[2]==='=')	// expression
542
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,$literal));
543
					else if($str[2]==='%')  // statements
544
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_STATEMENTS,$literal));
545
					else if($str[2]==='#')
546
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_DATABINDING,$literal));
547
					else if($str[2]==='$')
548
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"\$this->getApplication()->getParameters()->itemAt('$literal')"));
549
					else if($str[2]==='~')
550
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"\$this->publishFilePath('$this->_contextPath/$literal')"));
551
					else if($str[2]==='/')
552
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"rtrim(dirname(\$this->getApplication()->getRequest()->getApplicationUrl()), '/').'/$literal'"));
553
					else if($str[2]==='[')
554
					{
555
						$literal=strtr(trim(substr($literal,0,strlen($literal)-1)),array("'"=>"\'","\\"=>"\\\\"));
556
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"Prado::localize('$literal')"));
557
					}
558
				}
559
				else if(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++]=array($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=array();
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++]=array($container,substr($input,$textStart,$matchStart-$textStart));
590
							$textStart=$matchEnd+1;
591
							$expectPropEnd=true;
592
						}
593
					}
594
				}
595
				else if(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);
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,array($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
				else if(strpos($str,'<!--')===0)	// comments
631
				{
632
					if($expectPropEnd)
633
						throw new TConfigurationException('template_comments_forbidden');
634
					if($matchStart>$textStart)
635
						$tpl[$c++]=array($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);
646
			}
647
			if($textStart<strlen($input))
648
				$tpl[$c++]=array($container,substr($input,$textStart));
649
		}
650
		/*
651
		catch(\Exception $e)
652
		{
653
			if(($e instanceof TException) && ($e instanceof TTemplateException))
654
				throw $e;
655
			if($matchEnd===0)
656
				$line=$this->_startingLine+1;
657
			else
658
				$line=$this->_startingLine+count(explode("\n",substr($input,0,$matchEnd+1)));
659
			$this->handleException($e,$line,$input);
660
		} */
661
662
		if($this->_directive===null)
663
			$this->_directive=array();
664
665
		// optimization by merging consecutive strings, expressions, statements and bindings
666
		$objects=array();
667
		$parent=null;
668
		$merged=array();
669
		foreach($tpl as $id=>$object)
670
		{
671
			if(isset($object[2]) || $object[0]!==$parent)
672
			{
673
				if($parent!==null)
674
				{
675
					if(count($merged[1])===1 && is_string($merged[1][0]))
676
						$objects[$id-1]=array($merged[0],$merged[1][0]);
677
					else
678
						$objects[$id-1]=array($merged[0],new TCompositeLiteral($merged[1]));
679
				}
680
				if(isset($object[2]))
681
				{
682
					$parent=null;
683
					$objects[$id]=$object;
684
				}
685
				else
686
				{
687
					$parent=$object[0];
688
					$merged=array($parent,array($object[1]));
689
				}
690
			}
691
			else
692
				$merged[1][]=$object[1];
693
		}
694
		if($parent!==null)
695
		{
696
			if(count($merged[1])===1 && is_string($merged[1][0]))
697
				$objects[$id]=array($merged[0],$merged[1][0]);
0 ignored issues
show
Bug introduced by
The variable $id seems to be defined by a foreach iteration on line 669. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

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

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
964
	 * @param string $method
965
	 * @return boolean
966
	 */
967
	protected function isClassBehaviorMethod(\ReflectionClass $class,$method)
968
	{
969
	  $component=new \ReflectionClass('\Prado\TComponent');
970
	  $behaviors=$component->getStaticProperties();
971
	  if(!isset($behaviors['_um']))
972
	    return false;
973
	  foreach($behaviors['_um'] as $name=>$list)
0 ignored issues
show
Bug introduced by
The expression $behaviors['_um'] of type string is not traversable.
Loading history...
974
	  {
975
	    if(strtolower($class->getShortName())!==$name && !$class->isSubclassOf($name)) continue;
976
	    foreach($list as $param)
977
	    {
978
	      if(method_exists($param->getBehavior(),$method))
979
	        return true;
980
	    }
981
	  }
982
	  return false;
983
	}
984
}
985