Completed
Push — master ( f548dd...46b15a )
by Hamish
23s
created

view/SSViewer.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use SilverStripe\ORM\FieldType\DBField;
4
use SilverStripe\ORM\FieldType\DBHTMLText;
5
use SilverStripe\Security\Permission;
6
use SilverStripe\View\TemplateLoader;
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();
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,
115
					$this->currentIndex) = $this->itemStack[$this->upIndex];
116
				break;
117
118
			case 'Top':
119
				list($this->item, $this->itemIterator, $this->itemIteratorTotal, $unused2, $this->upIndex,
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;
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;
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;
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;
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.
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) {
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
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)
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
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
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
	 * @config
691
	 * @var boolean $source_file_comments
692
	 */
693
	private static $source_file_comments = false;
694
695
	/**
696
	 * @ignore
697
	 */
698
	private static $template_cache_flushed = false;
699
700
	/**
701
	 * @ignore
702
	 */
703
	private static $cacheblock_cache_flushed = false;
704
705
	/**
706
	 * Set whether HTML comments indicating the source .SS file used to render this page should be
707
	 * included in the output.  This is enabled by default
708
	 *
709
	 * @deprecated 4.0 Use the "SSViewer.source_file_comments" config setting instead
710
	 * @param boolean $val
711
	 */
712
	public static function set_source_file_comments($val) {
713
		Deprecation::notice('4.0', 'Use the "SSViewer.source_file_comments" config setting instead');
714
		Config::inst()->update('SSViewer', 'source_file_comments', $val);
715
	}
716
717
	/**
718
	 * @deprecated 4.0 Use the "SSViewer.source_file_comments" config setting instead
719
	 * @return boolean
720
	 */
721
	public static function get_source_file_comments() {
722
		Deprecation::notice('4.0', 'Use the "SSViewer.source_file_comments" config setting instead');
723
		return Config::inst()->get('SSViewer', 'source_file_comments');
724
	}
725
726
	/**
727
	 * @var array $templates List of templates to select from
728
	 */
729
	private $templates = null;
730
731
	/**
732
	 * @var string $chosen Absolute path to chosen template file
733
	 */
734
	private $chosen = null;
735
736
	/**
737
	 * @var array Templates to use when looking up 'Layout' or 'Content'
738
	 */
739
	private $subTemplates = null;
740
741
	/**
742
	 * @var boolean
743
	 */
744
	protected $rewriteHashlinks = true;
745
746
	/**
747
	 * @config
748
	 * @var string A list (highest priority first) of themes to use
749
	 * Only used when {@link $theme_enabled} is set to TRUE.
750
	 */
751
	private static $themes = [];
752
753
	/**
754
	 * @deprecated 4.0..5.0
755
	 * @config
756
	 * @var string The used "theme", which usually consists of templates, images and stylesheets.
757
	 * Only used when {@link $theme_enabled} is set to TRUE, and $themes is empty
758
	 */
759
	private static $theme = null;
760
761
	/**
762
	 * @config
763
	 * @var boolean Use the theme. Set to FALSE in order to disable themes,
764
	 * which can be useful for scenarios where theme overrides are temporarily undesired,
765
	 * such as an administrative interface separate from the website theme.
766
	 * It retains the theme settings to be re-enabled, for example when a website content
767
	 * needs to be rendered from within this administrative interface.
768
	 */
769
	private static $theme_enabled = true;
770
771
	/**
772
	 * @var boolean
773
	 */
774
	protected $includeRequirements = true;
775
776
	/**
777
	 * @var TemplateParser
778
	 */
779
	protected $parser;
780
781
	/*
782
	 * Default prepended cache key for partial caching
783
	 *
784
	 * @var string
785
	 * @config
786
	 */
787
	private static $global_key = '$CurrentReadingMode, $CurrentUser.ID';
788
789
	/**
790
	 * Triggered early in the request when someone requests a flush.
791
	 */
792
	public static function flush() {
793
		self::flush_template_cache(true);
794
		self::flush_cacheblock_cache(true);
795
	}
796
797
	/**
798
	 * Create a template from a string instead of a .ss file
799
	 *
800
	 * @param string $content The template content
801
	 * @param bool|void $cacheTemplate Whether or not to cache the template from string
802
	 * @return SSViewer
803
	 */
