Completed
Push — master ( 10f429...3b71b7 )
by Ingo
11:44
created

SSViewer::dontRewriteHashlinks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
use SilverStripe\ORM\FieldType\DBField;
4
use SilverStripe\ORM\FieldType\DBHTMLText;
5
use SilverStripe\Security\Permission;
6
use SilverStripe\View\ThemeResourceLoader;
7
8
/**
9
 * This tracks the current scope for an SSViewer instance. It has three goals:
10
 *   - Handle entering & leaving sub-scopes in loops and withs
11
 *   - Track Up and Top
12
 *   - (As a side effect) Inject data that needs to be available globally (used to live in ViewableData)
13
 *
14
 * In order to handle up, rather than tracking it using a tree, which would involve constructing new objects
15
 * for each step, we use indexes into the itemStack (which already has to exist).
16
 *
17
 * Each item has three indexes associated with it
18
 *
19
 *   - Pop. Which item should become the scope once the current scope is popped out of
20
 *   - Up. Which item is up from this item
21
 *   - Current. Which item is the first time this object has appeared in the stack
22
 *
23
 * We also keep the index of the current starting point for lookups. A lookup is a sequence of obj calls -
24
 * when in a loop or with tag the end result becomes the new scope, but for injections, we throw away the lookup
25
 * and revert back to the original scope once we've got the value we're after
26
 *
27
 * @package framework
28
 * @subpackage view
29
 */
30
class SSViewer_Scope {
31
32
	const ITEM = 0;
33
	const ITEM_ITERATOR = 1;
34
	const ITEM_ITERATOR_TOTAL = 2;
35
	const POP_INDEX = 3;
36
	const UP_INDEX = 4;
37
	const CURRENT_INDEX = 5;
38
	const ITEM_OVERLAY = 6;
39
40
	// The stack of previous "global" items
41
	// An indexed array of item, item iterator, item iterator total, pop index, up index, current index & parent overlay
42
	private $itemStack = array();
43
44
	// The current "global" item (the one any lookup starts from)
45
	protected $item;
46
47
	// If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
48
	protected $itemIterator;
49
50
	//Total number of items in the iterator
51
	protected $itemIteratorTotal;
52
53
	// A pointer into the item stack for which item should be scope on the next pop call
54
	private $popIndex;
55
56
	// A pointer into the item stack for which item is "up" from this one
57
	private $upIndex = null;
58
59
	// A pointer into the item stack for which item is this one (or null if not in stack yet)
60
	private $currentIndex = null;
61
62
	private $localIndex;
63
64
	public function __construct($item, $inheritedScope = null) {
65
		$this->item = $item;
66
		$this->localIndex = 0;
67
		$this->localStack = array();
0 ignored issues
show
Bug introduced by
The property localStack does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
68
		if ($inheritedScope instanceof SSViewer_Scope) {
69
			$this->itemIterator = $inheritedScope->itemIterator;
70
			$this->itemIteratorTotal = $inheritedScope->itemIteratorTotal;
71
			$this->itemStack[] = array($this->item, $this->itemIterator, $this->itemIteratorTotal, null, null, 0);
72
		} else {
73
			$this->itemStack[] = array($this->item, null, 0, null, null, 0);
74
		}
75
	}
76
77
	public function getItem(){
78
		return $this->itemIterator ? $this->itemIterator->current() : $this->item;
79
	}
80
81
	/** Called at the start of every lookup chain by SSTemplateParser to indicate a new lookup from local scope */
82
	public function locally() {
83
		list($this->item, $this->itemIterator, $this->itemIteratorTotal, $this->popIndex, $this->upIndex,
84
			$this->currentIndex) = $this->itemStack[$this->localIndex];
85
86
		// Remember any  un-completed (resetLocalScope hasn't been called) lookup chain. Even if there isn't an
87
		// un-completed chain we need to store an empty item, as resetLocalScope doesn't know the difference later
88
		$this->localStack[] = array_splice($this->itemStack, $this->localIndex+1);
89
90
		return $this;
91
	}
92
93
	public function resetLocalScope(){
94
		$previousLocalState = $this->localStack ? array_pop($this->localStack) : null;
95
96
		array_splice($this->itemStack, $this->localIndex+1, count($this->itemStack), $previousLocalState);
97
98
		list($this->item, $this->itemIterator, $this->itemIteratorTotal, $this->popIndex, $this->upIndex,
99
			$this->currentIndex) = end($this->itemStack);
100
	}
101
102
	public function getObj($name, $arguments = [], $cache = false, $cacheName = null) {
103
		$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
104
		return $on->obj($name, $arguments, $cache, $cacheName);
105
	}
106
107
	public function obj($name, $arguments = [], $cache = false, $cacheName = null) {
108
		switch ($name) {
109
			case 'Up':
110
				if ($this->upIndex === null) {
111
					user_error('Up called when we\'re already at the top of the scope', E_USER_ERROR);
112
				}
113
114
				list($this->item, $this->itemIterator, $this->itemIteratorTotal, $unused2, $this->upIndex,
0 ignored issues
show
Unused Code introduced by
The assignment to $unused2 is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
115
					$this->currentIndex) = $this->itemStack[$this->upIndex];
116
				break;
117
118
			case 'Top':
119
				list($this->item, $this->itemIterator, $this->itemIteratorTotal, $unused2, $this->upIndex,
0 ignored issues
show
Unused Code introduced by
The assignment to $unused2 is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
120
					$this->currentIndex) = $this->itemStack[0];
121
				break;
122
123
			default:
124
				$this->item = $this->getObj($name, $arguments, $cache, $cacheName);
125
				$this->itemIterator = null;
126
				$this->upIndex = $this->currentIndex ? $this->currentIndex : count($this->itemStack)-1;
127
				$this->currentIndex = count($this->itemStack);
128
				break;
129
		}
130
131
		$this->itemStack[] = array($this->item, $this->itemIterator, $this->itemIteratorTotal, null,
132
			$this->upIndex, $this->currentIndex);
133
		return $this;
134
	}
135
136
	/**
137
	 * Gets the current object and resets the scope.
138
	 *
139
	 * @return object
140
	 */
141
	public function self() {
142
		$result = $this->itemIterator ? $this->itemIterator->current() : $this->item;
143
		$this->resetLocalScope();
144
145
		return $result;
146
	}
147
148
	public function pushScope(){
149
		$newLocalIndex = count($this->itemStack)-1;
150
151
		$this->popIndex = $this->itemStack[$newLocalIndex][SSViewer_Scope::POP_INDEX] = $this->localIndex;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
152
		$this->localIndex = $newLocalIndex;
153
154
		// We normally keep any previous itemIterator around, so local $Up calls reference the right element. But
155
		// once we enter a new global scope, we need to make sure we use a new one
156
		$this->itemIterator = $this->itemStack[$newLocalIndex][SSViewer_Scope::ITEM_ITERATOR] = null;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
157
158
		return $this;
159
	}
160
161
	public function popScope(){
162
		$this->localIndex = $this->popIndex;
163
		$this->resetLocalScope();
164
165
		return $this;
166
	}
167
168
	public function next(){
169
		if (!$this->item) return false;
170
171
		if (!$this->itemIterator) {
172
			if (is_array($this->item)) $this->itemIterator = new ArrayIterator($this->item);
173
			else $this->itemIterator = $this->item->getIterator();
174
175
			$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR] = $this->itemIterator;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
176
			$this->itemIteratorTotal = iterator_count($this->itemIterator); //count the total number of items
177
			$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR_TOTAL] = $this->itemIteratorTotal;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
178
			$this->itemIterator->rewind();
179
		}
