Elgg  Version 2.2
 All Classes Namespaces Files Functions Variables Pages
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 
83  public function __construct(PluginHooksService $hooks, Logger $logger) {
84  $this->hooks = $hooks;
85  $this->logger = $logger;
86  }
87 
95  public function canonicalizeViewName($alias) {
96  if (!is_string($alias)) {
97  return false;
98  }
99 
100  $canonical = $alias;
101 
102  $extension = pathinfo($canonical, PATHINFO_EXTENSION);
103  $hasValidFileExtension = isset(CacheHandler::$extensions[$extension]);
104 
105  if (strpos($canonical, "js/") === 0) {
106  $canonical = substr($canonical, 3);
107  if (!$hasValidFileExtension) {
108  $canonical .= ".js";
109  }
110  } else if (strpos($canonical, "css/") === 0) {
111  $canonical = substr($canonical, 4);
112  if (!$hasValidFileExtension) {
113  $canonical .= ".css";
114  }
115  }
116 
117  return $canonical;
118  }
119 
123  public function autoregisterViews($view_base, $folder, $viewtype) {
124  $folder = rtrim($folder, '/\\');
125  $view_base = rtrim($view_base, '/\\');
126 
127  $handle = opendir($folder);
128  if (!$handle) {
129  return false;
130  }
131 
132  while ($entry = readdir($handle)) {
133  if ($entry[0] === '.') {
134  continue;
135  }
136 
137  $path = "$folder/$entry";
138 
139  if (!empty($view_base)) {
140  $view_base_new = $view_base . "/";
141  } else {
142  $view_base_new = "";
143  }
144 
145  if (is_dir($path)) {
146  $this->autoregisterViews($view_base_new . $entry, $path, $viewtype);
147  } else {
148  $view = $view_base_new . basename($entry, '.php');
149  $this->setViewLocation($view, $viewtype, $path);
150  }
151  }
152 
153  return true;
154  }
155 
166  public function findViewFile($view, $viewtype) {
167  if (!isset($this->locations[$viewtype][$view])) {
168  return "";
169  }
170 
171  $path = $this->locations[$viewtype][$view];
172  if ($this->fileExists($path)) {
173  return $path;
174  }
175 
176  return "";
177  }
178 
186  public function setViewDir($view, $location, $viewtype = '') {
187  $view = $this->canonicalizeViewName($view);
188 
189  if (empty($viewtype)) {
190  $viewtype = 'default';
191  }
192 
193  $location = rtrim($location, '/\\');
194 
195  if ($this->fileExists("$location/$viewtype/$view.php")) {
196  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view.php");
197  } elseif ($this->fileExists("$location/$viewtype/$view")) {
198  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view");
199  }
200  }
201 
205  public function registerViewtypeFallback($viewtype) {
206  $this->fallbacks[] = $viewtype;
207  }
208 
212  public function doesViewtypeFallback($viewtype) {
213  return in_array($viewtype, $this->fallbacks);
214  }
215 
229  public function renderDeprecatedView($view, array $vars, $suggestion, $version) {
230  $view = $this->canonicalizeViewName($view);
231 
232  $rendered = $this->renderView($view, $vars, false, '', false);
233  if ($rendered) {
234  elgg_deprecated_notice("The $view view has been deprecated. $suggestion", $version, 3);
235  }
236  return $rendered;
237  }
238 
247  private function getViewList($view) {
248  if (isset($this->extensions[$view])) {
249  return $this->extensions[$view];
250  } else {
251  return [500 => $view];
252  }
253  }
254 
258  public function renderView($view, array $vars = [], $ignored = false, $viewtype = '', $issue_missing_notice = true) {
259  $view = $this->canonicalizeViewName($view);
260 
261  if (!is_string($view) || !is_string($viewtype)) {
262  $this->logger->log("View and Viewtype in views must be a strings: $view", 'NOTICE');
263  return '';
264  }
265  // basic checking for bad paths
266  if (strpos($view, '..') !== false) {
267  return '';
268  }
269 
270  if (!is_array($vars)) {
271  $this->logger->log("Vars in views must be an array: $view", 'ERROR');
272  $vars = array();
273  }
274 
275  // Get the current viewtype
276  if ($viewtype === '' || !_elgg_is_valid_viewtype($viewtype)) {
277  $viewtype = elgg_get_viewtype();
278  }
279 
280  // allow altering $vars
281  $vars_hook_params = [
282  'view' => $view,
283  'vars' => $vars,
284  'viewtype' => $viewtype,
285  ];
286  $vars = $this->hooks->trigger(self::VIEW_VARS_HOOK, $view, $vars_hook_params, $vars);
287 
288  // allow $vars to hijack output
289  if (isset($vars[self::OUTPUT_KEY])) {
290  return (string)$vars[self::OUTPUT_KEY];
291  }
292 
293  $view_orig = $view;
294 
295  $viewlist = $this->getViewList($view);
296 
297  $content = '';
298  foreach ($viewlist as $view) {
299 
300  $rendering = $this->renderViewFile($view, $vars, $viewtype, $issue_missing_notice);
301  if ($rendering !== false) {
302  $content .= $rendering;
303  continue;
304  }
305 
306  // attempt to load default view
307  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
308 
309  $rendering = $this->renderViewFile($view, $vars, 'default', $issue_missing_notice);
310  if ($rendering !== false) {
311  $content .= $rendering;
312  }
313  }
314  }
315 
316  // Plugin hook
317  $params = [
318  'view' => $view_orig,
319  'vars' => $vars,
320  'viewtype' => $viewtype,
321  ];
322  $content = $this->hooks->trigger(self::VIEW_HOOK, $view_orig, $params, $content);
323 
324  return $content;
325  }
326 
334  protected function fileExists($path) {
335  if (!isset($this->file_exists_cache[$path])) {
336  $this->file_exists_cache[$path] = file_exists($path);
337  }
338  return $this->file_exists_cache[$path];
339  }
340 
351  private function renderViewFile($view, array $vars, $viewtype, $issue_missing_notice) {
352  $file = $this->findViewFile($view, $viewtype);
353  if (!$file) {
354  if ($issue_missing_notice) {
355  $this->logger->log("$viewtype/$view view does not exist.", 'NOTICE');
356  }
357  return false;
358  }
359 
360  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
361  ob_start();
362  include $file;
363  return ob_get_clean();
364  }
365 
366  return file_get_contents($file);
367  }
368 
372  public function viewExists($view, $viewtype = '', $recurse = true) {
373  $view = $this->canonicalizeViewName($view);
374 
375  if (empty($view) || !is_string($view)) {
376  return false;
377  }
378 
379  // Detect view type
380  if ($viewtype === '' || !_elgg_is_valid_viewtype($viewtype)) {
381  $viewtype = elgg_get_viewtype();
382  }
383 
384 
385  $file = $this->findViewFile($view, $viewtype);
386  if ($file) {
387  return true;
388  }
389 
390  // If we got here then check whether this exists as an extension
391  // We optionally recursively check whether the extended view exists also for the viewtype
392  if ($recurse && isset($this->extensions[$view])) {
393  foreach ($this->extensions[$view] as $view_extension) {
394  // do not recursively check to stay away from infinite loops
395  if ($this->viewExists($view_extension, $viewtype, false)) {
396  return true;
397  }
398  }
399  }
400 
401  // Now check if the default view exists if the view is registered as a fallback
402  if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
403  return $this->viewExists($view, 'default');
404  }
405 
406  return false;
407 
408  }
409 
413  public function extendView($view, $view_extension, $priority = 501) {
414  $view = $this->canonicalizeViewName($view);
415  $view_extension = $this->canonicalizeViewName($view_extension);
416 
417  if (!isset($this->extensions[$view])) {
418  $this->extensions[$view][500] = (string) $view;
419  }
420 
421  // raise priority until it doesn't match one already registered
422  while (isset($this->extensions[$view][$priority])) {
423  $priority++;
424  }
425 
426  $this->extensions[$view][$priority] = (string) $view_extension;
427  ksort($this->extensions[$view]);
428  }
429 
439  public function viewIsExtended($view) {
440  return count($this->getViewList($view)) > 1;
441  }
442 
452  public function viewHasHookHandlers($view) {
453  return $this->hooks->hasHandler('view', $view) || $this->hooks->hasHandler('view_vars', $view);
454  }
455 
459  public function unextendView($view, $view_extension) {
460  $view = $this->canonicalizeViewName($view);
461  $view_extension = $this->canonicalizeViewName($view_extension);
462 
463  if (!isset($this->extensions[$view])) {
464  return false;
465  }
466 
467  $priority = array_search($view_extension, $this->extensions[$view]);
468  if ($priority === false) {
469  return false;
470  }
471 
472  unset($this->extensions[$view][$priority]);
473 
474  return true;
475  }
476 
480  public function registerCacheableView($view) {
481  $view = $this->canonicalizeViewName($view);
482 
483  $this->simplecache_views[$view] = true;
484  }
485 
489  public function isCacheableView($view) {
490  $view = $this->canonicalizeViewName($view);
491  if (isset($this->simplecache_views[$view])) {
492  return true;
493  }
494 
495  // build list of viewtypes to check
496  $current_viewtype = elgg_get_viewtype();
497  $viewtypes = array($current_viewtype);
498 
499  if ($this->doesViewtypeFallback($current_viewtype) && $current_viewtype != 'default') {
500  $viewtypes[] = 'default';
501  }
502 
503  // If a static view file is found in any viewtype, it's considered cacheable
504  foreach ($viewtypes as $viewtype) {
505  $file = $this->findViewFile($view, $viewtype);
506 
507  if ($file && pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
508  $this->simplecache_views[$view] = true;
509  return true;
510  }
511  }
512 
513  // Assume not-cacheable by default
514  return false;
515  }
516 
526  public function registerPluginViews($path, &$failed_dir = '') {
527  $path = rtrim($path, "\\/");
528  $view_dir = "$path/views/";
529 
530  // plugins don't have to have views.
531  if (!is_dir($view_dir)) {
532  return true;
533  }
534 
535  // but if they do, they have to be readable
536  $handle = opendir($view_dir);
537  if (!$handle) {
538  $failed_dir = $view_dir;
539  return false;
540  }
541 
542  while (false !== ($view_type = readdir($handle))) {
543  $view_type_dir = $view_dir . $view_type;
544 
545  if ('.' !== substr($view_type, 0, 1) && is_dir($view_type_dir)) {
546  if ($this->autoregisterViews('', $view_type_dir, $view_type)) {
547  elgg_register_viewtype($view_type);
548  } else {
549  $failed_dir = $view_type_dir;
550  return false;
551  }
552  }
553  }
554 
555  return true;
556  }
557 
568  public function mergeViewsSpec(array $spec) {
569  foreach ($spec as $viewtype => $list) {
570  foreach ($list as $view => $paths) {
571  if (!is_array($paths)) {
572  $paths = [$paths];
573  }
574 
575  foreach ($paths as $path) {
576  if (preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
577  // absolute path
578  } else {
579  // relative path
580  $path = Directory\Local::root()->getPath($path);
581  }
582 
583  if (substr($view, -1) === '/') {
584  // prefix
585  $this->autoregisterViews($view, $path, $viewtype);
586  } else {
587  $this->setViewLocation($view, $viewtype, $path);
588  }
589  }
590  }
591  }
592  }
593 
603  public function listViews($viewtype = 'default') {
604  if (empty($this->locations[$viewtype])) {
605  return [];
606  }
607  return array_keys($this->locations[$viewtype]);
608  }
609 
617  public function getInspectorData() {
618  $overrides = $this->overrides;
619 
620  if ($this->cache) {
621  $data = $this->cache->load('view_overrides');
622  if ($data) {
623  $overrides = unserialize($data);
624  }
625  }
626 
627  return [
628  'locations' => $this->locations,
629  'overrides' => $overrides,
630  'extensions' => $this->extensions,
631  'simplecache' => $this->simplecache_views,
632  ];
633  }
634 
642  public function configureFromCache(SystemCache $cache) {
643  $data = $cache->load('view_locations');
644  if (!is_string($data)) {
645  return false;
646  }
647  // format changed, check version
648  $data = unserialize($data);
649  if (empty($data['version']) || $data['version'] !== '2.0') {
650  return false;
651  }
652  $this->locations = $data['locations'];
653  $this->cache = $cache;
654 
655  return true;
656  }
657 
665  public function cacheConfiguration(SystemCache $cache) {
666  $cache->save('view_locations', serialize([
667  'version' => '2.0',
668  'locations' => $this->locations,
669  ]));
670 
671  // this is saved just for the inspector and is not loaded in loadAll()
672  $cache->save('view_overrides', serialize($this->overrides));
673  }
674 
684  private function setViewLocation($view, $viewtype, $path) {
685  $view = $this->canonicalizeViewName($view);
686  $path = strtr($path, '\\', '/');
687 
688  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
689  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
690  }
691  $this->locations[$viewtype][$view] = $path;
692  }
693 }
A simple directory abstraction.
Definition: Directory.php:13
$view
Definition: crop.php:34
if(!array_key_exists($filename, $text_files)) $file
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
$extensions
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:170
registerViewtypeFallback($viewtype)
private
viewExists($view, $viewtype= '', $recurse=true)
private
$vars['entity']
elgg_get_viewtype()
Return the current view type.
Definition: views.php:94
$params
Definition: login.php:72
getInspectorData()
Get inspector data.
mergeViewsSpec(array $spec)
Merge a specification of absolute view paths.
registerPluginViews($path, &$failed_dir= '')
Register a plugin'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:1070
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:132
unextendView($view, $view_extension)
private
setViewDir($view, $location, $viewtype= '')
findViewFile($view, $viewtype)
Find the view file.
__construct(PluginHooksService $hooks, Logger $logger)
Constructor.
renderDeprecatedView($view, array $vars, $suggestion, $version)
Display a view with a deprecation notice.
$version
Definition: version.php:14
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