804
	public static function fromString($content, $cacheTemplate = null) {
805
		$viewer = new SSViewer_FromString($content);
806
		if ($cacheTemplate !== null) {
807
			$viewer->setCacheTemplate($cacheTemplate);
808
		}
809
		return $viewer;
810
	}
811
812
	public static function set_themes($themes = []) {
813
		Config::inst()->remove('SSViewer', 'themes');
814
		Config::inst()->update('SSViewer', 'themes', $themes);
815
	}
816
817
	public static function add_themes($themes = []) {
818
		Config::inst()->update('SSViewer', 'themes', $themes);
819
	}
820
821
	public static function get_themes() {
822
		$res = ['$default'];
823
824
		if (Config::inst()->get('SSViewer', 'theme_enabled')) {
825
			if ($list = Config::inst()->get('SSViewer', 'themes')) $res = $list;
826
			elseif ($theme = Config::inst()->get('SSViewer', 'theme')) $res = [$theme, '$default'];
827
		}
828
829
		return $res;
830
	}
831
832
	/**
833
	 * @deprecated 4.0 Use the "SSViewer.theme" config setting instead
834
	 * @param string $theme The "base theme" name (without underscores).
835
	 */
836
	public static function set_theme($theme) {
837
		Deprecation::notice('4.0', 'Use the "SSViewer#set_themes" instead');
838
		self::set_themes([$theme]);
839
	}
840
841
	/**
842
	 * Traverses the given the given class context looking for templates with the relevant name.
843
	 *
844
	 * @param $className string - valid class name
845
	 * @param $suffix string
846
	 * @param $baseClass string
847
	 *
848
	 * @return array
849
	 */
850
	public static function get_templates_by_class($className, $suffix = '', $baseClass = null) {
851
		// Figure out the class name from the supplied context.
852
		if(!is_string($className) || !class_exists($className)) {
853
			throw new InvalidArgumentException('SSViewer::get_templates_by_class() expects a valid class name as ' .
854
				'its first parameter.');
855
			return array();
0 ignored issues
show
return array(); 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...
856
		}
857
		$templates = array();
858
		$classes = array_reverse(ClassInfo::ancestry($className));
859
		foreach($classes as $class) {
860
			$template = $class . $suffix;
861
			if(SSViewer::hasTemplate($template)) $templates[] = $template;
0 ignored issues
show
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...
862
			elseif(SSViewer::hasTemplate('Includes/'.$template)) $templates[] = 'Includes/'.$template;
863
864
			// If the class is "Page_Controller", look for Page.ss
865
			if(stripos($class,'_controller') !== false) {
866
				$template = str_ireplace('_controller','',$class) . $suffix;
867
				if(SSViewer::hasTemplate($template)) $templates[] = $template;
0 ignored issues
show
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...
868
			}
869
870
			if($baseClass && $class == $baseClass) break;
871
		}
872
		return $templates;
873
	}
874
875
	/**
876
	 * @param string|array $templateList If passed as a string with .ss extension, used as the "main" template.
877
	 *  If passed as an array, it can be used for template inheritance (first found template "wins").
878
	 *  Usually the array values are PHP class names, which directly correlate to template names.
879
	 *  <code>
880
	 *  array('MySpecificPage', 'MyPage', 'Page')
881
	 *  </code>
882
	 */
883
	public function __construct($templates, TemplateParser $parser = null) {
884
		if ($parser) {
885
			$this->setParser($parser);
886
		}
887
888
		$this->setTemplate($templates);
889
890
		if(!$this->chosen) {
891
			$message = 'None of the following templates could be found: ';
892
			$message .= print_r($templates, true);
893
894
			$themes = self::get_themes();
895
			if(!$themes) {
896
				$message .= ' (no theme in use)';
897
			} else {
898
				$message .= ' in themes "' . print_r($themes, true) . '"';
899
			}
900
901
			user_error($message, E_USER_WARNING);
902
		}
903
	}
904
905
	public function setTemplate($templates) {
906
		$this->templates = $templates;
907
		$this->chosen = TemplateLoader::instance()->findTemplate($templates, self::get_themes());
908
		$this->subTemplates = [];
909
	}
910
911
	/**
912
	 * Set the template parser that will be used in template generation
913
	 * @param \TemplateParser $parser
914
	 */
915
	public function setParser(TemplateParser $parser)
916
	{
917
		$this->parser = $parser;
918
	}
