Elgg  Version 5.1
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 $events;
70 
74  private $cache;
75 
79  private $request;
80 
84  private $viewtype;
85 
92  public function __construct(EventsService $events, HttpRequest $request) {
93  $this->events = $events;
94  $this->request = $request;
95  }
96 
104  public function setViewtype(string $viewtype = ''): bool {
105  if (!$viewtype) {
106  $this->viewtype = null;
107 
108  return true;
109  }
110 
111  if ($this->isValidViewtype($viewtype)) {
112  $this->viewtype = $viewtype;
113 
114  return true;
115  }
116 
117  return false;
118  }
119 
125  public function getViewtype(): string {
126  if (!isset($this->viewtype)) {
127  $this->viewtype = $this->resolveViewtype();
128  }
129 
130  return $this->viewtype;
131  }
132 
138  private function resolveViewtype(): string {
139  if ($this->request) {
140  $view = $this->request->getParam('view', '', false);
141  if ($this->isValidViewtype($view) && !empty($this->locations[$view])) {
142  return $view;
143  }
144  }
145 
146  $view = (string) elgg_get_config('view');
147  if ($this->isValidViewtype($view) && !empty($this->locations[$view])) {
148  return $view;
149  }
150 
151  return 'default';
152  }
153 
161  public function isValidViewtype(string $viewtype): bool {
162  if ($viewtype === '') {
163  return false;
164  }
165 
166  if (preg_match('/\W/', $viewtype)) {
167  return false;
168  }
169 
170  return true;
171  }
172 
180  public static function canonicalizeViewName(string $alias): string {
181 
182  $canonical = $alias;
183 
184  $extension = pathinfo($canonical, PATHINFO_EXTENSION);
185  $hasValidFileExtension = isset(CacheHandler::$extensions[$extension]);
186 
187  if (str_starts_with($canonical, 'js/')) {
188  $canonical = substr($canonical, 3);
189  if (!$hasValidFileExtension) {
190  $canonical .= '.js';
191  }
192  } else if (str_starts_with($canonical, 'css/')) {
193  $canonical = substr($canonical, 4);
194  if (!$hasValidFileExtension) {
195  $canonical .= '.css';
196  }
197  }
198 
199  return $canonical;
200  }
201 
211  public function autoregisterViews(string $view_base, string $folder, string $viewtype): bool {
212  $folder = rtrim($folder, '/\\');
213  $view_base = rtrim($view_base, '/\\');
214 
215  if (!is_dir($folder) || !is_readable($folder)) {
216  $this->getLogger()->notice("Unable to register views from the directory: {$folder}");
217  return false;
218  }
219 
220  try {
221  $dir = new \DirectoryIterator($folder);
222  } catch (\Exception $e) {
223  $this->getLogger()->error($e->getMessage());
224  return false;
225  }
226 
227  $view_base_new = '';
228  if (!empty($view_base)) {
229  $view_base_new = $view_base . '/';
230  }
231 
232  /* @var $fileinfo \SplFileInfo */
233  foreach ($dir as $fileinfo) {
234  if ($fileinfo->isDot()) {
235  continue;
236  }
237 
238  $path = $fileinfo->getPathname();
239 
240  if ($fileinfo->isDir()) {
241  // Found a directory so go deeper
242  $this->autoregisterViews($view_base_new . $fileinfo->getFilename(), $path, $viewtype);
243  continue;
244  }
245 
246  // found a file add it to the views
247  $view = $view_base_new . $fileinfo->getBasename('.php');
248  $this->setViewLocation($view, $viewtype, $path);
249  }
250 
251  return true;
252  }
253 
262  public function findViewFile(string $view, string $viewtype): string {
263  if (!isset($this->locations[$viewtype][$view])) {
264  return '';
265  }
266 
267  $path = $this->locations[$viewtype][$view];
268  if ($this->fileExists($path)) {
269  return $path;
270  }
271 
272  return '';
273  }
274 
286  public function setViewDir(string $view, string $location, string $viewtype = ''): void {
287  $view = self::canonicalizeViewName($view);
288 
289  if (empty($viewtype)) {
290  $viewtype = 'default';
291  }
292 
293  $location = rtrim($location, '/\\');
294 
295  if ($this->fileExists("$location/$viewtype/$view.php")) {
296  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view.php");
297  } else if ($this->fileExists("$location/$viewtype/$view")) {
298  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view");
299  }
300  }
301 
312  public function registerViewtypeFallback(string $viewtype): void {
313  $this->fallbacks[] = $viewtype;
314  }
315 
323  public function doesViewtypeFallback(string $viewtype): bool {
324  return in_array($viewtype, $this->fallbacks);
325  }
326 
339  public function renderDeprecatedView(string $view, array $vars, string $suggestion, string $version): string {
340  $view = self::canonicalizeViewName($view);
341 
342  $rendered = $this->renderView($view, $vars, '', false);
343  if ($rendered) {
344  $this->logDeprecatedMessage("The '{$view}' view has been deprecated. {$suggestion}", $version);
345  }
346 
347  return $rendered;
348  }
349 
359  public function getViewList(string $view): array {
360  return $this->extensions[$view] ?? [self::BASE_VIEW_PRIORITY => $view];
361  }
362 
376  public function renderView(string $view, array $vars = [], string $viewtype = '', bool $issue_missing_notice = null, array $extensions_tree = []): string {
377  $view = self::canonicalizeViewName($view);
378 
379  // basic checking for bad paths
380  if (str_contains($view, '..')) {
381  return '';
382  }
383 
384  // check for extension deadloops
385  if (in_array($view, $extensions_tree)) {
386  $this->getLogger()->error("View $view is detected as an extension of itself. This is not allowed");
387 
388  return '';
389  }
390 
391  $extensions_tree[] = $view;
392 
393  // Get the current viewtype
394  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
395  $viewtype = $this->getViewtype();
396  }
397 
398  if (!isset($issue_missing_notice)) {
399  $issue_missing_notice = $viewtype === 'default';
400  }
401 
402  // allow altering $vars
403  $vars_event_params = [
404  'view' => $view,
405  'vars' => $vars,
406  'viewtype' => $viewtype,
407  ];
408  $vars = $this->events->triggerResults(self::VIEW_VARS_HOOK, $view, $vars_event_params, $vars);
409 
410  // allow $vars to hijack output
411  if (isset($vars[self::OUTPUT_KEY])) {
412  return (string) $vars[self::OUTPUT_KEY];
413  }
414 
415  $viewlist = $this->getViewList($view);
416 
417  $content = '';
418  foreach ($viewlist as $priority => $view_name) {
419  if ($priority !== self::BASE_VIEW_PRIORITY) {
420  // the others are extensions
421  $content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
422  continue;
423  }
424 
425  // actual rendering of a single view
426  $rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
427  if ($rendering !== false) {
428  $content .= $rendering;
429  continue;
430  }
431 
432  // attempt to load default view
433  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
434  $rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
435  if ($rendering !== false) {
436  $content .= $rendering;
437  }
438  }
439  }
440 
441  $params = [
442  'view' => $view,
443  'vars' => $vars,
444  'viewtype' => $viewtype,
445  ];
446 
447  return (string) $this->events->triggerResults(self::VIEW_HOOK, $view, $params, $content);
448  }
449 
458  protected function fileExists(string $path): bool {
459  if (!isset($this->file_exists_cache[$path])) {
460  $this->file_exists_cache[$path] = file_exists($path);
461  }
462 
463  return $this->file_exists_cache[$path];
464  }
465 
476  private function renderViewFile(string $view, array $vars, string $viewtype, bool $issue_missing_notice): string|false {
477  $file = $this->findViewFile($view, $viewtype);
478  if (!$file) {
479  if ($issue_missing_notice) {
480  $this->getLogger()->notice("$viewtype/$view view does not exist.");
481  }
482 
483  return false;
484  }
485 
486  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
487  ob_start();
488 
489  try {
490  // don't isolate, scripts use the local $vars
491  include $file;
492 
493  return ob_get_clean();
494  } catch (\Exception $e) {
495  ob_get_clean();
496  throw $e;
497  }
498  }
499 
500  return file_get_contents($file);
501  }
502 
514  public function viewExists(string $view, string $viewtype = '', bool $recurse = true): bool {
515  $view = self::canonicalizeViewName($view);
516 
517  if (empty($view)) {
518  return false;
519  }
520 
521  // Detect view type
522  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
523  $viewtype = $this->getViewtype();
524  }
525 
526 
527  $file = $this->findViewFile($view, $viewtype);
528  if ($file) {
529  return true;
530  }
531 
532  // If we got here then check whether this exists as an extension
533  // We optionally recursively check whether the extended view exists also for the viewtype
534  if ($recurse && isset($this->extensions[$view])) {
535  foreach ($this->extensions[$view] as $view_extension) {
536  // do not recursively check to stay away from infinite loops
537  if ($this->viewExists($view_extension, $viewtype, false)) {
538  return true;
539  }
540  }
541  }
542 
543  // Now check if the default view exists if the view is registered as a fallback
544  if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
545  return $this->viewExists($view, 'default');
546  }
547 
548  return false;
549  }
550 
562  public function extendView(string $view, string $view_extension, int $priority = 501): void {
563  $view = self::canonicalizeViewName($view);
564  $view_extension = self::canonicalizeViewName($view_extension);
565 
566  if ($view === $view_extension) {
567  // do not allow direct extension on self with self
568  return;
569  }
570 
571  if (!isset($this->extensions[$view])) {
572  $this->extensions[$view][self::BASE_VIEW_PRIORITY] = (string) $view;
573  }
574 
575  // raise priority until it doesn't match one already registered
576  while (isset($this->extensions[$view][$priority])) {
577  $priority++;
578  }
579 
580  $this->extensions[$view][$priority] = (string) $view_extension;
581  ksort($this->extensions[$view]);
582  }
583 
594  public function unextendView(string $view, string $view_extension): bool {
595  $view = self::canonicalizeViewName($view);
596  $view_extension = self::canonicalizeViewName($view_extension);
597 
598  if (!isset($this->extensions[$view])) {
599  return false;
600  }
601 
602  $extensions = $this->extensions[$view];
603  unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
604 
605  $priority = array_search($view_extension, $extensions);
606  if ($priority === false) {
607  return false;
608  }
609 
610  unset($this->extensions[$view][$priority]);
611 
612  return true;
613  }
614 
622  public function registerCacheableView(string $view): void {
623  $view = self::canonicalizeViewName($view);
624 
625  $this->simplecache_views[$view] = true;
626  }
627 
635  public function isCacheableView(string $view): bool {
636  $view = self::canonicalizeViewName($view);
637  if (isset($this->simplecache_views[$view])) {
638  return true;
639  }
640 
641  // build list of viewtypes to check
642  $current_viewtype = $this->getViewtype();
643  $viewtypes = [$current_viewtype];
644 
645  if ($this->doesViewtypeFallback($current_viewtype) && $current_viewtype != 'default') {
646  $viewtypes[] = 'default';
647  }
648 
649  // If a static view file is found in any viewtype, it's considered cacheable
650  foreach ($viewtypes as $viewtype) {
651  $file = $this->findViewFile($view, $viewtype);
652 
653  if ($file && pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
654  $this->simplecache_views[$view] = true;
655 
656  return true;
657  }
658  }
659 
660  // Assume not-cacheable by default
661  return false;
662  }
663 
671  public function registerPluginViews(string $path): bool {
672  $path = Paths::sanitize($path);
673  $view_dir = "{$path}views/";
674 
675  // plugins don't have to have views.
676  if (!is_dir($view_dir)) {
677  return true;
678  }
679 
680  // but if they do, they have to be readable
681  $handle = opendir($view_dir);
682  if ($handle === false) {
683  $this->getLogger()->notice("Unable to register views from the directory: {$view_dir}");
684 
685  return false;
686  }
687 
688  while (($view_type = readdir($handle)) !== false) {
689  $view_type_dir = $view_dir . $view_type;
690 
691  if (!str_starts_with($view_type, '.') && is_dir($view_type_dir)) {
692  if (!$this->autoregisterViews('', $view_type_dir, $view_type)) {
693  return false;
694  }
695  }
696  }
697 
698  return true;
699  }
700 
711  public function mergeViewsSpec(array $spec): void {
712  foreach ($spec as $viewtype => $list) {
713  foreach ($list as $view => $paths) {
714  if (!is_array($paths)) {
715  $paths = [$paths];
716  }
717 
718  foreach ($paths as $path) {
719  if (!preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
720  // relative path
721  $path = Directory\Local::projectRoot()->getPath($path);
722  }
723 
724  if (str_ends_with($view, '/')) {
725  // prefix
726  $this->autoregisterViews($view, $path, $viewtype);
727  } else {
728  $this->setViewLocation($view, $viewtype, $path);
729  }
730  }
731  }
732  }
733  }
734 
742  public function listViews(string $viewtype = 'default'): array {
743  if (empty($this->locations[$viewtype])) {
744  return [];
745  }
746 
747  return array_keys($this->locations[$viewtype]);
748  }
749 
755  public function getInspectorData(): array {
756  $overrides = $this->overrides;
757 
758  if ($this->cache) {
759  $data = $this->cache->load('view_overrides');
760  if (is_array($data)) {
761  $overrides = $data;
762  }
763  }
764 
765  return [
766  'locations' => $this->locations,
767  'overrides' => $overrides,
768  'extensions' => $this->extensions,
769  'simplecache' => $this->simplecache_views,
770  ];
771  }
772 
780  public function configureFromCache(SystemCache $cache): bool {
781  $data = $cache->load('view_locations');
782  if (!is_array($data)) {
783  return false;
784  }
785 
786  $this->locations = $data['locations'];
787  $this->cache = $cache;
788 
789  return true;
790  }
791 
799  public function cacheConfiguration(SystemCache $cache): void {
800  if (empty($this->locations)) {
801  $cache->delete('view_locations');
802  return;
803  }
804 
805  $cache->save('view_locations', ['locations' => $this->locations]);
806 
807  // this is saved just for the inspector and is not loaded in loadAll()
808  $cache->save('view_overrides', $this->overrides);
809  }
810 
820  private function setViewLocation(string $view, string $viewtype, string $path): void {
821  $view = self::canonicalizeViewName($view);
822  $path = strtr($path, '\\', '/');
823 
824  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
825  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
826  }
827 
828  $this->locations[$viewtype][$view] = $path;
829 
830  // Test if view is cacheable and push it to the cacheable views stack,
831  // if it's not registered as cacheable explicitly
832  $this->isCacheableView($view);
833  }
834 }
save(string $type, $data, int $expire_after=null)
Saves a system cache.
Definition: SystemCache.php:52
list extensions
Definition: conf.py:31
viewExists(string $view, string $viewtype= '', bool $recurse=true)
Returns whether the specified view exists.
isCacheableView(string $view)
Is the view cacheable.
$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
isValidViewtype(string $viewtype)
Checks if $viewtype is a string suitable for use as a viewtype name.
elgg_get_config(string $name, $default=null)
Get an Elgg configuration value.
listViews(string $viewtype= 'default')
List all views in a viewtype.
cacheConfiguration(SystemCache $cache)
Cache the configuration.
$request
Definition: livesearch.php:12
$version
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
Events service.
renderDeprecatedView(string $view, array $vars, string $suggestion, string $version)
Display a view with a deprecation notice.
setViewDir(string $view, string $location, string $viewtype= '')
Set an alternative base location for a view.
renderView(string $view, array $vars=[], string $viewtype= '', bool $issue_missing_notice=null, array $extensions_tree=[])
Renders a view.
delete(string $type)
Deletes the contents of a system cache.
Definition: SystemCache.php:84
$path
Definition: details.php:70
if(!$entity instanceof\ElggUser) $data
Definition: attributes.php:13
getInspectorData()
Get inspector data.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
getViewtype()
Get the viewtype.
unextendView(string $view, string $view_extension)
Unextends a view.
mergeViewsSpec(array $spec)
Merge a specification of absolute view paths.
load(string $type)
Retrieve the contents of a system cache.
Definition: SystemCache.php:67
setViewtype(string $viewtype= '')
Set the viewtype.
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
static canonicalizeViewName(string $alias)
Takes a view name and returns the canonical name for that view.
load(stdClass $row)
Loads attributes from the entities table into the object.
extendView(string $view, string $view_extension, int $priority=501)
Extends a view with another view.
Views service.
findViewFile(string $view, string $viewtype)
Find the view file.
__construct(EventsService $events, HttpRequest $request)
Constructor.
$viewtype
Definition: default.php:11
$extensions
doesViewtypeFallback(string $viewtype)
Checks if a viewtype falls back to default.
getLogger()
Returns logger.
Definition: Loggable.php:37
registerViewtypeFallback(string $viewtype)
Register a viewtype to fall back to a default view if a view isn&#39;t found for that viewtype...
getViewList(string $view)
Get the views, including extensions, used to render a view.
$location
Definition: member.php:29
$vars
Definition: theme.php:5
configureFromCache(SystemCache $cache)
Configure locations from the cache.
$content
Set robots.txt action.
Definition: set_robots.php:6
autoregisterViews(string $view_base, string $folder, string $viewtype)
Auto-registers views from a location.
registerCacheableView(string $view)
Register a view a cacheable.
fileExists(string $path)
Wrapper for file_exists() that caches false results (the stat cache only caches true results)...
$priority
registerPluginViews(string $path)
Register a plugin&#39;s views.
$extension
Definition: default.php:25
logDeprecatedMessage(string $message, string $version)
Sends a message about deprecated use of a function, view, etc.
Definition: Loggable.php:76