Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

ViewsService::cacheConfiguration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace Elgg;
3
4
use Elgg\Application\CacheHandler;
5
use Elgg\Cache\SystemCache;
6
use Elgg\Filesystem\Directory;
7
use Elgg\Http\Input;
8
9
/**
10
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
11
 *
12
 * Use the elgg_* versions instead.
13
 *
14
 * @access private
15
 *
16
 * @since 1.9.0
17
 */
18
class ViewsService {
19
20
	const VIEW_HOOK = 'view';
21
	const VIEW_VARS_HOOK = 'view_vars';
22
	const OUTPUT_KEY = '__view_output';
23
	const BASE_VIEW_PRIORITY = 500;
24
25
	/**
26
	 * @see fileExists
27
	 * @var array
28
	 */
29
	protected $file_exists_cache = [];
30
31
	/**
32
	 * @var array
33
	 *
34
	 * [viewtype][view] => '/path/to/views/style.css'
35
	 */
36
	private $locations = [];
37
38
	/**
39
	 * @var array Tracks location changes for views
40
	 *
41
	 * [viewtype][view][] => '/path/to/views/style.css'
42
	 */
43
	private $overrides = [];
44
45
	/**
46
	 * @var array Simplecache views (view names are keys)
47
	 *
48
	 * [view] = true
49
	 */
50
	private $simplecache_views = [];
51
52
	/**
53
	 * @var array
54
	 *
55
	 * [view][priority] = extension_view
56
	 */
57
	private $extensions = [];
58
59
	/**
60
	 * @var string[] A list of fallback viewtypes
61
	 */
62
	private $fallbacks = [];
63
64
	/**
65
	 * @var PluginHooksService
66
	 */
67
	private $hooks;
68
69
	/**
70
	 * @var Logger
71
	 */
72
	private $logger;
73
74
	/**
75
	 * @var SystemCache|null This is set if the views are configured via cache
76
	 */
77
	private $cache;
78
79
	/**
80
	 * @var Input
81
	 */
82
	private $input;
83
84
	/**
85
	 * @var string
86
	 */
87
	private $viewtype;
88
89
	/**
90
	 * Constructor
91
	 *
92
	 * @param PluginHooksService $hooks  The hooks service
93
	 * @param Logger             $logger Logger
94
	 * @param Input              $input  Input service
95
	 */
96 695
	public function __construct(PluginHooksService $hooks, Logger $logger, Input $input = null) {
97 695
		$this->hooks = $hooks;
98 695
		$this->logger = $logger;
99 695
		$this->input = $input;
100 695
	}
101
102
	/**
103
	 * Set the viewtype
104
	 *
105
	 * @param string $viewtype Viewtype
106
	 *
107
	 * @return bool
108
	 */
109 96
	public function setViewtype($viewtype = '') {
110 96
		if (!$viewtype) {
111 73
			$this->viewtype = null;
112 73
			return true;
113
		}
114 23
		if ($this->isValidViewtype($viewtype)) {
115 23
			$this->viewtype = $viewtype;
116 23
			return true;
117
		}
118
119
		return false;
120
	}
121
122
	/**
123
	 * Get the viewtype
124
	 *
125
	 * @return string
126
	 */
127 838
	public function getViewtype() {
128 838
		if ($this->viewtype === null) {
129 682
			$this->viewtype = $this->resolveViewtype();
130
		}
131 838
		return $this->viewtype;
132
	}
133
134
	/**
135
	 * If the current viewtype has no views, reset it to "default"
136
	 *
137
	 * @return void
138
	 */
139 18
	public function clampViewtypeToPopulatedViews() {
140 18
		$viewtype = $this->getViewtype();
141 18
		if (empty($this->locations[$viewtype])) {
142
			$this->viewtype = 'default';
143
		}
144 18
	}
145
146
	/**
147
	 * Resolve the initial viewtype
148
	 *
149
	 * @return string
150
	 */
151 682
	private function resolveViewtype() {
152 682
		if ($this->input) {
153 651
			$view = $this->input->get('view', '', false);
154 651
			if ($this->isValidViewtype($view)) {
155 3
				return $view;
156
			}
157
		}
158 682
		$view = elgg_get_config('view');
159 682
		if ($this->isValidViewtype($view)) {
160
			return $view;
161
		}
162
163 682
		return 'default';
164
	}
165
166
	/**
167
	 * Checks if $viewtype is a string suitable for use as a viewtype name
168
	 *
169
	 * @param string $viewtype Potential viewtype name. Alphanumeric chars plus _ allowed.
170
	 *
171
	 * @return bool
172
	 */
173 1026
	public function isValidViewtype($viewtype) {
174 1026
		if (!is_string($viewtype) || $viewtype === '') {
175 682
			return false;
176
		}
177
178 784
		if (preg_match('/\W/', $viewtype)) {
179
			return false;
180
		}
181
182 784
		return true;
183
	}
184
185
	/**
186
	 * Takes a view name and returns the canonical name for that view.
187
	 *
188
	 * @param string $alias The possibly non-canonical view name.
189
	 *
190
	 * @return string The canonical view name.
191
	 */
192 985
	public static function canonicalizeViewName($alias) {
193 985
		if (!is_string($alias)) {
194 34
			return false;
0 ignored issues
show
Bug Best Practice introduced by Evan Winslow
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
195
		}
196
197 985
		$canonical = $alias;
198
		
199 985
		$extension = pathinfo($canonical, PATHINFO_EXTENSION);
200 985
		$hasValidFileExtension = isset(CacheHandler::$extensions[$extension]);
201
202 985
		if (strpos($canonical, "js/") === 0) {
203 109
			$canonical = substr($canonical, 3);
204 109
			if (!$hasValidFileExtension) {
205 109
				$canonical .= ".js";
206
			}
207 985
		} else if (strpos($canonical, "css/") === 0) {
208 37
			$canonical = substr($canonical, 4);
209 37
			if (!$hasValidFileExtension) {
210 32
				$canonical .= ".css";
211
			}
212
		}
213
		
214 985
		return $canonical;
215
	}
216
217
	/**
218
	 * Auto-registers views from a location.
219
	 *
220
	 * @param string $view_base Optional The base of the view name without the view type.
221
	 * @param string $folder    Required The folder to begin looking in
222
	 * @param string $viewtype  The type of view we're looking at (default, rss, etc)
223
	 *
224
	 * @return bool returns false if folder can't be read
225
	 *
226
	 * @see autoregister_views()
227
	 * @access private
228
	 */
229 636
	public function autoregisterViews($view_base, $folder, $viewtype) {
230 636
		$folder = rtrim($folder, '/\\');
231 636
		$view_base = rtrim($view_base, '/\\');
232
233 636
		$handle = opendir($folder);
234 636
		if (!$handle) {
235
			return false;
236
		}
237
		
238 636
		while ($entry = readdir($handle)) {
239 636
			if ($entry[0] === '.') {
240 636
				continue;
241
			}
242
243 636
			$path = "$folder/$entry";
244
245 636
			if (!empty($view_base)) {
246 636
				$view_base_new = $view_base . "/";
247
			} else {
248 636
				$view_base_new = "";
249
			}
250
251 636
			if (is_dir($path)) {
252 636
				$this->autoregisterViews($view_base_new . $entry, $path, $viewtype);
253
			} else {
254 636
				$view = $view_base_new . basename($entry, '.php');
255 636
				$this->setViewLocation($view, $viewtype, $path);
256
			}
257
		}
258
259 636
		return true;
260
	}
261
262
	/**
263
	 * Find the view file
264
	 *
265
	 * @param string $view     View name
266
	 * @param string $viewtype Viewtype
267
	 *
268
	 * @return string Empty string if not found
269
	 * @access private
270
	 * @internal Plugins should not use this.
271
	 */
272 975
	public function findViewFile($view, $viewtype) {
273 975
		if (!isset($this->locations[$viewtype][$view])) {
274 463
			return "";
275
		}
276
277 971
		$path = $this->locations[$viewtype][$view];
278 971
		if ($this->fileExists($path)) {
279 971
			return $path;
280
		}
281
282
		return "";
283
	}
284
285
	/**
286
	 * Set an alternative base location for a view
287
	 *
288
	 * @param string $view     Name of the view
289
	 * @param string $location Full path to the view file
290
	 * @param string $viewtype The viewtype to register this under
291
	 *
292
	 * @return void
293
	 *
294
	 * @see elgg_set_view_location()
295
	 * @access private
296
	 */
297 1
	public function setViewDir($view, $location, $viewtype = '') {
298 1
		$view = self::canonicalizeViewName($view);
299
300 1
		if (empty($viewtype)) {
301 1
			$viewtype = 'default';
302
		}
303
304 1
		$location = rtrim($location, '/\\');
305
306 1
		if ($this->fileExists("$location/$viewtype/$view.php")) {
307
			$this->setViewLocation($view, $viewtype, "$location/$viewtype/$view.php");
308 1
		} elseif ($this->fileExists("$location/$viewtype/$view")) {
309 1
			$this->setViewLocation($view, $viewtype, "$location/$viewtype/$view");
310
		}
311 1
	}
312
313
	/**
314
	 * Register a viewtype to fall back to a default view if a view isn't
315
	 * found for that viewtype.
316
	 *
317
	 * @param string $viewtype The viewtype to register
318
	 *
319
	 * @return void
320
	 *
321
	 * @see elgg_register_viewtype_fallback()
322
	 * @access private
323
	 */
324 2
	public function registerViewtypeFallback($viewtype) {
325 2
		$this->fallbacks[] = $viewtype;
326 2
	}
327
328
	/**
329
	 * Checks if a viewtype falls back to default.
330
	 *
331
	 * @param string $viewtype Viewtype
332
	 *
333
	 * @return bool
334
	 *
335
	 * @see elgg_does_viewtype_fallback()
336
	 * @access private
337
	 */
338 745
	public function doesViewtypeFallback($viewtype) {
339 745
		return in_array($viewtype, $this->fallbacks);
340
	}
341
342
	/**
343
	 * Display a view with a deprecation notice. No missing view NOTICE is logged
344
	 *
345
	 * @param string $view       The name and location of the view to use
346
	 * @param array  $vars       Variables to pass to the view
347
	 * @param string $suggestion Suggestion with the deprecation message
348
	 * @param string $version    Human-readable *release* version: 1.7, 1.8, ...
349
	 *
350
	 * @return string The parsed view
351
	 *
352
	 * @access private
353
	 * @see elgg_view()
354
	 */
355
	public function renderDeprecatedView($view, array $vars, $suggestion, $version) {
356
		$view = self::canonicalizeViewName($view);
357
358
		$rendered = $this->renderView($view, $vars, '', false);
359
		if ($rendered) {
360
			elgg_deprecated_notice("The $view view has been deprecated. $suggestion", $version, 3);
361
		}
362
		return $rendered;
363
	}
364
365
	/**
366
	 * Get the views, including extensions, used to render a view
367
	 *
368
	 * Keys returned are view priorities. View existence is not checked.
369
	 *
370
	 * @param string $view View name
371
	 * @return string[]
372
	 * @access private
373
	 */
374 727
	public function getViewList($view) {
375 727
		if (isset($this->extensions[$view])) {
376 35
			return $this->extensions[$view];
377
		} else {
378 725
			return [self::BASE_VIEW_PRIORITY => $view];
379
		}
380
	}
381
382
	/**
383
	 * Renders a view
384
	 *
385
	 * @param string $view                 Name of the view
386
	 * @param array  $vars                 Variables to pass to the view
387
	 * @param string $viewtype             Viewtype to use
388
	 * @param bool   $issue_missing_notice Should a missing notice be issued
389
	 * @param array  $extensions_tree      Array of views that are before the current view in the extension path
390
	 *
391
	 * @return string
392
	 *
393
	 * @see elgg_view()
394
	 */
395 292
	public function renderView($view, array $vars = [], $viewtype = '', $issue_missing_notice = true, array $extensions_tree = []) {
396 292
		$view = self::canonicalizeViewName($view);
397
398 292
		if (!is_string($view) || !is_string($viewtype)) {
399
			$this->logger->log("View and Viewtype in views must be a strings: $view", 'NOTICE');
0 ignored issues
show
Bug introduced by Evan Winslow
'NOTICE' of type string is incompatible with the type integer expected by parameter $level of Elgg\Logger::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

399
			$this->logger->log("View and Viewtype in views must be a strings: $view", /** @scrutinizer ignore-type */ 'NOTICE');
Loading history...
400
			return '';
401
		}
402
		// basic checking for bad paths
403 292
		if (strpos($view, '..') !== false) {
404
			return '';
405
		}
406
		
407
		// check for extension deadloops
408 292
		if (in_array($view, $extensions_tree)) {
409 1
			$this->logger->log("View $view is detected as an extension of itself. This is not allowed", 'ERROR');
410 1
			return '';
411
		}
412 292
		$extensions_tree[] = $view;
413
414 292
		if (!is_array($vars)) {
415
			$this->logger->log("Vars in views must be an array: $view", 'ERROR');
416
			$vars = [];
417
		}
418
419
		// Get the current viewtype
420 292
		if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
421 160
			$viewtype = $this->getViewtype();
422
		}
423
424
		// allow altering $vars
425
		$vars_hook_params = [
426 292
			'view' => $view,
427 292
			'vars' => $vars,
428 292
			'viewtype' => $viewtype,
429
		];
430 292
		$vars = $this->hooks->trigger(self::VIEW_VARS_HOOK, $view, $vars_hook_params, $vars);
431
432
		// allow $vars to hijack output
433 292
		if (isset($vars[self::OUTPUT_KEY])) {
434 1
			return (string) $vars[self::OUTPUT_KEY];
435
		}
436
437 291
		$viewlist = $this->getViewList($view);
438
439 291
		$content = '';
440 291
		foreach ($viewlist as $priority => $view_name) {
441 291
			if ($priority !== self::BASE_VIEW_PRIORITY) {
442
				// the others are extensions
443 33
				$content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
444 33
				continue;
445
			}
446
			
447
			// actual rendering of a single view
448 291
			$rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
449 291
			if ($rendering !== false) {
450 289
				$content .= $rendering;
451 289
				continue;
452
			}
453
454
			// attempt to load default view
455 104
			if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
456 1
				$rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
457 1
				if ($rendering !== false) {
458 104
					$content .= $rendering;
459
				}
460
			}
461
		}