919
920
	/**
921
	 * Returns the parser that is set for template generation
922
	 * @return \TemplateParser
923
	 */
924
	public function getParser()
925
	{
926
		if (!$this->parser) {
927
			$this->setParser(Injector::inst()->get('SSTemplateParser'));
928
		}
929
		return $this->parser;
930
	}
931
932
	/**
933
	 * Returns true if at least one of the listed templates exists.
934
	 *
935
	 * @param array $templates
936
	 *
937
	 * @return boolean
938
	 */
939
	public static function hasTemplate($templates) {
940
		return (bool)TemplateLoader::instance()->findTemplate($templates, self::get_themes());
941
	}
942
943
	/**
944
	 * Set a global rendering option.
945
	 *
946
	 * The following options are available:
947
	 *  - rewriteHashlinks: If true (the default), <a href="#..."> will be rewritten to contain the
948
	 *    current URL.  This lets it play nicely with our <base> tag.
949
	 *  - If rewriteHashlinks = 'php' then, a piece of PHP script will be inserted before the hash
950
	 *    links: "<?php echo $_SERVER['REQUEST_URI']; ?>".  This is useful if you're generating a
951
	 *    page that will be saved to a .php file and may be accessed from different URLs.
952
	 *
953
	 * @deprecated 4.0 Use the "SSViewer.rewrite_hash_links" config setting instead
954
	 * @param string $optionName
955
	 * @param mixed $optionVal
956
	 */
957
	public static function setOption($optionName, $optionVal) {
958
		if($optionName == 'rewriteHashlinks') {
959
			Deprecation::notice('4.0', 'Use the "SSViewer.rewrite_hash_links" config setting instead');
960
			Config::inst()->update('SSViewer', 'rewrite_hash_links', $optionVal);
961
		} else {
962
			Deprecation::notice('4.0', 'Use the "SSViewer.' . $optionName . '" config setting instead');
963
			Config::inst()->update('SSViewer', $optionName, $optionVal);
964
		}
965
	}
966
967
	/**
968
 	 * @deprecated 4.0 Use the "SSViewer.rewrite_hash_links" config setting instead
969
 	 * @param string
970
 	 * @return mixed
971
	 */
972
	public static function getOption($optionName) {
973
		if($optionName == 'rewriteHashlinks') {
974
			Deprecation::notice('4.0', 'Use the "SSViewer.rewrite_hash_links" config setting instead');
975
			return Config::inst()->get('SSViewer', 'rewrite_hash_links');
976
		} else {
977
			Deprecation::notice('4.0', 'Use the "SSViewer.' . $optionName . '" config setting instead');
978
			return Config::inst()->get('SSViewer', $optionName);
979
		}
980
	}
981
982
	/**
983
	 * @config
984
	 * @var boolean
985
	 */
986
	private static $rewrite_hash_links = true;
987
988
	protected static $topLevel = array();
989
990
	public static function topLevel() {
991
		if(SSViewer::$topLevel) {
992
			return SSViewer::$topLevel[sizeof(SSViewer::$topLevel)-1];
993
		}
994
	}
995
996
	/**
997
	 * Call this to disable rewriting of <a href="#xxx"> links.  This is useful in Ajax applications.
998
	 * It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process();
999
	 */
1000
	public function dontRewriteHashlinks() {
1001
		$this->rewriteHashlinks = false;
1002
		Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
1003
		return $this;
1004
	}
1005
1006
	public function exists() {
1007
		return $this->chosen;
1008
	}
1009
1010
	/**
1011
	 * @param string $identifier A template name without '.ss' extension or path
1012
	 * @param string $type The template type, either "main", "Includes" or "Layout"
1013
	 *
1014
	 * @return string Full system path to a template file
1015
	 */
1016
	public static function getTemplateFileByType($identifier, $type) {
1017
		return TemplateLoader::instance()->findTemplate(['type' => $type, $identifier], self::get_themes());
1018
	}
1019
1020
	/**
1021
	 * Clears all parsed template files in the cache folder.
1022
	 *
1023
	 * Can only be called once per request (there may be multiple SSViewer instances).
1024
	 *
1025
	 * @param bool $force Set this to true to force a re-flush. If left to false, flushing
1026
	 * may only be performed once a request.
1027
	 */