180
		else {
181
			$this->itemIterator->next();
182
		}
183
184
		$this->resetLocalScope();
185
186
		if (!$this->itemIterator->valid()) return false;
187
		return $this->itemIterator->key();
188
	}
189
190
	public function __call($name, $arguments) {
191
		$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
192
		$retval = $on ? call_user_func_array(array($on, $name), $arguments) : null;
193
194
		$this->resetLocalScope();
195
		return $retval;
196
	}
197
198
	/**
199
	 * @return array
200
	 */
201
	protected function getItemStack() {
202
		return $this->itemStack;
203
	}
204
205
	/**
206
	 * @param array
207
	 */
208
	protected function setItemStack(array $stack) {
209
		$this->itemStack = $stack;
210
	}
211
212
	/**
213
	 * @return int|null
214
	 */
215
	protected function getUpIndex() {
216
		return $this->upIndex;
217
	}
218
}
219
220
/**
221
 * Defines an extra set of basic methods that can be used in templates
222
 * that are not defined on sub-classes of {@link ViewableData}.
223
 *
224
 * @package framework
225
 * @subpackage view
226
 */
227
class SSViewer_BasicIteratorSupport implements TemplateIteratorProvider {
228
229
	protected $iteratorPos;
230
	protected $iteratorTotalItems;
231
232
	public static function get_template_iterator_variables() {
233
		return array(
234
			'First',
235
			'Last',
236
			'FirstLast',
237
			'Middle',
238
			'MiddleString',
239
			'Even',
240
			'Odd',
241
			'EvenOdd',
242
			'Pos',
243
			'FromEnd',
244
			'TotalItems',
245
			'Modulus',
246
			'MultipleOf',
247
		);
248
	}
249
250
	/**
251
	 * Set the current iterator properties - where we are on the iterator.
252
	 *
253
	 * @param int $pos position in iterator
254
	 * @param int $totalItems total number of items
255
	 */
256
	public function iteratorProperties($pos, $totalItems) {
257
		$this->iteratorPos        = $pos;
258
		$this->iteratorTotalItems = $totalItems;
259
	}
260
261
	/**
262
	 * Returns true if this object is the first in a set.
263
	 *
264
	 * @return bool
265
	 */
266
	public function First() {
267
		return $this->iteratorPos == 0;
268
	}
269
270
	/**
271
	 * Returns true if this object is the last in a set.
272
	 *
273
	 * @return bool
274
	 */
275
	public function Last() {
276
		return $this->iteratorPos == $this->iteratorTotalItems - 1;
277
	}
278
279
	/**
280
	 * Returns 'first' or 'last' if this is the first or last object in the set.
281
	 *
282
	 * @return string|null
283
	 */
284
	public function FirstLast() {
285
		if($this->First() && $this->Last()) return 'first last';
286
		if($this->First()) return 'first';
287
		if($this->Last())  return 'last';
288
	}
289
290
	/**
291
	 * Return true if this object is between the first & last objects.
292
	 *
293
	 * @return bool
294
	 */
295
	public function Middle() {
296
		return !$this->First() && !$this->Last();
297
	}
298
299
	/**
300
	 * Return 'middle' if this object is between the first & last objects.
301
	 *
302
	 * @return string|null
303
	 */
304
	public function MiddleString() {
305
		if($this->Middle()) return 'middle';
306
	}
307
308
	/**
309
	 * Return true if this object is an even item in the set.
310
	 * The count starts from $startIndex, which defaults to 1.
311
	 *
312
	 * @param int $startIndex Number to start count from.
313
	 * @return bool
314
	 */
315
	public function Even($startIndex = 1) {
316
		return !$this->Odd($startIndex);
317
	}
318
319
	/**
320
	 * Return true if this is an odd item in the set.
321
	 *
322
	 * @param int $startIndex Number to start count from.
323
	 * @return bool
324
	 */
325
	public function Odd($startIndex = 1) {
326
		return (bool) (($this->iteratorPos+$startIndex) % 2);
327
	}
328
329
	/**
330
	 * Return 'even' or 'odd' if this object is in an even or odd position in the set respectively.
331
	 *
332
	 * @param int $startIndex Number to start count from.
333
	 * @return string
334
	 */
335
	public function EvenOdd($startIndex = 1) {
336
		return ($this->Even($startIndex)) ? 'even' : 'odd';
337
	}
338
339
	/**
340
	 * Return the numerical position of this object in the container set. The count starts at $startIndex.
341
	 * The default is the give the position using a 1-based index.
342
	 *
343
	 * @param int $startIndex Number to start count from.
344
	 * @return int
345
	 */
346
	public function Pos($startIndex = 1) {
347
		return $this->iteratorPos + $startIndex;
348
	}
349
350
	/**
351
	 * Return the position of this item from the last item in the list. The position of the final
352
	 * item is $endIndex, which defaults to 1.
353
	 *
354
	 * @param integer $endIndex Value of the last item
355
	 * @return int
356
	 */
357
	public function FromEnd($endIndex = 1) {
358
		return $this->iteratorTotalItems - $this->iteratorPos + $endIndex - 1;
359
	}
360
361
	/**
362
	 * Return the total number of "sibling" items in the dataset.
363
	 *
364
	 * @return int
365
	 */
366
	public function TotalItems() {
367
		return $this->iteratorTotalItems;
368
	}
369
370
	/**
371
	 * Returns the modulus of the numerical position of the item in the data set.
372
	 * The count starts from $startIndex, which defaults to 1.
373
	 * @param int $Mod The number to perform Mod operation to.
0 ignored issues
show
Bug introduced by
There is no parameter named $Mod. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
374
	 * @param int $startIndex Number to start count from.
375
	 * @return int
376
	 */
377
	public function Modulus($mod, $startIndex = 1) {
378
		return ($this->iteratorPos + $startIndex) % $mod;
379
	}
380
381
	/**
382
	 * Returns true or false depending on if the pos of the iterator is a multiple of a specific number.
383
	 * So, <% if MultipleOf(3) %> would return true on indexes: 3,6,9,12,15, etc.
384
	 * The count starts from $offset, which defaults to 1.
385
	 * @param int $factor The multiple of which to return
386
	 * @param int $offset Number to start count from.
387
	 * @return bool
388
	 */
389
	public function MultipleOf($factor, $offset = 1) {
390
		return (bool) ($this->Modulus($factor, $offset) == 0);
391
	}
392
393
394
395
}
396
/**
397
 * This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global"
398
 * data that is scope-independant (like BaseURL), or type-specific data that is layered on top cross-cut like
399
 * (like $FirstLast etc).
400
 *
401
 * It's separate from SSViewer_Scope to keep that fairly complex code as clean as possible.
402
 *
403
 * @package framework
404
 * @subpackage view
405
 */