462
463
		// Plugin hook
464
		$params = [
465 291
			'view' => $view,
466 291
			'vars' => $vars,
467 291
			'viewtype' => $viewtype,
468
		];
469 291
		$content = $this->hooks->trigger(self::VIEW_HOOK, $view, $params, $content);
470
471 291
		return $content;
472
	}
473
474
	/**
475
	 * Wrapper for file_exists() that caches false results (the stat cache only caches true results).
476
	 * This saves us from many unneeded file stat calls when a common view uses a fallback.
477
	 *
478
	 * @param string $path Path to the file
479
	 * @return bool
480
	 */
481 971
	protected function fileExists($path) {
482 971
		if (!isset($this->file_exists_cache[$path])) {
483 635
			$this->file_exists_cache[$path] = file_exists($path);
484
		}
485 971
		return $this->file_exists_cache[$path];
486
	}
487
488
	/**
489
	 * Includes view PHP or static file
490
	 *
491
	 * @param string $view                 The view name
492
	 * @param array  $vars                 Variables passed to view
493
	 * @param string $viewtype             The viewtype
494
	 * @param bool   $issue_missing_notice Log a notice if the view is missing
495
	 *
496
	 * @return string|false output generated by view file inclusion or false
497
	 */
498 291
	private function renderViewFile($view, array $vars, $viewtype, $issue_missing_notice) {
499 291
		$file = $this->findViewFile($view, $viewtype);
500 291
		if (!$file) {
501 104
			if ($issue_missing_notice) {
502 104
				$this->logger->log("$viewtype/$view view does not exist.", 'NOTICE');
0 ignored issues
show
Bug introduced by Steve Clay
'NOTICE' of type string is incompatible with the type integer expected by parameter $level of Elgg\Logger::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

502
				$this->logger->log("$viewtype/$view view does not exist.", /** @scrutinizer ignore-type */ 'NOTICE');
Loading history...
503
			}
504 104
			return false;
505
		}
506
507 290
		if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
508 282
			ob_start();
509
510
			// don't isolate, scripts use the local $vars
511 282
			include $file;
512
513 282
			return ob_get_clean();
514
		}