1028
	public static function flush_template_cache($force = false) {
1029
		if (!self::$template_cache_flushed || $force) {
1030
			$dir = dir(TEMP_FOLDER);
1031
			while (false !== ($file = $dir->read())) {
1032
				if (strstr($file, '.cache')) unlink(TEMP_FOLDER . '/' . $file);
1033
			}
1034
			self::$template_cache_flushed = true;
1035
		}
1036
	}
1037
1038
	/**
1039
	 * Clears all partial cache blocks.
1040
	 *
1041
	 * Can only be called once per request (there may be multiple SSViewer instances).
1042
	 *
1043
	 * @param bool $force Set this to true to force a re-flush. If left to false, flushing
1044
	 * may only be performed once a request.
1045
	 */
1046
	public static function flush_cacheblock_cache($force = false) {
1047
		if (!self::$cacheblock_cache_flushed || $force) {
1048
			$cache = SS_Cache::factory('cacheblock');
1049
			$backend = $cache->getBackend();
1050
1051
			if(
1052
				$backend instanceof Zend_Cache_Backend_ExtendedInterface
1053
				&& ($capabilities = $backend->getCapabilities())
1054
				&& $capabilities['tags']
1055
			) {
1056
				$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, $cache->getTags());
1057
			} else {
1058
				$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
1059
			}
1060
1061
1062
			self::$cacheblock_cache_flushed = true;
1063
		}
1064
	}
1065
1066
	/**
1067
	 * @var Zend_Cache_Core
1068
	 */
1069
	protected $partialCacheStore = null;
1070
1071
	/**
1072
	 * Set the cache object to use when storing / retrieving partial cache blocks.
1073
	 *
1074
	 * @param Zend_Cache_Core $cache
1075
	 */
1076
	public function setPartialCacheStore($cache) {
1077
		$this->partialCacheStore = $cache;
1078
	}
1079
1080
	/**
1081
	 * Get the cache object to use when storing / retrieving partial cache blocks.
1082
	 *
1083
	 * @return Zend_Cache_Core
1084
	 */
1085
	public function getPartialCacheStore() {
1086
		return $this->partialCacheStore ? $this->partialCacheStore : SS_Cache::factory('cacheblock');
1087
	}
1088
1089
	/**
1090
	 * Flag whether to include the requirements in this response.
1091
	 *
1092
	 * @param boolean
1093
	 */
1094
	public function includeRequirements($incl = true) {
1095
		$this->includeRequirements = $incl;
1096
	}
1097
1098
	/**
1099
	 * An internal utility function to set up variables in preparation for including a compiled
1100
	 * template, then do the include
1101
	 *
1102
	 * Effectively this is the common code that both SSViewer#process and SSViewer_FromString#process call
1103
	 *
1104
	 * @param string $cacheFile - The path to the file that contains the template compiled to PHP
1105
	 * @param Object $item - The item to use as the root scope for the template
1106
	 * @param array|null $overlay - Any variables to layer on top of the scope
1107
	 * @param array|null $underlay - Any variables to layer underneath the scope
1108
	 * @param Object $inheritedScope - the current scope of a parent template including a sub-template
1109
	 *
1110
	 * @return string - The result of executing the template
1111
	 */
1112
	protected function includeGeneratedTemplate($cacheFile, $item, $overlay, $underlay, $inheritedScope = null) {
1113
		if(isset($_GET['showtemplate']) && $_GET['showtemplate'] && Permission::check('ADMIN')) {
1114
			$lines = file($cacheFile);
1115
			echo "<h2>Template: $cacheFile</h2>";
1116
			echo "<pre>";
1117
			foreach($lines as $num => $line) {
1118
				echo str_pad($num+1,5) . htmlentities($line, ENT_COMPAT, 'UTF-8');
1119
			}
1120
			echo "</pre>";
1121
		}
1122
1123
		$cache = $this->getPartialCacheStore();
1124
		$scope = new SSViewer_DataPresenter($item, $overlay, $underlay, $inheritedScope);
1125
		$val = '';
1126
1127
		include($cacheFile);
1128
1129
		return $val;
1130
	}