406
class SSViewer_DataPresenter extends SSViewer_Scope {
407
408
	private static $globalProperties = null;
409
	private static $iteratorProperties = null;
410
411
	/**
412
	 * Overlay variables. Take precedence over anything from the current scope
413
	 * @var array|null
414
	 */
415
	protected $overlay;
416
417
	/**
418
	 * Underlay variables. Concede precedence to overlay variables or anything from the current scope
419
	 * @var array|null
420
	 */
421
	protected $underlay;
422
423
	public function __construct($item, $overlay = null, $underlay = null, $inheritedScope = null) {
424
		parent::__construct($item, $inheritedScope);
425
426
		// Build up global property providers array only once per request
427
		if (self::$globalProperties === null) {
428
			self::$globalProperties = array();
429
			// Get all the exposed variables from all classes that implement the TemplateGlobalProvider interface
430
			$this->createCallableArray(self::$globalProperties, "TemplateGlobalProvider",
431
				"get_template_global_variables");
432
		}
433
434
		// Build up iterator property providers array only once per request
435
		if (self::$iteratorProperties === null) {
436
			self::$iteratorProperties = array();
437
			// Get all the exposed variables from all classes that implement the TemplateIteratorProvider interface
438
			// //call non-statically
439
			$this->createCallableArray(self::$iteratorProperties, "TemplateIteratorProvider",
440
				"get_template_iterator_variables", true);
441
		}
442
443
		$this->overlay = $overlay ? $overlay : array();
444
		$this->underlay = $underlay ? $underlay : array();
445
	}
446
447
	protected function createCallableArray(&$extraArray, $interfaceToQuery, $variableMethod, $createObject = false) {
448
		$implementers = ClassInfo::implementorsOf($interfaceToQuery);
449
		if($implementers) foreach($implementers as $implementer) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $implementers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
450
451
			// Create a new instance of the object for method calls
452
			if ($createObject) $implementer = new $implementer();
453
454
			// Get the exposed variables
455
			$exposedVariables = call_user_func(array($implementer, $variableMethod));
456
457
			foreach($exposedVariables as $varName => $details) {
458
				if (!is_array($details)) $details = array('method' => $details,
459
					'casting' => Config::inst()->get('ViewableData', 'default_cast', Config::FIRST_SET));
460
461
				// If just a value (and not a key => value pair), use it for both key and value
462
				if (is_numeric($varName)) $varName = $details['method'];
463
464
				// Add in a reference to the implementing class (might be a string class name or an instance)
465
				$details['implementer'] = $implementer;
466
467
				// And a callable array
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
468
				if (isset($details['method'])) $details['callable'] = array($implementer, $details['method']);
469
470
				// Save with both uppercase & lowercase first letter, so either works
471
				$lcFirst = strtolower($varName[0]) . substr($varName,1);
472
				$extraArray[$lcFirst] = $details;
473
				$extraArray[ucfirst($varName)] = $details;
474
			}
475
		}
476
	}
477
478
	/**
479
	 * Get the injected value
480
	 *
481
	 * @param string $property Name of property
482
	 * @param array $params
483
	 * @param bool $cast If true, an object is always returned even if not an object.
484
	 * @return array Result array with the keys 'value' for raw value, or 'obj' if contained in an object
485
	 * @throws InvalidArgumentException
486
	 */
487
	public function getInjectedValue($property, $params, $cast = true) {
488
		$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
489
490
		// Find the source of the value
491
		$source = null;
492
493
		// Check for a presenter-specific override
494
		if (array_key_exists($property, $this->overlay)) {
495
			$source = array('value' => $this->overlay[$property]);
496
		}
497
		// Check if the method to-be-called exists on the target object - if so, don't check any further
498
		// injection locations
499
		else if (isset($on->$property) || method_exists($on, $property)) {
500
			$source = null;
501
		}
502
		// Check for a presenter-specific override
503
		else if (array_key_exists($property, $this->underlay)) {
504
			$source = array('value' => $this->underlay[$property]);
505
		}
506
		// Then for iterator-specific overrides
507
		else if (array_key_exists($property, self::$iteratorProperties)) {
508
			$source = self::$iteratorProperties[$property];
509
			if ($this->itemIterator) {
510
				// Set the current iterator position and total (the object instance is the first item in
511
				// the callable array)
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
512
				$source['implementer']->iteratorProperties($this->itemIterator->key(), $this->itemIteratorTotal);
513
			} else {
514
				// If we don't actually have an iterator at the moment, act like a list of length 1
515
				$source['implementer']->iteratorProperties(0, 1);
516
			}
517
		}
518
		// And finally for global overrides
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
519
		else if (array_key_exists($property, self::$globalProperties)) {
520
			$source = self::$globalProperties[$property];  //get the method call
521
		}
522
523
		if ($source) {
524
			$res = array();
525
526
			// Look up the value - either from a callable, or from a directly provided value
527
			if (isset($source['callable'])) $res['value'] = call_user_func_array($source['callable'], $params);
528
			elseif (isset($source['value'])) $res['value'] = $source['value'];
529
			else throw new InvalidArgumentException("Injected property $property does't have a value or callable " .
530
				"value source provided");
531
532
			// If we want to provide a casted object, look up what type object to use
533
			if ($cast) {
534
				// If the handler returns an object, then we don't need to cast.
535
				if(is_object($res['value'])) {
536
					$res['obj'] = $res['value'];
537
				} else {
538
					// Get the object to cast as
539
					$casting = isset($source['casting']) ? $source['casting'] : null;
540
541
					// If not provided, use default
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
542
					if (!$casting) $casting = Config::inst()->get('ViewableData', 'default_cast', Config::FIRST_SET);
543
544
					$obj = Injector::inst()->get($casting, false, array($property));
545
					$obj->setValue($res['value']);
546
547
					$res['obj'] = $obj;
548
				}
549
			}
550
551
			return $res;
552
		}
553
554
	}