515
516 14
		return file_get_contents($file);
517
	}
518
519
	/**
520
	 * Returns whether the specified view exists
521
	 *
522
	 * @param string $view     The view name
523
	 * @param string $viewtype If set, forces the viewtype
524
	 * @param bool   $recurse  If false, do not check extensions
525
	 *
526
	 * @return bool
527
	 *
528
	 * @see elgg_view_exists()
529
	 * @access private
530
	 */
531 799
	public function viewExists($view, $viewtype = '', $recurse = true) {
532 799
		$view = self::canonicalizeViewName($view);
533
		
534 799
		if (empty($view) || !is_string($view)) {
535 40
			return false;
536
		}
537
		
538
		// Detect view type
539 798
		if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
540 97
			$viewtype = $this->getViewtype();
541
		}
542
543
		
544 798
		$file = $this->findViewFile($view, $viewtype);
545 798
		if ($file) {
546 790
			return true;
547
		}
548
549
		// If we got here then check whether this exists as an extension
550
		// We optionally recursively check whether the extended view exists also for the viewtype
551 76
		if ($recurse && isset($this->extensions[$view])) {
552
			foreach ($this->extensions[$view] as $view_extension) {
553
				// do not recursively check to stay away from infinite loops
554
				if ($this->viewExists($view_extension, $viewtype, false)) {
555
					return true;
556
				}
557
			}
558
		}