1131
1132
	/**
1133
	 * The process() method handles the "meat" of the template processing.
1134
	 *
1135
	 * It takes care of caching the output (via {@link SS_Cache}), as well as
1136
	 * replacing the special "$Content" and "$Layout" placeholders with their
1137
	 * respective subtemplates.
1138
	 *
1139
	 * The method injects extra HTML in the header via {@link Requirements::includeInHTML()}.
1140
	 *
1141
	 * Note: You can call this method indirectly by {@link ViewableData->renderWith()}.
1142
	 *
1143
	 * @param ViewableData $item
1144
	 * @param array|null $arguments - arguments to an included template
1145
	 * @param Object $inheritedScope - the current scope of a parent template including a sub-template
1146
	 *
1147
	 * @return DBHTMLText Parsed template output.
1148
	 */
1149
	public function process($item, $arguments = null, $inheritedScope = null) {
1150
		SSViewer::$topLevel[] = $item;
1151
1152
		$template = $this->chosen;
1153
1154
		$cacheFile = TEMP_FOLDER . "/.cache"
1155
			. str_replace(array('\\','/',':'), '.', Director::makeRelative(realpath($template)));
1156
		$lastEdited = filemtime($template);
1157
1158
		if(!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited) {
1159
			$content = file_get_contents($template);
1160
			$content = $this->parseTemplateContent($content, $template);
1161
1162
			$fh = fopen($cacheFile,'w');
1163
			fwrite($fh, $content);
1164
			fclose($fh);
1165
		}
1166
1167
		$underlay = array('I18NNamespace' => basename($template));
1168
1169
		// Makes the rendered sub-templates available on the parent item,
1170
		// through $Content and $Layout placeholders.
1171
		foreach(array('Content', 'Layout') as $subtemplate) {
1172
			$sub = null;
1173
			if(isset($this->subTemplates[$subtemplate])) {
1174
				$sub = $this->subTemplates[$subtemplate];
1175
			}
1176
			elseif(!is_array($this->templates)) {
1177
				$sub = ['type' => $subtemplate, $this->templates];
1178
			}
1179
			elseif(!array_key_exists('type', $this->templates) || !$this->templates['type']) {
1180
				$sub = array_merge($this->templates, ['type' => $subtemplate]);
1181
			}
1182
1183
			if ($sub) {
1184
				$subtemplateViewer = clone $this;
1185
				// Disable requirements - this will be handled by the parent template
1186
				$subtemplateViewer->includeRequirements(false);
1187
				// Select the right template
1188
				$subtemplateViewer->setTemplate($sub);
1189
1190
				if ($subtemplateViewer->exists()) {
1191
					$underlay[$subtemplate] = $subtemplateViewer->process($item, $arguments);
1192
				}
1193
			}
1194
		}
1195
1196
		$output = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, $underlay, $inheritedScope);
1197
1198
		if($this->includeRequirements) {
1199
			$output = Requirements::includeInHTML($output);
1200
		}
1201
1202
		array_pop(SSViewer::$topLevel);
1203
1204
		// If we have our crazy base tag, then fix # links referencing the current page.
1205
1206
		$rewrite = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1207
		if($this->rewriteHashlinks && $rewrite) {
1208
			if(strpos($output, '<base') !== false) {
1209
				if($rewrite === 'php') {
1210
					$thisURLRelativeToBase = "<?php echo Convert::raw2att(preg_replace(\"/^(\\\\/)+/\", \"/\", \$_SERVER['REQUEST_URI'])); ?>";
1211
				} else {
1212
					$thisURLRelativeToBase = Convert::raw2att(preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI']));
1213
				}
1214
1215
				$output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);
1216
			}
1217
		}
1218
1219
		return DBField::create_field('HTMLFragment', $output);
1220
	}
1221
1222
	/**
1223
	 * Execute the given template, passing it the given data.
1224
	 * Used by the <% include %> template tag to process templates.
1225
	 *
1226
	 * @param string $template Template name
1227
	 * @param mixed $data Data context
1228
	 * @param array $arguments Additional arguments
1229
	 * @return string Evaluated result
1230
	 */
1231
	public static function execute_template($template, $data, $arguments = null, $scope = null) {
1232
		$v = new SSViewer($template);
1233
		$v->includeRequirements(false);
1234
1235
		return $v->process($data, $arguments, $scope);
1236
	}
1237
1238
	/**
1239
	 * Execute the evaluated string, passing it the given data.
1240
	 * Used by partial caching to evaluate custom cache keys expressed using
1241
	 * template expressions
1242
	 *
1243
	 * @param string $content Input string
1244
	 * @param mixed $data Data context
1245
	 * @param array $arguments Additional arguments
1246
	 * @return string Evaluated result
1247
	 */