555
556
	/**
557
	 * Store the current overlay (as it doesn't directly apply to the new scope
558
	 * that's being pushed). We want to store the overlay against the next item
559
	 * "up" in the stack (hence upIndex), rather than the current item, because
560
	 * SSViewer_Scope::obj() has already been called and pushed the new item to
561
	 * the stack by this point
562
	 * @return SSViewer_Scope
563
	 */
564
	public function pushScope() {
565
		$scope = parent::pushScope();
566
		$upIndex = $this->getUpIndex();
567
568
		if ($upIndex !== null) {
569
		$itemStack = $this->getItemStack();
570
			$itemStack[$upIndex][SSViewer_Scope::ITEM_OVERLAY] = $this->overlay;
571
572
		$this->setItemStack($itemStack);
573
		$this->overlay = array();
574
		}
575
576
		return $scope;
577
	}
578
579
	/**
580
	 * Now that we're going to jump up an item in the item stack, we need to
581
	 * restore the overlay that was previously stored against the next item "up"
582
	 * in the stack from the current one
583
	 * @return SSViewer_Scope
584
	 */
585
	public function popScope() {
586
		$upIndex = $this->getUpIndex();
587
588
		if ($upIndex !== null) {
589
		$itemStack = $this->getItemStack();
590
		$this->overlay = $itemStack[$this->getUpIndex()][SSViewer_Scope::ITEM_OVERLAY];
591
		}
592
593
		return parent::popScope();
594
	}
595
596
	/**
597
	 * $Up and $Top need to restore the overlay from the parent and top-level
598
	 * scope respectively.
599
	 */
600
	public function obj($name, $arguments = [], $cache = false, $cacheName = null) {
601
		$overlayIndex = false;
602
603
		switch($name) {
604
			case 'Up':
605
				$upIndex = $this->getUpIndex();
606
				if ($upIndex === null) {
607
					user_error('Up called when we\'re already at the top of the scope', E_USER_ERROR);
608
				}
609
610
				$overlayIndex = $upIndex; // Parent scope
611
				break;
612
			case 'Top':
613
				$overlayIndex = 0; // Top-level scope
614
				break;
615
		}
616
617
		if ($overlayIndex !== false) {
618
			$itemStack = $this->getItemStack();
619
			if (!$this->overlay && isset($itemStack[$overlayIndex][SSViewer_Scope::ITEM_OVERLAY])) {
620
				$this->overlay = $itemStack[$overlayIndex][SSViewer_Scope::ITEM_OVERLAY];
621
			}
622
		}
623
624
		return parent::obj($name, $arguments, $cache, $cacheName);
625
	}
626
627
	public function getObj($name, $arguments = [], $cache = false, $cacheName = null) {
628
		$result = $this->getInjectedValue($name, (array)$arguments);
629
		if($result) {
630
			return $result['obj'];
631
		}
632
		return parent::getObj($name, $arguments, $cache, $cacheName);
633
	}
634
635
	public function __call($name, $arguments) {
636
		//extract the method name and parameters
637
		$property = $arguments[0];  //the name of the public function being called
638
639
		//the public function parameters in an array
640
		if (isset($arguments[1]) && $arguments[1] != null) $params = $arguments[1];
641
		else $params = array();
642
643
		$val = $this->getInjectedValue($property, $params);
644
		if ($val) {
645
			$obj = $val['obj'];
646
			if ($name === 'hasValue') {
647
				$res = $obj instanceof Object
648
					? $obj->exists()
649
					: (bool)$obj;
650
			} else {
651
				// XML_val
652
				$res = $obj->forTemplate();
653
			}
654
			$this->resetLocalScope();
655
			return $res;
656
		} else {
657
			return parent::__call($name, $arguments);
658
		}
659
	}
660
}
661
662
/**
663
 * Parses a template file with an *.ss file extension.
664
 *
665
 * In addition to a full template in the templates/ folder, a template in
666
 * templates/Content or templates/Layout will be rendered into $Content and
667
 * $Layout, respectively.
668
 *
669
 * A single template can be parsed by multiple nested {@link SSViewer} instances
670
 * through $Layout/$Content placeholders, as well as <% include MyTemplateFile %> template commands.
671
 *
672
 * <b>Themes</b>
673
 *
674
 * See http://doc.silverstripe.org/themes and http://doc.silverstripe.org/themes:developing
675
 *
676
 * <b>Caching</b>
677
 *
678
 * Compiled templates are cached via {@link SS_Cache}, usually on the filesystem.
679
 * If you put ?flush=1 on your URL, it will force the template to be recompiled.
680
 *
681
 * @see http://doc.silverstripe.org/themes
682
 * @see http://doc.silverstripe.org/themes:developing
683
 *
684
 * @package framework
685
 * @subpackage view
686
 */