559
560
		// Now check if the default view exists if the view is registered as a fallback
561 76
		if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
562 1
			return $this->viewExists($view, 'default');
563
		}
564
565 75
		return false;
566
567
	}
568
569
	/**
570
	 * Extends a view with another view
571
	 *
572
	 * @param string $view           The view to extend.
573
	 * @param string $view_extension This view is added to $view
574
	 * @param int    $priority       The priority, from 0 to 1000, to add at (lowest numbers displayed first)
575
	 *
576
	 * @return void
577
	 *
578
	 * @see elgg_extend_view()
579
	 * @access private
580
	 */
581 39
	public function extendView($view, $view_extension, $priority = 501) {
582 39
		$view = self::canonicalizeViewName($view);
583 39
		$view_extension = self::canonicalizeViewName($view_extension);
584
		
585 39
		if ($view === $view_extension) {
586
			// do not allow direct extension on self with self
587 1
			return;
588
		}
589
590 38
		if (!isset($this->extensions[$view])) {
591 25
			$this->extensions[$view][self::BASE_VIEW_PRIORITY] = (string) $view;
592
		}
593
594
		// raise priority until it doesn't match one already registered
595 38
		while (isset($this->extensions[$view][$priority])) {
596 31
			$priority++;
597
		}
598
599 38
		$this->extensions[$view][$priority] = (string) $view_extension;
600 38
		ksort($this->extensions[$view]);
601 38
	}