1248
	public static function execute_string($content, $data, $arguments = null) {
1249
		$v = SSViewer::fromString($content);
1250
		$v->includeRequirements(false);
1251
1252
		return $v->process($data, $arguments);
1253
	}
1254
1255
	public function parseTemplateContent($content, $template="") {
1256
		return $this->getParser()->compileString(
1257
			$content,
1258
			$template,
1259
			Director::isDev() && Config::inst()->get('SSViewer', 'source_file_comments')
1260
		);
1261
	}
1262
1263
	/**
1264
	 * Returns the filenames of the template that will be rendered.  It is a map that may contain
1265
	 * 'Content' & 'Layout', and will have to contain 'main'
1266
	 */
1267
	public function templates() {
1268
		return array_merge(['main' => $this->chosen], $this->subTemplates);
1269
	}
1270
1271
	/**
1272
	 * @param string $type "Layout" or "main"
1273
	 * @param string $file Full system path to the template file
1274
	 */
1275
	public function setTemplateFile($type, $file) {
1276
		if (!$type || $type == 'main') $this->chosen = $file;
1277
		else $this->subTemplates[$type] = $file;
1278
	}
1279
1280
	/**
1281
	 * Return an appropriate base tag for the given template.
1282
	 * It will be closed on an XHTML document, and unclosed on an HTML document.
1283
	 *
1284
	 * @param $contentGeneratedSoFar The content of the template generated so far; it should contain
1285
	 * the DOCTYPE declaration.
1286
	 */
1287
	public static function get_base_tag($contentGeneratedSoFar) {
1288
		$base = Director::absoluteBaseURL();
1289
1290
		// Is the document XHTML?
1291
		if(preg_match('/<!DOCTYPE[^>]+xhtml/i', $contentGeneratedSoFar)) {
1292
			return "<base href=\"$base\" />";
1293
		} else {
1294
			return "<base href=\"$base\"><!--[if lte IE 6]></base><![endif]-->";
1295
		}
1296
	}
1297
}
1298
1299
/**
1300
 * Special SSViewer that will process a template passed as a string, rather than a filename.
1301
 * @package framework
1302
 * @subpackage view
1303
 */
1304
class SSViewer_FromString extends SSViewer {
1305
1306
	/**
1307
	 * The global template caching behaviour if no instance override is specified
1308
	 * @config
1309
	 * @var bool
1310
	 */
1311
	private static $cache_template = true;
1312
1313
	/**
1314
	 * The template to use
1315
	 * @var string
1316
	 */
1317
	protected $content;
1318
1319
	/**
1320
	 * Indicates whether templates should be cached
1321
	 * @var bool
1322
	 */
1323
	protected $cacheTemplate;
1324
1325
	public function __construct($content, TemplateParser $parser = null) {
1326
		if ($parser) {
1327
			$this->setParser($parser);
1328
		}
1329
1330
		$this->content = $content;
1331
	}
1332
1333
	public function process($item, $arguments = null, $scope = null) {
1334
		$hash = sha1($this->content);
1335
		$cacheFile = TEMP_FOLDER . "/.cache.$hash";
1336
1337
		if(!file_exists($cacheFile) || isset($_GET['flush'])) {
1338
			$content = $this->parseTemplateContent($this->content, "string sha1=$hash");
1339
			$fh = fopen($cacheFile,'w');
1340
			fwrite($fh, $content);
1341
			fclose($fh);
1342
		}
1343
1344
		$val = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, null, $scope);
1345
1346
		if ($this->cacheTemplate !== null) {
1347
			$cacheTemplate = $this->cacheTemplate;
1348
		} else {
1349
			$cacheTemplate = Config::inst()->get('SSViewer_FromString', 'cache_template');
1350
		}
1351
1352
		if (!$cacheTemplate) {
1353
			unlink($cacheFile);
1354
		}
1355
1356
		return $val;
1357
	}
1358
1359
	/**
1360
	 * @param boolean $cacheTemplate
1361
	 */
1362
	public function setCacheTemplate($cacheTemplate) {
1363
		$this->cacheTemplate = (bool) $cacheTemplate;
1364
	}
1365
1366
	/**
1367
	 * @return boolean
1368
	 */
1369
	public function getCacheTemplate() {
1370
		return $this->cacheTemplate;
1371
	}
1372
}
1373