687
class SSViewer implements Flushable {
688
689
	/**
690
	 * Identifier for the default theme
691
	 */
692
	const DEFAULT_THEME = '$default';
693
694
	/**
695
	 * @config
696
	 * @var boolean $source_file_comments
697
	 */
698
	private static $source_file_comments = false;
699
700
	/**
701
	 * @ignore
702
	 */
703
	private static $template_cache_flushed = false;
704
705
	/**
706
	 * @ignore
707
	 */
708
	private static $cacheblock_cache_flushed = false;
709
710
	/**
711
	 * Set whether HTML comments indicating the source .SS file used to render this page should be
712
	 * included in the output.  This is enabled by default
713
	 *
714
	 * @deprecated 4.0 Use the "SSViewer.source_file_comments" config setting instead
715
	 * @param boolean $val
716
	 */
717
	public static function set_source_file_comments($val) {
718
		Deprecation::notice('4.0', 'Use the "SSViewer.source_file_comments" config setting instead');
719
		Config::inst()->update('SSViewer', 'source_file_comments', $val);
720
	}
721
722
	/**
723
	 * @deprecated 4.0 Use the "SSViewer.source_file_comments" config setting instead
724
	 * @return boolean
725
	 */
726
	public static function get_source_file_comments() {
727
		Deprecation::notice('4.0', 'Use the "SSViewer.source_file_comments" config setting instead');
728
		return Config::inst()->get('SSViewer', 'source_file_comments');
729
	}
730
731
	/**
732
	 * @var array $templates List of templates to select from
733
	 */
734
	private $templates = null;
735
736
	/**
737
	 * @var string $chosen Absolute path to chosen template file
738
	 */
739
	private $chosen = null;
740
741
	/**
742
	 * @var array Templates to use when looking up 'Layout' or 'Content'
743
	 */
744
	private $subTemplates = null;
745
746
	/**
747
	 * @var boolean
748
	 */
749
	protected $rewriteHashlinks = true;
750
751
	/**
752
	 * @config
753
	 * @var string A list (highest priority first) of themes to use
754
	 * Only used when {@link $theme_enabled} is set to TRUE.
755
	 */
756
	private static $themes = [];
757
758
	/**
759
	 * @deprecated 4.0..5.0
760
	 * @config
761
	 * @var string The used "theme", which usually consists of templates, images and stylesheets.
762
	 * Only used when {@link $theme_enabled} is set to TRUE, and $themes is empty
763
	 */
764
	private static $theme = null;
765
766
	/**
767
	 * @config
768
	 * @var boolean Use the theme. Set to FALSE in order to disable themes,
769
	 * which can be useful for scenarios where theme overrides are temporarily undesired,
770
	 * such as an administrative interface separate from the website theme.
771
	 * It retains the theme settings to be re-enabled, for example when a website content
772
	 * needs to be rendered from within this administrative interface.
773
	 */
774
	private static $theme_enabled = true;
775
776
	/**
777
	 * @var boolean
778
	 */
779
	protected $includeRequirements = true;
780
781
	/**
782
	 * @var TemplateParser
783
	 */
784
	protected $parser;
785
786
	/*
787
	 * Default prepended cache key for partial caching
788
	 *
789
	 * @var string
790
	 * @config
791
	 */
792
	private static $global_key = '$CurrentReadingMode, $CurrentUser.ID';
793
794
	/**
795
	 * Triggered early in the request when someone requests a flush.
796
	 */
797
	public static function flush() {
798
		self::flush_template_cache(true);
799
		self::flush_cacheblock_cache(true);
800
	}
801
802
	/**
803
	 * Create a template from a string instead of a .ss file
804
	 *
805
	 * @param string $content The template content
806
	 * @param bool|void $cacheTemplate Whether or not to cache the template from string
807
	 * @return SSViewer
808
	 */
809
	public static function fromString($content, $cacheTemplate = null) {
810
		$viewer = new SSViewer_FromString($content);
811
		if ($cacheTemplate !== null) {
812
			$viewer->setCacheTemplate($cacheTemplate);
813
		}
814
		return $viewer;
815
	}
816
817
	/**
818
	 * Assign the list of active themes to apply.
819
	 * If default themes should be included add $default as the last entry.
820
	 *
821
	 * @param array $themes
822
	 */
823
	public static function set_themes($themes = []) {
824
		Config::inst()->remove('SSViewer', 'themes');
825
		Config::inst()->update('SSViewer', 'themes', $themes);
826
	}
827
828
	public static function add_themes($themes = []) {
829
		Config::inst()->update('SSViewer', 'themes', $themes);
830
	}
831
832
	public static function get_themes() {
833
		$default = [self::DEFAULT_THEME];
834
835
		if (!Config::inst()->get('SSViewer', 'theme_enabled')) {
836
			return $default;
837
		}
838
839
		// Explicit list is assigned
840
		if ($list = Config::inst()->get('SSViewer', 'themes')) {
841
			return $list;
842
		}
843
844
		// Support legacy behaviour
845
		if ($theme = Config::inst()->get('SSViewer', 'theme')) {
846
			return [$theme, self::DEFAULT_THEME];
847
		}
848
849
		return $default;
850
	}
851
852
	/**
853
	 * @deprecated 4.0 Use the "SSViewer.theme" config setting instead
854
	 * @param string $theme The "base theme" name (without underscores).
855
	 */
856
	public static function set_theme($theme) {
857
		Deprecation::notice('4.0', 'Use the "SSViewer#set_themes" instead');
858
		self::set_themes([$theme, self::DEFAULT_THEME]);
859
	}
860
861
	/**
862
	 * Traverses the given the given class context looking for candidate template names
863
	 * which match each item in the class hierarchy. The resulting list of template candidates
864
	 * may or may not exist, but you can invoke {@see SSViewer::chooseTemplate} on any list
865
	 * to determine the best candidate based on the current themes.
866
	 *
867
	 * @param string|object $classOrObject Valid class name, or object
868
	 * @param string $suffix
869
	 * @param string $baseClass Class to halt ancestry search at
870
	 * @return array
871
	 */
872
	public static function get_templates_by_class($classOrObject, $suffix = '', $baseClass = null) {
873
		// Figure out the class name from the supplied context.
874
		if (!is_object($classOrObject) && !(
875
			is_string($classOrObject) && class_exists($classOrObject)
876
		)) {
877
			throw new InvalidArgumentException(
878
				'SSViewer::get_templates_by_class() expects a valid class name as its first parameter.'
879
			);
880
		}
881
		$templates = array();
882
		$classes = array_reverse(ClassInfo::ancestry($classOrObject));
883
		foreach($classes as $class) {
884
			$template = $class . $suffix;
885
			$templates[] = $template;
886
			$templates[] = ['type' => 'Includes', $template];
887
888
			// If the class is "Page_Controller", look for Page.ss
889
			if (stripos($class, '_controller') !== false) {
890
				$templates[] = str_ireplace('_controller', '', $class) . $suffix;
891
			}
892
893
			if($baseClass && $class == $baseClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $baseClass of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
894
				break;
895
			}
896
		}
897
		return $templates;
898
	}
899
900
	/**
901
	 * @param string|array $templates If passed as a string with .ss extension, used as the "main" template.
902
	 *  If passed as an array, it can be used for template inheritance (first found template "wins").
903
	 *  Usually the array values are PHP class names, which directly correlate to template names.
904
	 *  <code>
905
	 *  array('MySpecificPage', 'MyPage', 'Page')
906
	 *  </code>
907
	 * @param TemplateParser $parser
908
	 */
909
	public function __construct($templates, TemplateParser $parser = null) {
910
		if ($parser) {
911
			$this->setParser($parser);
912
		}
913
914
		$this->setTemplate($templates);
915
916
		if(!$this->chosen) {
917
			$message = 'None of the following templates could be found: ';
918
			$message .= print_r($templates, true);
919
920
			$themes = self::get_themes();
921
			if(!$themes) {
922
				$message .= ' (no theme in use)';
923
			} else {
924
				$message .= ' in themes "' . print_r($themes, true) . '"';
925
			}
926
927
			user_error($message, E_USER_WARNING);
928
		}
929
	}
930
931
	public function setTemplate($templates) {
932
		$this->templates = $templates;
933
		$this->chosen = $this->chooseTemplate($templates);
934
		$this->subTemplates = [];
935
	}
936
937
	/**
938
	 * Find the template to use for a given list
939
	 *
940
	 * @param array|string $templates
941
	 * @return string
942
	 */
943
	public static function chooseTemplate($templates) {
944
		return ThemeResourceLoader::instance()->findTemplate($templates, self::get_themes());
945
	}
946
947
	/**
948
	 * Set the template parser that will be used in template generation
949
	 * @param \TemplateParser $parser
950
	 */
951
	public function setParser(TemplateParser $parser)
952
	{
953
		$this->parser = $parser;
954
	}
955
956
	/**
957
	 * Returns the parser that is set for template generation
958
	 * @return \TemplateParser
959
	 */
960
	public function getParser()
961
	{
962
		if (!$this->parser) {
963
			$this->setParser(Injector::inst()->get('SSTemplateParser'));
964
		}
965
		return $this->parser;
966
	}
967
968
	/**
969
	 * Returns true if at least one of the listed templates exists.
970
	 *
971
	 * @param array $templates
972
	 *
973
	 * @return boolean
974
	 */
975
	public static function hasTemplate($templates) {
976
		return (bool)ThemeResourceLoader::instance()->findTemplate($templates, self::get_themes());
977
	}
978
979
	/**
980
	 * Set a global rendering option.
981
	 *
982
	 * The following options are available:
983
	 *  - rewriteHashlinks: If true (the default), <a href="#..."> will be rewritten to contain the
984
	 *    current URL.  This lets it play nicely with our <base> tag.
985
	 *  - If rewriteHashlinks = 'php' then, a piece of PHP script will be inserted before the hash
986
	 *    links: "<?php echo $_SERVER['REQUEST_URI']; ?>".  This is useful if you're generating a
987
	 *    page that will be saved to a .php file and may be accessed from different URLs.
988
	 *
989
	 * @deprecated 4.0 Use the "SSViewer.rewrite_hash_links" config setting instead
990
	 * @param string $optionName
991
	 * @param mixed $optionVal
992
	 */
993
	public static function setOption($optionName, $optionVal) {
994
		if($optionName == 'rewriteHashlinks') {
995
			Deprecation::notice('4.0', 'Use the "SSViewer.rewrite_hash_links" config setting instead');
996
			Config::inst()->update('SSViewer', 'rewrite_hash_links', $optionVal);
997
		} else {
998
			Deprecation::notice('4.0', 'Use the "SSViewer.' . $optionName . '" config setting instead');
999
			Config::inst()->update('SSViewer', $optionName, $optionVal);
1000
		}
1001
	}
1002
1003
	/**
1004
 	 * @deprecated 4.0 Use the "SSViewer.rewrite_hash_links" config setting instead
1005
 	 * @param string
1006
 	 * @return mixed
1007
	 */
1008
	public static function getOption($optionName) {
1009
		if($optionName == 'rewriteHashlinks') {
1010
			Deprecation::notice('4.0', 'Use the "SSViewer.rewrite_hash_links" config setting instead');
1011
			return Config::inst()->get('SSViewer', 'rewrite_hash_links');
1012
		} else {
1013
			Deprecation::notice('4.0', 'Use the "SSViewer.' . $optionName . '" config setting instead');
1014
			return Config::inst()->get('SSViewer', $optionName);
1015
		}
1016
	}
1017
1018
	/**
1019
	 * @config
1020
	 * @var boolean
1021
	 */
1022
	private static $rewrite_hash_links = true;
1023
1024
	protected static $topLevel = array();
1025
1026
	public static function topLevel() {
1027
		if(SSViewer::$topLevel) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \SSViewer::$topLevel of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1028
			return SSViewer::$topLevel[sizeof(SSViewer::$topLevel)-1];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1029
		}
1030
	}