602
603
	/**
604
	 * Is the given view extended?
605
	 *
606
	 * @param string $view View name
607
	 *
608
	 * @return bool
609
	 * @internal Plugins should not use this
610
	 * @access private
611
	 */
612 433
	public function viewIsExtended($view) {
613 433
		return count($this->getViewList($view)) > 1;
614
	}
615
616
	/**
617
	 * Do hook handlers exist to modify the view?
618
	 *
619
	 * @param string $view View name
620
	 *
621
	 * @return bool
622
	 * @internal Plugins should not use this
623
	 * @access private
624
	 */
625
	public function viewHasHookHandlers($view) {
626
		return $this->hooks->hasHandler('view', $view) || $this->hooks->hasHandler('view_vars', $view);
627
	}
628
629
	/**
630
	 * Unextends a view.
631
	 *
632
	 * @param string $view           The view that was extended.
633
	 * @param string $view_extension This view that was added to $view
634
	 *
635
	 * @return bool
636
	 *
637
	 * @see elgg_unextend_view()
638
	 * @access private
639
	 */
640 33
	public function unextendView($view, $view_extension) {
641 33
		$view = self::canonicalizeViewName($view);
642 33
		$view_extension = self::canonicalizeViewName($view_extension);
643
644 33
		if (!isset($this->extensions[$view])) {
645
			return false;
646
		}
647
		
648 33
		$extensions = $this->extensions[$view];
649 33
		unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
650
651 33
		$priority = array_search($view_extension, $extensions);
652 33
		if ($priority === false) {
653 2
			return false;
654
		}
655
656 32
		unset($this->extensions[$view][$priority]);
657
658 32
		return true;
659
	}
