Elgg  Version 3.0
ViewsService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
10 
19 class ViewsService {
20 
21  use Loggable;
22 
23  const VIEW_HOOK = 'view';
24  const VIEW_VARS_HOOK = 'view_vars';
25  const OUTPUT_KEY = '__view_output';
26  const BASE_VIEW_PRIORITY = 500;
27 
32  protected $file_exists_cache = [];
33 
39  private $locations = [];
40 
46  private $overrides = [];
47 
53  private $simplecache_views = [];
54 
60  private $extensions = [];
61 
65  private $fallbacks = [];
66 
70  private $hooks;
71 
75  private $cache;
76 
80  private $request;
81 
85  private $viewtype;
86 
94  public function __construct(PluginHooksService $hooks, LoggerInterface $logger, HttpRequest $request = null) {
95  $this->hooks = $hooks;
96  $this->logger = $logger;
97  $this->request = $request;
98  }
99 
107  public function setViewtype($viewtype = '') {
108  if (!$viewtype) {
109  $this->viewtype = null;
110 
111  return true;
112  }
113  if ($this->isValidViewtype($viewtype)) {
114  $this->viewtype = $viewtype;
115 
116  return true;
117  }
118 
119  return false;
120  }
121 
127  public function getViewtype() {
128  if ($this->viewtype === null) {
129  $this->viewtype = $this->resolveViewtype();
130  }
131 
132  return $this->viewtype;
133  }
134 
140  public function clampViewtypeToPopulatedViews() {
141  $viewtype = $this->getViewtype();
142  if (empty($this->locations[$viewtype])) {
143  $this->viewtype = 'default';
144  }
145  }
146 
152  private function resolveViewtype() {
153  if ($this->request) {
154  $view = $this->request->getParam('view', '', false);
155  if ($this->isValidViewtype($view)) {
156  return $view;
157  }
158  }
159 
160  $view = elgg_get_config('view');
161  if ($this->isValidViewtype($view)) {
162  return $view;
163  }
164 
165  return 'default';
166  }
167 
175  public function isValidViewtype($viewtype) {
176  if (!is_string($viewtype) || $viewtype === '') {
177  return false;
178  }
179 
180  if (preg_match('/\W/', $viewtype)) {
181  return false;
182  }
183 
184  return true;
185  }
186 
194  public static function canonicalizeViewName($alias) {
195  if (!is_string($alias)) {
196  return false;
197  }
198 
199  $canonical = $alias;
200 
201  $extension = pathinfo($canonical, PATHINFO_EXTENSION);
202  $hasValidFileExtension = isset(CacheHandler::$extensions[$extension]);
203 
204  if (strpos($canonical, "js/") === 0) {
205  $canonical = substr($canonical, 3);
206  if (!$hasValidFileExtension) {
207  $canonical .= ".js";
208  }
209  } else if (strpos($canonical, "css/") === 0) {
210  $canonical = substr($canonical, 4);
211  if (!$hasValidFileExtension) {
212  $canonical .= ".css";
213  }
214  }
215 
216  return $canonical;
217  }
218 
230  public function autoregisterViews($view_base, $folder, $viewtype) {
231  $folder = rtrim($folder, '/\\');
232  $view_base = rtrim($view_base, '/\\');
233 
234  $handle = opendir($folder);
235  if (!$handle) {
236  return false;
237  }
238 
239  while ($entry = readdir($handle)) {
240  if ($entry[0] === '.') {
241  continue;
242  }
243 
244  $path = "$folder/$entry";
245 
246  if (!empty($view_base)) {
247  $view_base_new = $view_base . "/";
248  } else {
249  $view_base_new = "";
250  }
251 
252  if (is_dir($path)) {
253  $this->autoregisterViews($view_base_new . $entry, $path, $viewtype);
254  } else {
255  $view = $view_base_new . basename($entry, '.php');
256  $this->setViewLocation($view, $viewtype, $path);
257  }
258  }
259 
260  return true;
261  }
262 
271  public function findViewFile($view, $viewtype) {
272  if (!isset($this->locations[$viewtype][$view])) {
273  return "";
274  }
275 
276  $path = $this->locations[$viewtype][$view];
277  if ($this->fileExists($path)) {
278  return $path;
279  }
280 
281  return "";
282  }
283 
295  public function setViewDir($view, $location, $viewtype = '') {
296  $view = self::canonicalizeViewName($view);
297 
298  if (empty($viewtype)) {
299  $viewtype = 'default';
300  }
301 
302  $location = rtrim($location, '/\\');
303 
304  if ($this->fileExists("$location/$viewtype/$view.php")) {
305  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view.php");
306  } else if ($this->fileExists("$location/$viewtype/$view")) {
307  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view");
308  }
309  }
310 
322  $this->fallbacks[] = $viewtype;
323  }
324 
334  public function doesViewtypeFallback($viewtype) {
335  return in_array($viewtype, $this->fallbacks);
336  }
337 
350  public function renderDeprecatedView($view, array $vars, $suggestion, $version) {
351  $view = self::canonicalizeViewName($view);
352 
353  $rendered = $this->renderView($view, $vars, '', false);
354  if ($rendered) {
355  elgg_deprecated_notice("The $view view has been deprecated. $suggestion", $version, 3);
356  }
357 
358  return $rendered;
359  }
360 
370  public function getViewList($view) {
371  if (isset($this->extensions[$view])) {
372  return $this->extensions[$view];
373  } else {
374  return [self::BASE_VIEW_PRIORITY => $view];
375  }
376  }
377 
391  public function renderView($view, array $vars = [], $viewtype = '', $issue_missing_notice = true, array $extensions_tree = []) {
392  $view = self::canonicalizeViewName($view);
393 
394  if (!is_string($view) || !is_string($viewtype)) {
395  $this->logger->notice("View and Viewtype in views must be a strings: $view");
396 
397  return '';
398  }
399  // basic checking for bad paths
400  if (strpos($view, '..') !== false) {
401  return '';
402  }
403 
404  // check for extension deadloops
405  if (in_array($view, $extensions_tree)) {
406  $this->logger->error("View $view is detected as an extension of itself. This is not allowed");
407 
408  return '';
409  }
410  $extensions_tree[] = $view;
411 
412  // Get the current viewtype
413  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
414  $viewtype = $this->getViewtype();
415  }
416 
417  // allow altering $vars
418  $vars_hook_params = [
419  'view' => $view,
420  'vars' => $vars,
421  'viewtype' => $viewtype,
422  ];
423  $vars = $this->hooks->trigger(self::VIEW_VARS_HOOK, $view, $vars_hook_params, $vars);
424 
425  // allow $vars to hijack output
426  if (isset($vars[self::OUTPUT_KEY])) {
427  return (string) $vars[self::OUTPUT_KEY];
428  }
429 
430  $viewlist = $this->getViewList($view);
431 
432  $content = '';
433  foreach ($viewlist as $priority => $view_name) {
434  if ($priority !== self::BASE_VIEW_PRIORITY) {
435  // the others are extensions
436  $content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
437  continue;
438  }
439 
440  // actual rendering of a single view
441  $rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
442  if ($rendering !== false) {
443  $content .= $rendering;
444  continue;
445  }
446 
447  // attempt to load default view
448  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
449  $rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
450  if ($rendering !== false) {
451  $content .= $rendering;
452  }
453  }
454  }
455 
456  // Plugin hook
457  $params = [
458  'view' => $view,
459  'vars' => $vars,
460  'viewtype' => $viewtype,
461  ];
462  $content = $this->hooks->trigger(self::VIEW_HOOK, $view, $params, $content);
463 
464  return $content;
465  }
466 
475  protected function fileExists($path) {
476  if (!isset($this->file_exists_cache[$path])) {
477  $this->file_exists_cache[$path] = file_exists($path);
478  }
479 
480  return $this->file_exists_cache[$path];
481  }
482 
493  private function renderViewFile($view, array $vars, $viewtype, $issue_missing_notice) {
494  $file = $this->findViewFile($view, $viewtype);
495  if (!$file) {
496  if ($issue_missing_notice) {
497  $this->logger->notice("$viewtype/$view view does not exist.");
498  }
499 
500  return false;
501  }
502 
503  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
504  ob_start();
505 
506  try {
507  // don't isolate, scripts use the local $vars
508  include $file;
509 
510  return ob_get_clean();
511  } catch (\Exception $e) {
512  ob_get_clean();
513  throw $e;
514  }
515  }
516 
517  return file_get_contents($file);
518  }
519 
531  public function viewExists($view, $viewtype = '', $recurse = true) {
532  $view = self::canonicalizeViewName($view);
533 
534  if (empty($view) || !is_string($view)) {
535  return false;
536  }
537 
538  // Detect view type
539  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
540  $viewtype = $this->getViewtype();
541  }
542 
543 
544  $file = $this->findViewFile($view, $viewtype);
545  if ($file) {
546  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  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  if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
562  return $this->viewExists($view, 'default');
563  }
564 
565  return false;
566 
567  }
568 
580  public function extendView($view, $view_extension, $priority = 501) {
581  $view = self::canonicalizeViewName($view);
582  $view_extension = self::canonicalizeViewName($view_extension);
583 
584  if ($view === $view_extension) {
585  // do not allow direct extension on self with self
586  return;
587  }
588 
589  if (!isset($this->extensions[$view])) {
590  $this->extensions[$view][self::BASE_VIEW_PRIORITY] = (string) $view;
591  }
592 
593  // raise priority until it doesn't match one already registered
594  while (isset($this->extensions[$view][$priority])) {
595  $priority++;
596  }
597 
598  $this->extensions[$view][$priority] = (string) $view_extension;
599  ksort($this->extensions[$view]);
600  }
601 
609  public function viewIsExtended($view) {
610  return count($this->getViewList($view)) > 1;
611  }
612 
620  public function viewHasHookHandlers($view) {
621  return $this->hooks->hasHandler('view', $view) || $this->hooks->hasHandler('view_vars', $view);
622  }
623 
634  public function unextendView($view, $view_extension) {
635  $view = self::canonicalizeViewName($view);
636  $view_extension = self::canonicalizeViewName($view_extension);
637 
638  if (!isset($this->extensions[$view])) {
639  return false;
640  }
641 
642  $extensions = $this->extensions[$view];
643  unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
644 
645  $priority = array_search($view_extension, $extensions);
646  if ($priority === false) {
647  return false;
648  }
649 
650  unset($this->extensions[$view][$priority]);
651 
652  return true;
653  }
654 
662  public function registerCacheableView($view) {
663  $view = self::canonicalizeViewName($view);
664 
665  $this->simplecache_views[$view] = true;
666  }
667 
675  public function isCacheableView($view) {
676  $view = self::canonicalizeViewName($view);
677  if (isset($this->simplecache_views[$view])) {
678  return true;
679  }
680 
681  // build list of viewtypes to check
682  $current_viewtype = $this->getViewtype();
683  $viewtypes = [$current_viewtype];
684 
685  if ($this->doesViewtypeFallback($current_viewtype) && $current_viewtype != 'default') {
686  $viewtypes[] = 'default';
687  }
688 
689  // If a static view file is found in any viewtype, it's considered cacheable
690  foreach ($viewtypes as $viewtype) {
691  $file = $this->findViewFile($view, $viewtype);
692 
693  if ($file && pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
694  $this->simplecache_views[$view] = true;
695 
696  return true;
697  }
698  }
699 
700  // Assume not-cacheable by default
701  return false;
702  }
703 
712  public function registerPluginViews($path, &$failed_dir = '') {
713  $path = rtrim($path, "\\/");
714  $view_dir = "$path/views/";
715 
716  // plugins don't have to have views.
717  if (!is_dir($view_dir)) {
718  return true;
719  }
720 
721  // but if they do, they have to be readable
722  $handle = opendir($view_dir);
723  if (!$handle) {
724  $failed_dir = $view_dir;
725 
726  return false;
727  }
728 
729  while (false !== ($view_type = readdir($handle))) {
730  $view_type_dir = $view_dir . $view_type;
731 
732  if ('.' !== substr($view_type, 0, 1) && is_dir($view_type_dir)) {
733  if (!$this->autoregisterViews('', $view_type_dir, $view_type)) {
734  $failed_dir = $view_type_dir;
735 
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.
if(!array_key_exists($filename, $text_files)) $file
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.
__construct(PluginHooksService $hooks, LoggerInterface $logger, HttpRequest $request=null)
Constructor.
$extensions
static canonicalizeViewName($alias)
Takes a view name and returns the canonical name for that view.
cacheConfiguration(SystemCache $cache)
Cache the configuration.
$request
Page handler for autocomplete endpoint.
Definition: livesearch.php:9
doesViewtypeFallback($viewtype)
Checks if a viewtype falls back to default.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:12
$path
Definition: details.php:89
if(elgg_trigger_plugin_hook('usersettings:save', 'user', $hooks_params, true)) foreach($request->validation() ->all() as $item) $data
Definition: save.php:57
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.
getInspectorData()
Get inspector data.
getViewtype()
Get the viewtype.
mergeViewsSpec(array $spec)
Merge a specification of absolute view paths.
Configuration exception.
if(!$owner||!$owner->canEdit()) if(!$owner->hasIcon('master')) if(!$owner->saveIconFromElggFile($owner->getIcon('master'), 'icon', $coords)) $view
Definition: crop.php:30
registerPluginViews($path, &$failed_dir= '')
Register a plugin&#39;s views.
renderView($view, array $vars=[], $viewtype= '', $issue_missing_notice=true, array $extensions_tree=[])
Renders a view.
clampViewtypeToPopulatedViews()
If the current viewtype has no views, reset it to "default".
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:841
$viewtype
Definition: default.php:11
save($type, $data)
Saves a system cache.
Definition: SystemCache.php:53
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
$vars['type']
Definition: save.php:11
$location
Definition: default.php:42
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.
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.
$version
Definition: version.php:14
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:68
$extension
Definition: default.php:28