Elgg  Version 2.3
ViewsService.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg;
3 
7 
17 class ViewsService {
18 
19  const VIEW_HOOK = 'view';
20  const VIEW_VARS_HOOK = 'view_vars';
21  const OUTPUT_KEY = '__view_output';
22 
27  protected $file_exists_cache = [];
28 
34  private $locations = [];
35 
41  private $overrides = [];
42 
48  private $simplecache_views = [];
49 
55  private $extensions = [];
56 
60  private $fallbacks = [];
61 
65  private $hooks;
66 
70  private $logger;
71 
75  private $cache;
76 
80  public $view_path;
81 
88  public function __construct(PluginHooksService $hooks, Logger $logger) {
89  $this->hooks = $hooks;
90  $this->logger = $logger;
91  }
92 
100  public function canonicalizeViewName($alias) {
101  if (!is_string($alias)) {
102  return false;
103  }
104 
105  $canonical = $alias;
106 
107  $extension = pathinfo($canonical, PATHINFO_EXTENSION);
108  $hasValidFileExtension = isset(CacheHandler::$extensions[$extension]);
109 
110  if (strpos($canonical, "js/") === 0) {
111  $canonical = substr($canonical, 3);
112  if (!$hasValidFileExtension) {
113  $canonical .= ".js";
114  }
115  } else if (strpos($canonical, "css/") === 0) {
116  $canonical = substr($canonical, 4);
117  if (!$hasValidFileExtension) {
118  $canonical .= ".css";
119  }
120  }
121 
122  return $canonical;
123  }
124 
128  public function autoregisterViews($view_base, $folder, $viewtype) {
129  $folder = rtrim($folder, '/\\');
130  $view_base = rtrim($view_base, '/\\');
131 
132  $handle = opendir($folder);
133  if (!$handle) {
134  return false;
135  }
136 
137  while ($entry = readdir($handle)) {
138  if ($entry[0] === '.') {
139  continue;
140  }
141 
142  $path = "$folder/$entry";
143 
144  if (!empty($view_base)) {
145  $view_base_new = $view_base . "/";
146  } else {
147  $view_base_new = "";
148  }
149 
150  if (is_dir($path)) {
151  $this->autoregisterViews($view_base_new . $entry, $path, $viewtype);
152  } else {
153  $view = $view_base_new . basename($entry, '.php');
154  $this->setViewLocation($view, $viewtype, $path);
155  }
156  }
157 
158  return true;
159  }
160 
171  public function findViewFile($view, $viewtype) {
172  if (!isset($this->locations[$viewtype][$view])) {
173  return "";
174  }
175 
176  $path = $this->locations[$viewtype][$view];
177  if ($this->fileExists($path)) {
178  return $path;
179  }
180 
181  return "";
182  }
183 
191  public function setViewDir($view, $location, $viewtype = '') {
192  $view = $this->canonicalizeViewName($view);
193 
194  if (empty($viewtype)) {
195  $viewtype = 'default';
196  }
197 
198  $location = rtrim($location, '/\\');
199 
200  if ($this->fileExists("$location/$viewtype/$view.php")) {
201  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view.php");
202  } elseif ($this->fileExists("$location/$viewtype/$view")) {
203  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view");
204  }
205  }
206 
210  public function registerViewtypeFallback($viewtype) {
211  $this->fallbacks[] = $viewtype;
212  }
213 
217  public function doesViewtypeFallback($viewtype) {
218  return in_array($viewtype, $this->fallbacks);
219  }
220 
234  public function renderDeprecatedView($view, array $vars, $suggestion, $version) {
235  $view = $this->canonicalizeViewName($view);
236 
237  $rendered = $this->renderView($view, $vars, false, '', false);
238  if ($rendered) {
239  elgg_deprecated_notice("The $view view has been deprecated. $suggestion", $version, 3);
240  }
241  return $rendered;
242  }
243 
253  public function getViewList($view) {
254  if (isset($this->extensions[$view])) {
255  return $this->extensions[$view];
256  } else {
257  return [500 => $view];
258  }
259  }
260 
264  public function renderView($view, array $vars = [], $ignored = false, $viewtype = '', $issue_missing_notice = true) {
265  $view = $this->canonicalizeViewName($view);
266 
267  if (!is_string($view) || !is_string($viewtype)) {
268  $this->logger->log("View and Viewtype in views must be a strings: $view", 'NOTICE');
269  return '';
270  }
271  // basic checking for bad paths
272  if (strpos($view, '..') !== false) {
273  return '';
274  }
275 
276  if (!is_array($vars)) {
277  $this->logger->log("Vars in views must be an array: $view", 'ERROR');
278  $vars = array();
279  }
280 
281  // Get the current viewtype
282  if ($viewtype === '' || !_elgg_is_valid_viewtype($viewtype)) {
283  $viewtype = elgg_get_viewtype();
284  }
285 
286  // allow altering $vars
287  $vars_hook_params = [
288  'view' => $view,
289  'vars' => $vars,
290  'viewtype' => $viewtype,
291  ];
292  $vars = $this->hooks->trigger(self::VIEW_VARS_HOOK, $view, $vars_hook_params, $vars);
293 
294  // allow $vars to hijack output
295  if (isset($vars[self::OUTPUT_KEY])) {
296  return (string)$vars[self::OUTPUT_KEY];
297  }
298 
299  $view_orig = $view;
300 
301  $viewlist = $this->getViewList($view);
302 
303  $content = '';
304  foreach ($viewlist as $view) {
305 
306  $rendering = $this->renderViewFile($view, $vars, $viewtype, $issue_missing_notice);
307  if ($rendering !== false) {
308  $content .= $rendering;
309  continue;
310  }
311 
312  // attempt to load default view
313  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
314 
315  $rendering = $this->renderViewFile($view, $vars, 'default', $issue_missing_notice);
316  if ($rendering !== false) {
317  $content .= $rendering;
318  }
319  }
320  }
321 
322  // Plugin hook
323  $params = [
324  'view' => $view_orig,
325  'vars' => $vars,
326  'viewtype' => $viewtype,
327  ];
328  $content = $this->hooks->trigger(self::VIEW_HOOK, $view_orig, $params, $content);
329 
330  return $content;
331  }
332 
340  protected function fileExists($path) {
341  if (!isset($this->file_exists_cache[$path])) {
342  $this->file_exists_cache[$path] = file_exists($path);
343  }
344  return $this->file_exists_cache[$path];
345  }
346 
357  private function renderViewFile($view, array $vars, $viewtype, $issue_missing_notice) {
358  $file = $this->findViewFile($view, $viewtype);
359  if (!$file) {
360  if ($issue_missing_notice) {
361  $this->logger->log("$viewtype/$view view does not exist.", 'NOTICE');
362  }
363  return false;
364  }
365 
366  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
367  ob_start();
368 
369  // don't isolate, scripts use the local $vars
370  include $file;
371 
372  return ob_get_clean();
373  }
374 
375  return file_get_contents($file);
376  }
377 
381  public function viewExists($view, $viewtype = '', $recurse = true) {
382  $view = $this->canonicalizeViewName($view);
383 
384  if (empty($view) || !is_string($view)) {
385  return false;
386  }
387 
388  // Detect view type
389  if ($viewtype === '' || !_elgg_is_valid_viewtype($viewtype)) {
390  $viewtype = elgg_get_viewtype();
391  }
392 
393 
394  $file = $this->findViewFile($view, $viewtype);
395  if ($file) {
396  return true;
397  }
398 
399  // If we got here then check whether this exists as an extension
400  // We optionally recursively check whether the extended view exists also for the viewtype
401  if ($recurse && isset($this->extensions[$view])) {
402  foreach ($this->extensions[$view] as $view_extension) {
403  // do not recursively check to stay away from infinite loops
404  if ($this->viewExists($view_extension, $viewtype, false)) {
405  return true;
406  }
407  }
408  }
409 
410  // Now check if the default view exists if the view is registered as a fallback
411  if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
412  return $this->viewExists($view, 'default');
413  }
414 
415  return false;
416 
417  }
418 
422  public function extendView($view, $view_extension, $priority = 501) {
423  $view = $this->canonicalizeViewName($view);
424  $view_extension = $this->canonicalizeViewName($view_extension);
425 
426  if (!isset($this->extensions[$view])) {
427  $this->extensions[$view][500] = (string) $view;
428  }
429 
430  // raise priority until it doesn't match one already registered
431  while (isset($this->extensions[$view][$priority])) {
432  $priority++;
433  }
434 
435  $this->extensions[$view][$priority] = (string) $view_extension;
436  ksort($this->extensions[$view]);
437  }
438 
448  public function viewIsExtended($view) {
449  return count($this->getViewList($view)) > 1;
450  }
451 
461  public function viewHasHookHandlers($view) {
462  return $this->hooks->hasHandler('view', $view) || $this->hooks->hasHandler('view_vars', $view);
463  }
464 
468  public function unextendView($view, $view_extension) {
469  $view = $this->canonicalizeViewName($view);
470  $view_extension = $this->canonicalizeViewName($view_extension);
471 
472  if (!isset($this->extensions[$view])) {
473  return false;
474  }
475 
476  $priority = array_search($view_extension, $this->extensions[$view]);
477  if ($priority === false) {
478  return false;
479  }
480 
481  unset($this->extensions[$view][$priority]);
482 
483  return true;
484  }
485 
489  public function registerCacheableView($view) {
490  $view = $this->canonicalizeViewName($view);
491 
492  $this->simplecache_views[$view] = true;
493  }
494 
498  public function isCacheableView($view) {
499  $view = $this->canonicalizeViewName($view);
500  if (isset($this->simplecache_views[$view])) {
501  return true;
502  }
503 
504  // build list of viewtypes to check
505  $current_viewtype = elgg_get_viewtype();
506  $viewtypes = array($current_viewtype);
507 
508  if ($this->doesViewtypeFallback($current_viewtype) && $current_viewtype != 'default') {
509  $viewtypes[] = 'default';
510  }
511 
512  // If a static view file is found in any viewtype, it's considered cacheable
513  foreach ($viewtypes as $viewtype) {
514  $file = $this->findViewFile($view, $viewtype);
515 
516  if ($file && pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
517  $this->simplecache_views[$view] = true;
518  return true;
519  }
520  }
521 
522  // Assume not-cacheable by default
523  return false;
524  }
525 
535  public function registerPluginViews($path, &$failed_dir = '') {
536  $path = rtrim($path, "\\/");
537  $view_dir = "$path/views/";
538 
539  // plugins don't have to have views.
540  if (!is_dir($view_dir)) {
541  return true;
542  }
543 
544  // but if they do, they have to be readable
545  $handle = opendir($view_dir);
546  if (!$handle) {
547  $failed_dir = $view_dir;
548  return false;
549  }
550 
551  while (false !== ($view_type = readdir($handle))) {
552  $view_type_dir = $view_dir . $view_type;
553 
554  if ('.' !== substr($view_type, 0, 1) && is_dir($view_type_dir)) {
555  if ($this->autoregisterViews('', $view_type_dir, $view_type)) {
556  elgg_register_viewtype($view_type);
557  } else {
558  $failed_dir = $view_type_dir;
559  return false;
560  }
561  }
562  }
563 
564  return true;
565  }
566 
577  public function mergeViewsSpec(array $spec) {
578  foreach ($spec as $viewtype => $list) {
579  foreach ($list as $view => $paths) {
580  if (!is_array($paths)) {
581  $paths = [$paths];
582  }
583 
584  foreach ($paths as $path) {
585  if (preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
586  // absolute path
587  } else {
588  // relative path
589  $path = Directory\Local::root()->getPath($path);
590  }
591 
592  if (substr($view, -1) === '/') {
593  // prefix
594  $this->autoregisterViews($view, $path, $viewtype);
595  } else {
596  $this->setViewLocation($view, $viewtype, $path);
597  }
598  }
599  }
600  }
601  }
602 
612  public function listViews($viewtype = 'default') {
613  if (empty($this->locations[$viewtype])) {
614  return [];
615  }
616  return array_keys($this->locations[$viewtype]);
617  }
618 
626  public function getInspectorData() {
627  $overrides = $this->overrides;
628 
629  if ($this->cache) {
630  $data = $this->cache->load('view_overrides');
631  if ($data) {
632  $overrides = unserialize($data);
633  }
634  }
635 
636  return [
637  'locations' => $this->locations,
638  'overrides' => $overrides,
639  'extensions' => $this->extensions,
640  'simplecache' => $this->simplecache_views,
641  ];
642  }
643 
651  public function configureFromCache(SystemCache $cache) {
652  $data = $cache->load('view_locations');
653  if (!is_string($data)) {
654  return false;
655  }
656  // format changed, check version
657  $data = unserialize($data);
658  if (empty($data['version']) || $data['version'] !== '2.0') {
659  return false;
660  }
661  $this->locations = $data['locations'];
662  $this->cache = $cache;
663 
664  return true;
665  }
666 
674  public function cacheConfiguration(SystemCache $cache) {
675  $cache->save('view_locations', serialize([
676  'version' => '2.0',
677  'locations' => $this->locations,
678  ]));
679 
680  // this is saved just for the inspector and is not loaded in loadAll()
681  $cache->save('view_overrides', serialize($this->overrides));
682  }
683 
693  private function setViewLocation($view, $viewtype, $path) {
694  $view = $this->canonicalizeViewName($view);
695  $path = strtr($path, '\\', '/');
696 
697  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
698  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
699  }
700  $this->locations[$viewtype][$view] = $path;
701  }
702 }
$extensions
Definition: summary.php:41
$view
Definition: crop.php:34
if(!array_key_exists($filename, $text_files)) $file
$version
list extensions
Definition: conf.py:31
isCacheableView($view)
private
listViews($viewtype= 'default')
List all views in a viewtype.
$paths
We handle here two possible locations of composer-generated autoload file.
Definition: autoloader.php:7
extendView($view, $view_extension, $priority=501)
private
registerCacheableView($view)
private
cacheConfiguration(SystemCache $cache)
Cache the configuration.
doesViewtypeFallback($viewtype)
private
$data
Definition: opendd.php:13
$path
Definition: details.php:88
_elgg_is_valid_viewtype($viewtype)
Checks if $viewtype is a string suitable for use as a viewtype name.
Definition: views.php:171
registerViewtypeFallback($viewtype)
private
viewExists($view, $viewtype= '', $recurse=true)
private
$vars['entity']
elgg_get_viewtype()
Return the current view type.
Definition: views.php:95
$params
Definition: login.php:72
getInspectorData()
Get inspector data.
mergeViewsSpec(array $spec)
Merge a specification of absolute view paths.
Save menu items.
registerPluginViews($path, &$failed_dir= '')
Register a plugin&#39;s views.
WARNING: API IN FLUX.
elgg_deprecated_notice($msg, $dep_version, $backtrace_level=1)
Log a notice about deprecated use of a function, view, etc.
Definition: elgglib.php:1098
renderView($view, array $vars=[], $ignored=false, $viewtype= '', $issue_missing_notice=true)
private
save($type, $data)
Saves a system cache.
Definition: SystemCache.php:65
viewIsExtended($view)
Is the given view extended?
fileExists($path)
Wrapper for file_exists() that caches false results (the stat cache only caches true results)...
configureFromCache(SystemCache $cache)
Configure locations from the cache.
$content
Set robots.txt action.
Definition: set_robots.php:6
canonicalizeViewName($alias)
Takes a view name and returns the canonical name for that view.
elgg_register_viewtype($viewtype)
Register a viewtype.
Definition: views.php:133
unextendView($view, $view_extension)
private
setViewDir($view, $location, $viewtype= '')
findViewFile($view, $viewtype)
Find the view file.
__construct(PluginHooksService $hooks, Logger $logger)
Constructor.
getViewList($view)
Get the views, including extensions, used to render a view.
renderDeprecatedView($view, array $vars, $suggestion, $version)
Display a view with a deprecation notice.
http free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:5
autoregisterViews($view_base, $folder, $viewtype)
private
$priority
viewHasHookHandlers($view)
Do hook handlers exist to modify the view?
load($type)
Retrieve the contents of a system cache.
Definition: SystemCache.php:79
$extension
Definition: default.php:23