660
661
	/**
662
	 * Register a view a cacheable
663
	 *
664
	 * @param string $view the view name
665
	 *
666
	 * @return void
667
	 *
668
	 * @access private
669
	 */
670 38
	public function registerCacheableView($view) {
671 38
		$view = self::canonicalizeViewName($view);
672
673 38
		$this->simplecache_views[$view] = true;
674 38
	}
675
676
	/**
677
	 * Is the view cacheable
678
	 *
679
	 * @param string $view the view name
680
	 *
681
	 * @return bool
682
	 *
683
	 * @access private
684
	 */
685 636
	public function isCacheableView($view) {
686 636
		$view = self::canonicalizeViewName($view);
687 636
		if (isset($this->simplecache_views[$view])) {
688 132
			return true;
689
		}
690
691
		// build list of viewtypes to check
692 636
		$current_viewtype = $this->getViewtype();
693 636
		$viewtypes = [$current_viewtype];
694
695 636
		if ($this->doesViewtypeFallback($current_viewtype) && $current_viewtype != 'default') {
696
			$viewtypes[] = 'default';
697
		}
698
699
		// If a static view file is found in any viewtype, it's considered cacheable
700 636
		foreach ($viewtypes as $viewtype) {
701 636
			$file = $this->findViewFile($view, $viewtype);
702
703 636
			if ($file && pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
704 553
				$this->simplecache_views[$view] = true;
705 636
				return true;
706
			}
707
		}
708
709
		// Assume not-cacheable by default
710 636
		return false;
711
	}
712
713
	/**
714
	 * Register a plugin's views
715
	 *
716
	 * @param string $path       Base path of the plugin
717
	 * @param string $failed_dir This var is set to the failed directory if registration fails
718
	 * @return bool
719
	 *
720
	 * @access private
721
	 */
722 527
	public function registerPluginViews($path, &$failed_dir = '') {
723 527
		$path = rtrim($path, "\\/");
724 527
		$view_dir = "$path/views/";
725
726
		// plugins don't have to have views.
727 527
		if (!is_dir($view_dir)) {
728
			return true;
729
		}
730
731
		// but if they do, they have to be readable
732 527
		$handle = opendir($view_dir);
733 527
		if (!$handle) {
734
			$failed_dir = $view_dir;
735
			return false;
736
		}
737
738 527
		while (false !== ($view_type = readdir($handle))) {
739 527
			$view_type_dir = $view_dir . $view_type;
740
741 527
			if ('.' !== substr($view_type, 0, 1) && is_dir($view_type_dir)) {
742 527
				if (!$this->autoregisterViews('', $view_type_dir, $view_type)) {
743
					$failed_dir = $view_type_dir;
744
					return false;
745
				}
746
			}
747
		}
748
749 527
		return true;
750
	}