1031
1032
	/**
1033
	 * Call this to disable rewriting of <a href="#xxx"> links.  This is useful in Ajax applications.
1034
	 * It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process();
1035
	 */
1036
	public function dontRewriteHashlinks() {
1037
		$this->rewriteHashlinks = false;
1038
		Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
1039
		return $this;
1040
	}
1041
1042
	public function exists() {
1043
		return $this->chosen;
1044
	}
1045
1046
	/**
1047
	 * @param string $identifier A template name without '.ss' extension or path
1048
	 * @param string $type The template type, either "main", "Includes" or "Layout"
1049
	 *
1050
	 * @return string Full system path to a template file
1051
	 */
1052
	public static function getTemplateFileByType($identifier, $type = null) {
1053
		return ThemeResourceLoader::instance()->findTemplate(['type' => $type, $identifier], self::get_themes());
1054
	}
1055
1056
	/**
1057
	 * Clears all parsed template files in the cache folder.
1058
	 *
1059
	 * Can only be called once per request (there may be multiple SSViewer instances).
1060
	 *
1061
	 * @param bool $force Set this to true to force a re-flush. If left to false, flushing
1062
	 * may only be performed once a request.
1063
	 */
1064
	public static function flush_template_cache($force = false) {
1065
		if (!self::$template_cache_flushed || $force) {
1066
			$dir = dir(TEMP_FOLDER);
1067
			while (false !== ($file = $dir->read())) {
1068
				if (strstr($file, '.cache')) unlink(TEMP_FOLDER . '/' . $file);
1069
			}
1070
			self::$template_cache_flushed = true;
1071
		}
1072
	}
