Elgg  Version 4.x
ViewsService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
11 
18 class ViewsService {
19 
20  use Loggable;
21 
22  const VIEW_HOOK = 'view';
23  const VIEW_VARS_HOOK = 'view_vars';
24  const OUTPUT_KEY = '__view_output';
25  const BASE_VIEW_PRIORITY = 500;
26 
31  protected $file_exists_cache = [];
32 
38  private $locations = [];
39 
45  private $overrides = [];
46 
52  private $simplecache_views = [];
53 
59  private $extensions = [];
60 
64  private $fallbacks = [];
65 
69  private $hooks;
70 
74  private $cache;
75 
79  private $request;
80 
84  private $viewtype;
85 
93  $this->hooks = $hooks;
94  $this->request = $request;
95  }
96 
104  public function setViewtype($viewtype = '') {
105  if (!$viewtype) {
106  $this->viewtype = null;
107 
108  return true;
109  }
110  if ($this->isValidViewtype($viewtype)) {
111  $this->viewtype = $viewtype;
112 
113  return true;
114  }
115 
116  return false;
117  }
118 
124  public function getViewtype() {
125  if ($this->viewtype === null) {
126  $this->viewtype = $this->resolveViewtype();
127  }
128 
129  return $this->viewtype;
130  }
131 
137  public function clampViewtypeToPopulatedViews() {
138  $viewtype = $this->getViewtype();
139  if (empty($this->locations[$viewtype])) {
140  $this->viewtype = 'default';
141  }
142  }
143 
149  private function resolveViewtype() {
150  if ($this->request) {
151  $view = $this->request->getParam('view', '', false);
152  if ($this->isValidViewtype($view)) {
153  return $view;
154  }
155  }
156 
157  $view = elgg_get_config('view');
158  if ($this->isValidViewtype($view)) {
159  return $view;
160  }
161 
162  return 'default';
163  }
164 
172  public function isValidViewtype($viewtype) {
173  if (!is_string($viewtype) || $viewtype === '') {
174  return false;
175  }
176 
177  if (preg_match('/\W/', $viewtype)) {
178  return false;
179  }
180 
181  return true;
182  }
183 
191  public static function canonicalizeViewName($alias) {
192  if (!is_string($alias)) {
193  return false;
194  }
195 
196  $canonical = $alias;
197 
198  $extension = pathinfo($canonical, PATHINFO_EXTENSION);
199  $hasValidFileExtension = isset(CacheHandler::$extensions[$extension]);
200 
201  if (strpos($canonical, "js/") === 0) {
202  $canonical = substr($canonical, 3);
203  if (!$hasValidFileExtension) {
204  $canonical .= ".js";
205  }
206  } else if (strpos($canonical, "css/") === 0) {
207  $canonical = substr($canonical, 4);
208  if (!$hasValidFileExtension) {
209  $canonical .= ".css";
210  }
211  }
212 
213  return $canonical;
214  }
215 
225  public function autoregisterViews($view_base, $folder, $viewtype) {
226  $folder = rtrim($folder, '/\\');
227  $view_base = rtrim($view_base, '/\\');
228 
229  if (!is_dir($folder) || !is_readable($folder)) {
230  $this->getLogger()->notice("Unable to register views from the directory: {$folder}");
231  return false;
232  }
233 
234  try {
235  $dir = new \DirectoryIterator($folder);
236  } catch (\Exception $e) {
237  $this->getLogger()->error($e->getMessage());
238  return false;
239  }
240 
241  $view_base_new = '';
242  if (!empty($view_base)) {
243  $view_base_new = $view_base . '/';
244  }
245 
246  /* @var $fileinfo \SplFileInfo */
247  foreach ($dir as $fileinfo) {
248  if ($fileinfo->isDot()) {
249  continue;
250  }
251 
252  $path = $fileinfo->getPathname();
253 
254  if ($fileinfo->isDir()) {
255  // Found a directory so go deeper
256  $this->autoregisterViews($view_base_new . $fileinfo->getFilename(), $path, $viewtype);
257  continue;
258  }
259 
260  // found a file add it to the views
261  $view = $view_base_new . $fileinfo->getBasename('.php');
262  $this->setViewLocation($view, $viewtype, $path);
263  }
264 
265  return true;
266  }
267 
276  public function findViewFile($view, $viewtype) {
277  if (!isset($this->locations[$viewtype][$view])) {
278  return "";
279  }
280 
281  $path = $this->locations[$viewtype][$view];
282  if ($this->fileExists($path)) {
283  return $path;
284  }
285 
286  return "";
287  }
288 
300  public function setViewDir($view, $location, $viewtype = '') {
301  $view = self::canonicalizeViewName($view);
302 
303  if (empty($viewtype)) {
304  $viewtype = 'default';
305  }
306 
307  $location = rtrim($location, '/\\');
308 
309  if ($this->fileExists("$location/$viewtype/$view.php")) {
310  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view.php");
311  } else if ($this->fileExists("$location/$viewtype/$view")) {
312  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view");
313  }
314  }
315 
327  $this->fallbacks[] = $viewtype;
328  }
329 
337  public function doesViewtypeFallback($viewtype) {
338  return in_array($viewtype, $this->fallbacks);
339  }
340 
353  public function renderDeprecatedView($view, array $vars, $suggestion, $version) {
354  $view = self::canonicalizeViewName($view);
355 
356  $rendered = $this->renderView($view, $vars, '', false);
357  if ($rendered) {
358  $this->logDeprecatedMessage("The '{$view}' view has been deprecated. {$suggestion}", $version);
359  }
360 
361  return $rendered;
362  }
363 
373  public function getViewList($view) {
374  if (isset($this->extensions[$view])) {
375  return $this->extensions[$view];
376  } else {
377  return [self::BASE_VIEW_PRIORITY => $view];
378  }
379  }
380 
394  public function renderView($view, array $vars = [], $viewtype = '', $issue_missing_notice = true, array $extensions_tree = []) {
395  $view = self::canonicalizeViewName($view);
396 
397  if (!is_string($view) || !is_string($viewtype)) {
398  $this->getLogger()->notice("View and Viewtype in views must be a strings: $view");
399 
400  return '';
401  }
402  // basic checking for bad paths
403  if (strpos($view, '..') !== false) {
404  return '';
405  }
406 
407  // check for extension deadloops
408  if (in_array($view, $extensions_tree)) {
409  $this->getLogger()->error("View $view is detected as an extension of itself. This is not allowed");
410 
411  return '';
412  }
413  $extensions_tree[] = $view;
414 
415  // Get the current viewtype
416  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
417  $viewtype = $this->getViewtype();
418  }
419 
420  // allow altering $vars
421  $vars_hook_params = [
422  'view' => $view,
423  'vars' => $vars,
424  'viewtype' => $viewtype,
425  ];
426  $vars = $this->hooks->trigger(self::VIEW_VARS_HOOK, $view, $vars_hook_params, $vars);
427 
428  // allow $vars to hijack output
429  if (isset($vars[self::OUTPUT_KEY])) {
430  return (string) $vars[self::OUTPUT_KEY];
431  }
432 
433  $viewlist = $this->getViewList($view);
434 
435  $content = '';
436  foreach ($viewlist as $priority => $view_name) {
437  if ($priority !== self::BASE_VIEW_PRIORITY) {
438  // the others are extensions
439  $content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
440  continue;
441  }
442 
443  // actual rendering of a single view
444  $rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
445  if ($rendering !== false) {
446  $content .= $rendering;
447  continue;
448  }
449 
450  // attempt to load default view
451  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
452  $rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
453  if ($rendering !== false) {
454  $content .= $rendering;
455  }
456  }
457  }
458 
459  // Plugin hook
460  $params = [
461  'view' => $view,
462  'vars' => $vars,
463  'viewtype' => $viewtype,
464  ];
465  $content = $this->hooks->trigger(self::VIEW_HOOK, $view, $params, $content);
466 
467  return $content;
468  }
469 
478  protected function fileExists($path) {
479  if (!isset($this->file_exists_cache[$path])) {
480  $this->file_exists_cache[$path] = file_exists($path);
481  }
482 
483  return $this->file_exists_cache[$path];
484  }
485 
496  private function renderViewFile($view, array $vars, $viewtype, $issue_missing_notice) {
497  $file = $this->findViewFile($view, $viewtype);
498  if (!$file) {
499  if ($issue_missing_notice) {
500  $this->getLogger()->notice("$viewtype/$view view does not exist.");
501  }
502 
503  return false;
504  }
505 
506  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
507  ob_start();
508 
509  try {
510  // don't isolate, scripts use the local $vars
511  include $file;
512 
513  return ob_get_clean();
514  } catch (\Exception $e) {
515  ob_get_clean();
516  throw $e;
517  }
518  }
519 
520  return file_get_contents($file);
521  }
522 
534  public function viewExists($view, $viewtype = '', $recurse = true) {
535  $view = self::canonicalizeViewName($view);
536 
537  if (empty($view) || !is_string($view)) {
538  return false;
539  }
540 
541  // Detect view type
542  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
543  $viewtype = $this->getViewtype();
544  }
545 
546 
547  $file = $this->findViewFile($view, $viewtype);
548  if ($file) {
549  return true;
550  }
551 
552  // If we got here then check whether this exists as an extension
553  // We optionally recursively check whether the extended view exists also for the viewtype
554  if ($recurse && isset($this->extensions[$view])) {
555  foreach ($this->extensions[$view] as $view_extension) {
556  // do not recursively check to stay away from infinite loops
557  if ($this->viewExists($view_extension, $viewtype, false)) {
558  return true;
559  }
560  }
561  }
562 
563  // Now check if the default view exists if the view is registered as a fallback
564  if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
565  return $this->viewExists($view, 'default');
566  }
567 
568  return false;
569 
570  }
571 
583  public function extendView($view, $view_extension, $priority = 501) {
584  $view = self::canonicalizeViewName($view);
585  $view_extension = self::canonicalizeViewName($view_extension);
586 
587  if ($view === $view_extension) {
588  // do not allow direct extension on self with self
589  return;
590  }
591 
592  if (!isset($this->extensions[$view])) {
593  $this->extensions[$view][self::BASE_VIEW_PRIORITY] = (string) $view;
594  }
595 
596  // raise priority until it doesn't match one already registered
597  while (isset($this->extensions[$view][$priority])) {
598  $priority++;
599  }
600 
601  $this->extensions[$view][$priority] = (string) $view_extension;
602  ksort($this->extensions[$view]);
603  }
604 
612  public function viewIsExtended($view) {
613  return count($this->getViewList($view)) > 1;
614  }
615 
623  public function viewHasHookHandlers($view) {
624  return $this->hooks->hasHandler('view', $view) || $this->hooks->hasHandler('view_vars', $view);
625  }
626 
637  public function unextendView($view, $view_extension) {
638  $view = self::canonicalizeViewName($view);
639  $view_extension = self::canonicalizeViewName($view_extension);
640 
641  if (!isset($this->extensions[$view])) {
642  return false;
643  }
644 
645  $extensions = $this->extensions[$view];
646  unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
647 
648  $priority = array_search($view_extension, $extensions);
649  if ($priority === false) {
650  return false;
651  }
652 
653  unset($this->extensions[$view][$priority]);
654 
655  return true;
656  }
657 
665  public function registerCacheableView($view) {
666  $view = self::canonicalizeViewName($view);
667 
668  $this->simplecache_views[$view] = true;
669  }
670 
678  public function isCacheableView($view) {
679  $view = self::canonicalizeViewName($view);
680  if (isset($this->simplecache_views[$view])) {
681  return true;
682  }
683 
684  // build list of viewtypes to check
685  $current_viewtype = $this->getViewtype();
686  $viewtypes = [$current_viewtype];
687 
688  if ($this->doesViewtypeFallback($current_viewtype) && $current_viewtype != 'default') {
689  $viewtypes[] = 'default';
690  }
691 
692  // If a static view file is found in any viewtype, it's considered cacheable
693  foreach ($viewtypes as $viewtype) {
694  $file = $this->findViewFile($view, $viewtype);
695 
696  if ($file && pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
697  $this->simplecache_views[$view] = true;
698 
699  return true;
700  }
701  }
702 
703  // Assume not-cacheable by default
704  return false;
705  }
706 
714  public function registerPluginViews($path) {
715  $path = Paths::sanitize($path);
716  $view_dir = "{$path}views/";
717 
718  // plugins don't have to have views.
719  if (!is_dir($view_dir)) {
720  return true;
721  }
722 
723  // but if they do, they have to be readable
724  $handle = opendir($view_dir);
725  if ($handle === false) {
726  $this->getLogger()->notice("Unable to register views from the directory: {$view_dir}");
727 
728  return false;
729  }
730 
731  while (false !== ($view_type = readdir($handle))) {
732  $view_type_dir = $view_dir . $view_type;
733 
734  if ('.' !== substr($view_type, 0, 1) && is_dir($view_type_dir)) {
735  if (!$this->autoregisterViews('', $view_type_dir, $view_type)) {
736  return false;
737  }
738  }
739  }
740 
741  return true;
742  }
743 
754  public function mergeViewsSpec(array $spec) {
755  foreach ($spec as $viewtype => $list) {
756  foreach ($list as $view => $paths) {
757  if (!is_array($paths)) {
758  $paths = [$paths];
759  }
760 
761  foreach ($paths as $path) {
762  if (preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
763  // absolute path
764  } else {
765  // relative path
766  $path = Directory\Local::projectRoot()->getPath($path);
767  }
768 
769  if (substr($view, -1) === '/') {
770  // prefix
771  $this->autoregisterViews($view, $path, $viewtype);
772  } else {
773  $this->setViewLocation($view, $viewtype, $path);
774  }
775  }
776  }
777  }
778  }
779 
787  public function listViews($viewtype = 'default') {
788  if (empty($this->locations[$viewtype])) {
789  return [];
790  }
791 
792  return array_keys($this->locations[$viewtype]);
793  }
794 
800  public function getInspectorData() {
801  $overrides = $this->overrides;
802 
803  if ($this->cache) {
804  $data = $this->cache->load('view_overrides');
805  if (is_array($data)) {
806  $overrides = $data;
807  }
808  }
809 
810  return [
811  'locations' => $this->locations,
812  'overrides' => $overrides,
813  'extensions' => $this->extensions,
814  'simplecache' => $this->simplecache_views,
815  ];
816  }
817 
825  public function configureFromCache(SystemCache $cache) {
826  $data = $cache->load('view_locations');
827  if (!is_array($data)) {
828  return false;
829  }
830  // format changed, check version
831  if (empty($data['version']) || $data['version'] !== '2.0') {
832  return false;
833  }
834 
835  $this->locations = $data['locations'];
836  $this->cache = $cache;
837 
838  return true;
839  }
840 
848  public function cacheConfiguration(SystemCache $cache) {
849  $cache->save('view_locations', [
850  'version' => '2.0',
851  'locations' => $this->locations,
852  ]);
853 
854  // this is saved just for the inspector and is not loaded in loadAll()
855  $cache->save('view_overrides', $this->overrides);
856  }
857 
867  private function setViewLocation($view, $viewtype, $path) {
868  $view = self::canonicalizeViewName($view);
869  $path = strtr($path, '\\', '/');
870 
871  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
872  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
873  }
874  $this->locations[$viewtype][$view] = $path;
875 
876  // Test if view is cacheable and push it to the cacheable views stack,
877  // if it's not registered as cacheable explicitly
878  $this->isCacheableView($view);
879  }
880 }
setViewtype($viewtype= '')
Set the viewtype.
__construct(PluginHooksService $hooks, HttpRequest $request)
Constructor.
list extensions
Definition: conf.py:31
isCacheableView($view)
Is the view cacheable.
listViews($viewtype= 'default')
List all views in a viewtype.
$params
Saves global plugin settings.
Definition: save.php:13
$paths
We handle here two possible locations of composer-generated autoload file.
Definition: autoloader.php:7
extendView($view, $view_extension, $priority=501)
Extends a view with another view.
registerCacheableView($view)
Register a view a cacheable.
static canonicalizeViewName($alias)
Takes a view name and returns the canonical name for that view.
cacheConfiguration(SystemCache $cache)
Cache the configuration.
$request
Definition: livesearch.php:11
$version
doesViewtypeFallback($viewtype)
Checks if a viewtype falls back to default.
if(elgg_trigger_plugin_hook('usersettings:save', 'user', $hooks_params, true)) foreach($request->validation() ->all() as $item) $data
Definition: save.php:53
registerViewtypeFallback($viewtype)
Register a viewtype to fall back to a default view if a view isn&#39;t found for that viewtype...
viewExists($view, $viewtype= '', $recurse=true)
Returns whether the specified view exists.
$path
Definition: details.php:68
getInspectorData()
Get inspector data.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
getViewtype()
Get the viewtype.
mergeViewsSpec(array $spec)
Merge a specification of absolute view paths.
renderView($view, array $vars=[], $viewtype= '', $issue_missing_notice=true, array $extensions_tree=[])
Renders a view.
if(!empty($avatar)&&!$avatar->isValid()) elseif(empty($avatar)) if(!$owner->saveIconFromUploadedFile('avatar')) if(!elgg_trigger_event('profileiconupdate', $owner->type, $owner)) $view
Definition: upload.php:39
clampViewtypeToPopulatedViews()
If the current viewtype has no views, reset it to "default".
Views service.
$viewtype
Definition: default.php:11
$extensions
getLogger()
Returns logger.
Definition: Loggable.php:37
viewIsExtended($view)
Is the given view extended?
$location
Definition: member.php:29
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
$vars['head']
Definition: html.php:24
unextendView($view, $view_extension)
Unextends a view.
setViewDir($view, $location, $viewtype= '')
Set an alternative base location for a view.
findViewFile($view, $viewtype)
Find the view file.
registerPluginViews($path)
Register a plugin&#39;s views.
getViewList($view)
Get the views, including extensions, used to render a view.
save($type, $data, int $expire_after=null)
Saves a system cache.
Definition: SystemCache.php:52
renderDeprecatedView($view, array $vars, $suggestion, $version)
Display a view with a deprecation notice.
isValidViewtype($viewtype)
Checks if $viewtype is a string suitable for use as a viewtype name.
autoregisterViews($view_base, $folder, $viewtype)
Auto-registers views from a location.
$priority
elgg_get_config($name, $default=null)
Get an Elgg configuration value.
viewHasHookHandlers($view)
Do hook handlers exist to modify the view?
load($type)
Retrieve the contents of a system cache.
Definition: SystemCache.php:67
$extension
Definition: default.php:25
logDeprecatedMessage(string $message, string $version)
Sends a message about deprecated use of a function, view, etc.
Definition: Loggable.php:80