751
752
	/**
753
	 * Merge a specification of absolute view paths
754
	 *
755
	 * @param array $spec Specification
756
	 *    viewtype => [
757
	 *        view_name => path or array of paths
758
	 *    ]
759
	 *
760
	 * @return void
761
	 *
762
	 * @access private
763
	 */
764 11
	public function mergeViewsSpec(array $spec) {
765 11
		foreach ($spec as $viewtype => $list) {
766 11
			foreach ($list as $view => $paths) {
767 11
				if (!is_array($paths)) {
768 11
					$paths = [$paths];
769
				}
770
771 11
				foreach ($paths as $path) {
772 11
					if (preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
773
						// absolute path
774
					} else {
775
						// relative path
776 11
						$path = Directory\Local::projectRoot()->getPath($path);
777
					}
778
779 11
					if (substr($view, -1) === '/') {
780
						// prefix
781 10
						$this->autoregisterViews($view, $path, $viewtype);
782
					} else {
783 11
						$this->setViewLocation($view, $viewtype, $path);
784
					}
785
				}
786
			}
787
		}
788 11
	}
789
790
	/**
791
	 * List all views in a viewtype
792
	 *
793
	 * @param string $viewtype Viewtype
794
	 *
795
	 * @return string[]
796
	 *
797
	 * @access private
798
	 */
799 1
	public function listViews($viewtype = 'default') {
800 1
		if (empty($this->locations[$viewtype])) {
801 1
			return [];
802
		}
803 1
		return array_keys($this->locations[$viewtype]);
804
	}
805
806
	/**
807
	 * Get inspector data
808
	 *
809
	 * @return array
810
	 *
811
	 * @access private
812
	 */
813 1
	public function getInspectorData() {
814 1
		$overrides = $this->overrides;
815
816 1
		if ($this->cache) {
817 1
			$data = $this->cache->load('view_overrides');
818 1
			if ($data) {
819 1
				$overrides = unserialize($data);
820
			}
821
		}
822
823
		return [
824 1
			'locations' => $this->locations,
825 1
			'overrides' => $overrides,
826 1
			'extensions' => $this->extensions,
827 1
			'simplecache' => $this->simplecache_views,
828
		];
829
	}
830
831
	/**
832
	 * Configure locations from the cache
833
	 *
834
	 * @param SystemCache $cache The system cache
835
	 * @return bool
836
	 * @access private
837
	 */
838 13
	public function configureFromCache(SystemCache $cache) {
839 13
		$data = $cache->load('view_locations');
840 13
		if (!is_string($data)) {
841 2
			return false;
842
		}
843
		// format changed, check version
844 12
		$data = unserialize($data);
845 12
		if (empty($data['version']) || $data['version'] !== '2.0') {
846
			return false;
847
		}
848 12
		$this->locations = $data['locations'];
849 12
		$this->cache = $cache;
850
851 12
		return true;
852
	}
853
854
	/**
855
	 * Cache the configuration
856
	 *
857
	 * @param SystemCache $cache The system cache
858
	 * @return void
859
	 * @access private
860
	 */
861 3
	public function cacheConfiguration(SystemCache $cache) {
862 3
		$cache->save('view_locations', serialize([
863 3
			'version' => '2.0',
864 3
			'locations' => $this->locations,
865
		]));
866
867
		// this is saved just for the inspector and is not loaded in loadAll()
868 3
		$cache->save('view_overrides', serialize($this->overrides));
869 3
	}
870
871
	/**
872
	 * Update the location of a view file
873
	 *
874
	 * @param string $view     View name
875
	 * @param string $viewtype Viewtype
876
	 * @param string $path     File path
877
	 *
878
	 * @return void
879
	 */
880 636
	private function setViewLocation($view, $viewtype, $path) {
881 636
		$view = self::canonicalizeViewName($view);
882 636
		$path = strtr($path, '\\', '/');
883
884 636
		if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
885 2
			$this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
886
		}
887 636
		$this->locations[$viewtype][$view] = $path;
888
889
		// Test if view is cacheable and push it to the cacheable views stack,
890
		// if it's not registered as cacheable explicitly
891 636
		$this->isCacheableView($view);
892 636
	}
893
}
894