1073
1074
	/**
1075
	 * Clears all partial cache blocks.
1076
	 *
1077
	 * Can only be called once per request (there may be multiple SSViewer instances).
1078
	 *
1079
	 * @param bool $force Set this to true to force a re-flush. If left to false, flushing
1080
	 * may only be performed once a request.
1081
	 */
1082
	public static function flush_cacheblock_cache($force = false) {
1083
		if (!self::$cacheblock_cache_flushed || $force) {
1084
			$cache = SS_Cache::factory('cacheblock');
1085
			$backend = $cache->getBackend();
1086
1087
			if(
1088
				$backend instanceof Zend_Cache_Backend_ExtendedInterface
1089
				&& ($capabilities = $backend->getCapabilities())
1090
				&& $capabilities['tags']
1091
			) {
1092
				$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, $cache->getTags());
1093
			} else {
1094
				$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
1095
			}
1096
1097
1098
			self::$cacheblock_cache_flushed = true;
1099
		}
1100
	}
1101
1102
	/**
1103
	 * @var Zend_Cache_Core
1104
	 */
1105
	protected $partialCacheStore = null;
1106
1107
	/**
1108
	 * Set the cache object to use when storing / retrieving partial cache blocks.
1109
	 *
1110
	 * @param Zend_Cache_Core $cache
1111
	 */
1112
	public function setPartialCacheStore($cache) {
1113
		$this->partialCacheStore = $cache;
1114
	}
1115
1116
	/**
1117
	 * Get the cache object to use when storing / retrieving partial cache blocks.
1118
	 *
1119
	 * @return Zend_Cache_Core
1120
	 */
1121
	public function getPartialCacheStore() {
1122
		return $this->partialCacheStore ? $this->partialCacheStore : SS_Cache::factory('cacheblock');
1123
	}
1124
1125
	/**
1126
	 * Flag whether to include the requirements in this response.
1127
	 *
1128
	 * @param boolean
1129
	 */
1130
	public function includeRequirements($incl = true) {
1131
		$this->includeRequirements = $incl;
1132
	}
1133
1134
	/**
1135
	 * An internal utility function to set up variables in preparation for including a compiled
1136
	 * template, then do the include
1137
	 *
1138
	 * Effectively this is the common code that both SSViewer#process and SSViewer_FromString#process call
1139
	 *
1140
	 * @param string $cacheFile - The path to the file that contains the template compiled to PHP
1141
	 * @param Object $item - The item to use as the root scope for the template
1142
	 * @param array|null $overlay - Any variables to layer on top of the scope
1143
	 * @param array|null $underlay - Any variables to layer underneath the scope
1144
	 * @param Object $inheritedScope - the current scope of a parent template including a sub-template
1145
	 *
1146
	 * @return string - The result of executing the template
1147
	 */
1148
	protected function includeGeneratedTemplate($cacheFile, $item, $overlay, $underlay, $inheritedScope = null) {
0 ignored issues
show
Coding Style introduced by
includeGeneratedTemplate uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1149
		if(isset($_GET['showtemplate']) && $_GET['showtemplate'] && Permission::check('ADMIN')) {
1150
			$lines = file($cacheFile);
1151
			echo "<h2>Template: $cacheFile</h2>";
1152
			echo "<pre>";
1153
			foreach($lines as $num => $line) {
1154
				echo str_pad($num+1,5) . htmlentities($line, ENT_COMPAT, 'UTF-8');
1155
			}
1156
			echo "</pre>";
1157
		}
1158
1159
		$cache = $this->getPartialCacheStore();
1160
		$scope = new SSViewer_DataPresenter($item, $overlay, $underlay, $inheritedScope);
1161
		$val = '';
1162
1163
		include($cacheFile);
1164
1165
		return $val;
1166
	}
1167
1168
	/**
1169
	 * The process() method handles the "meat" of the template processing.
1170
	 *
1171
	 * It takes care of caching the output (via {@link SS_Cache}), as well as
1172
	 * replacing the special "$Content" and "$Layout" placeholders with their
1173
	 * respective subtemplates.
1174
	 *
1175
	 * The method injects extra HTML in the header via {@link Requirements::includeInHTML()}.
1176
	 *
1177
	 * Note: You can call this method indirectly by {@link ViewableData->renderWith()}.
1178
	 *
1179
	 * @param ViewableData $item
1180
	 * @param array|null $arguments - arguments to an included template
1181
	 * @param Object $inheritedScope - the current scope of a parent template including a sub-template
1182
	 *
1183
	 * @return DBHTMLText Parsed template output.
1184
	 */
1185
	public function process($item, $arguments = null, $inheritedScope = null) {
0 ignored issues
show
Coding Style introduced by
process uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1186
		SSViewer::$topLevel[] = $item;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1187
1188
		$template = $this->chosen;
1189
1190
		$cacheFile = TEMP_FOLDER . "/.cache"
1191
			. str_replace(array('\\','/',':'), '.', Director::makeRelative(realpath($template)));
1192
		$lastEdited = filemtime($template);
1193
1194
		if(!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited) {
1195
			$content = file_get_contents($template);
1196
			$content = $this->parseTemplateContent($content, $template);
1197
1198
			$fh = fopen($cacheFile,'w');
1199
			fwrite($fh, $content);
1200
			fclose($fh);
1201
		}
1202
1203
		$underlay = array('I18NNamespace' => basename($template));
1204
1205
		// Makes the rendered sub-templates available on the parent item,
1206
		// through $Content and $Layout placeholders.
1207
		foreach(array('Content', 'Layout') as $subtemplate) {
1208
			$sub = null;
1209
			if(isset($this->subTemplates[$subtemplate])) {
1210
				$sub = $this->subTemplates[$subtemplate];
1211
			}
1212
			elseif(!is_array($this->templates)) {
1213
				$sub = ['type' => $subtemplate, $this->templates];
1214
			}
1215
			elseif(!array_key_exists('type', $this->templates) || !$this->templates['type']) {
1216
				$sub = array_merge($this->templates, ['type' => $subtemplate]);
1217
			}
1218
1219
			if ($sub) {
1220
				$subtemplateViewer = clone $this;
1221
				// Disable requirements - this will be handled by the parent template
1222
				$subtemplateViewer->includeRequirements(false);
1223
				// Select the right template
1224
				$subtemplateViewer->setTemplate($sub);
1225
1226
				if ($subtemplateViewer->exists()) {
1227
					$underlay[$subtemplate] = $subtemplateViewer->process($item, $arguments);
1228
				}
1229
			}
1230
		}
1231
1232
		$output = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, $underlay, $inheritedScope);
1233
1234
		if($this->includeRequirements) {
1235
			$output = Requirements::includeInHTML($output);
1236
		}
1237
1238
		array_pop(SSViewer::$topLevel);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1239
1240
		// If we have our crazy base tag, then fix # links referencing the current page.
1241
1242
		$rewrite = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1243
		if($this->rewriteHashlinks && $rewrite) {
1244
			if(strpos($output, '<base') !== false) {
1245
				if($rewrite === 'php') {
1246
					$thisURLRelativeToBase = "<?php echo Convert::raw2att(preg_replace(\"/^(\\\\/)+/\", \"/\", \$_SERVER['REQUEST_URI'])); ?>";
1247
				} else {
1248
					$thisURLRelativeToBase = Convert::raw2att(preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI']));
1249
				}
1250
1251
				$output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);
1252
			}
1253
		}
1254
1255
		return DBField::create_field('HTMLFragment', $output);
1256
	}
1257
1258
	/**
1259
	 * Execute the given template, passing it the given data.
1260
	 * Used by the <% include %> template tag to process templates.
1261
	 *
1262
	 * @param string $template Template name
1263
	 * @param mixed $data Data context
1264
	 * @param array $arguments Additional arguments
1265
	 * @return string Evaluated result
1266
	 */
1267
	public static function execute_template($template, $data, $arguments = null, $scope = null) {
1268
		$v = new SSViewer($template);
1269
		$v->includeRequirements(false);
1270
1271
		return $v->process($data, $arguments, $scope);
1272
	}
1273
1274
	/**
1275
	 * Execute the evaluated string, passing it the given data.
1276
	 * Used by partial caching to evaluate custom cache keys expressed using
1277
	 * template expressions
1278
	 *
1279
	 * @param string $content Input string
1280
	 * @param mixed $data Data context
1281
	 * @param array $arguments Additional arguments
1282
	 * @return string Evaluated result
1283
	 */
1284
	public static function execute_string($content, $data, $arguments = null) {
1285
		$v = SSViewer::fromString($content);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1286
		$v->includeRequirements(false);
1287
1288
		return $v->process($data, $arguments);
1289
	}
1290
1291
	public function parseTemplateContent($content, $template="") {
1292
		return $this->getParser()->compileString(
1293
			$content,
1294
			$template,
1295
			Director::isDev() && Config::inst()->get('SSViewer', 'source_file_comments')
1296
		);
1297
	}
1298
1299
	/**
1300
	 * Returns the filenames of the template that will be rendered.  It is a map that may contain
1301
	 * 'Content' & 'Layout', and will have to contain 'main'
1302
	 */
1303
	public function templates() {
1304
		return array_merge(['main' => $this->chosen], $this->subTemplates);
1305
	}
1306
1307
	/**
1308
	 * @param string $type "Layout" or "main"
1309
	 * @param string $file Full system path to the template file
1310
	 */
1311
	public function setTemplateFile($type, $file) {
1312
		if (!$type || $type == 'main') $this->chosen = $file;
1313
		else $this->subTemplates[$type] = $file;
1314
	}
1315
1316
	/**
1317
	 * Return an appropriate base tag for the given template.
1318
	 * It will be closed on an XHTML document, and unclosed on an HTML document.
1319
	 *
1320
	 * @param $contentGeneratedSoFar The content of the template generated so far; it should contain
1321
	 * the DOCTYPE declaration.
1322
	 */
1323
	public static function get_base_tag($contentGeneratedSoFar) {
1324
		$base = Director::absoluteBaseURL();
1325
1326
		// Is the document XHTML?
1327
		if(preg_match('/<!DOCTYPE[^>]+xhtml/i', $contentGeneratedSoFar)) {
1328
			return "<base href=\"$base\" />";
1329
		} else {
1330
			return "<base href=\"$base\"><!--[if lte IE 6]></base><![endif]-->";
1331
		}
1332
	}
1333
}
1334
1335
/**
1336
 * Special SSViewer that will process a template passed as a string, rather than a filename.
1337
 * @package framework
1338
 * @subpackage view
1339
 */
1340
class SSViewer_FromString extends SSViewer {
1341
1342
	/**
1343
	 * The global template caching behaviour if no instance override is specified
1344
	 * @config
1345
	 * @var bool
1346
	 */
1347
	private static $cache_template = true;
1348
1349
	/**
1350
	 * The template to use
1351
	 * @var string
1352
	 */
1353
	protected $content;
1354
1355
	/**
1356
	 * Indicates whether templates should be cached
1357
	 * @var bool
1358
	 */
1359
	protected $cacheTemplate;
1360
1361
	public function __construct($content, TemplateParser $parser = null) {
1362
		if ($parser) {
1363
			$this->setParser($parser);
1364
		}
1365
1366
		$this->content = $content;
1367
	}
1368
1369
	public function process($item, $arguments = null, $scope = null) {
0 ignored issues
show
Coding Style introduced by
process uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1370
		$hash = sha1($this->content);
1371
		$cacheFile = TEMP_FOLDER . "/.cache.$hash";
1372
1373
		if(!file_exists($cacheFile) || isset($_GET['flush'])) {
1374
			$content = $this->parseTemplateContent($this->content, "string sha1=$hash");
1375
			$fh = fopen($cacheFile,'w');
1376
			fwrite($fh, $content);
1377
			fclose($fh);
1378
		}
1379
1380
		$val = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, null, $scope);
1381
1382
		if ($this->cacheTemplate !== null) {
1383
			$cacheTemplate = $this->cacheTemplate;
1384
		} else {
1385
			$cacheTemplate = Config::inst()->get('SSViewer_FromString', 'cache_template');
1386
		}
1387
1388
		if (!$cacheTemplate) {
1389
			unlink($cacheFile);
1390
		}
1391
1392
		return $val;
1393
	}
1394
1395
	/**
1396
	 * @param boolean $cacheTemplate
1397
	 */
1398
	public function setCacheTemplate($cacheTemplate) {
1399
		$this->cacheTemplate = (bool) $cacheTemplate;
1400
	}
1401
1402
	/**
1403
	 * @return boolean
1404
	 */
1405
	public function getCacheTemplate() {
1406
		return $this->cacheTemplate;
1407
	}
1408
}
1409