Elgg  Version master
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 array $file_exists_cache = [];
32 
38  protected array $locations = [];
39 
45  protected array $overrides = [];
46 
52  protected array $simplecache_views = [];
53 
59  protected array $extensions = [];
60 
64  protected array $fallbacks = [];
65 
66  protected ?string $viewtype;
67 
68  protected bool $locations_loaded_from_cache = false;
69 
78  public function __construct(protected EventsService $events, protected HttpRequest $request, protected Config $config, protected SystemCache $server_cache) {
79  }
80 
88  public function setViewtype(string $viewtype = ''): bool {
89  if (!$viewtype) {
90  $this->viewtype = null;
91 
92  return true;
93  }
94 
95  if ($this->isValidViewtype($viewtype)) {
96  $this->viewtype = $viewtype;
97 
98  return true;
99  }
100 
101  return false;
102  }
103 
109  public function getViewtype(): string {
110  if (!isset($this->viewtype)) {
111  $this->viewtype = $this->resolveViewtype();
112  }
113 
114  return $this->viewtype;
115  }
116 
122  protected function resolveViewtype(): string {
123  if ($this->request) {
124  $view = $this->request->getParam('view', '', false);
125  if ($this->isValidViewtype($view) && !empty($this->locations[$view])) {
126  return $view;
127  }
128  }
129 
130  $view = (string) $this->config->view;
131  if ($this->isValidViewtype($view) && !empty($this->locations[$view])) {
132  return $view;
133  }
134 
135  return 'default';
136  }
137 
145  public function isValidViewtype(string $viewtype): bool {
146  if ($viewtype === '') {
147  return false;
148  }
149 
150  if (preg_match('/\W/', $viewtype)) {
151  return false;
152  }
153 
154  return true;
155  }
156 
164  public static function canonicalizeViewName(string $alias): string {
165 
166  $canonical = $alias;
167 
168  $extension = pathinfo($canonical, PATHINFO_EXTENSION);
169  $hasValidFileExtension = isset(CacheHandler::$extensions[$extension]);
170 
171  if (str_starts_with($canonical, 'js/')) {
172  $canonical = substr($canonical, 3);
173  if (!$hasValidFileExtension) {
174  $canonical .= '.js';
175  }
176  } else if (str_starts_with($canonical, 'css/')) {
177  $canonical = substr($canonical, 4);
178  if (!$hasValidFileExtension) {
179  $canonical .= '.css';
180  }
181  }
182 
183  return $canonical;
184  }
185 
195  public function autoregisterViews(string $view_base, string $folder, string $viewtype): bool {
196  $folder = rtrim($folder, '/\\');
197  $view_base = rtrim($view_base, '/\\');
198 
199  if (!is_dir($folder) || !is_readable($folder)) {
200  $this->getLogger()->notice("Unable to register views from the directory: {$folder}");
201  return false;
202  }
203 
204  try {
205  $dir = new \DirectoryIterator($folder);
206  } catch (\Exception $e) {
207  $this->getLogger()->error($e->getMessage());
208  return false;
209  }
210 
211  $view_base_new = '';
212  if (!empty($view_base)) {
213  $view_base_new = $view_base . '/';
214  }
215 
216  /* @var $fileinfo \SplFileInfo */
217  foreach ($dir as $fileinfo) {
218  if ($fileinfo->isDot()) {
219  continue;
220  }
221 
222  $path = $fileinfo->getPathname();
223 
224  if ($fileinfo->isDir()) {
225  // Found a directory so go deeper
226  $this->autoregisterViews($view_base_new . $fileinfo->getFilename(), $path, $viewtype);
227  continue;
228  }
229 
230  // found a file add it to the views
231  $view = $view_base_new . $fileinfo->getBasename('.php');
232  $this->setViewLocation($view, $viewtype, $path);
233  }
234 
235  return true;
236  }
237 
246  public function findViewFile(string $view, string $viewtype): string {
247  if (!isset($this->locations[$viewtype][$view])) {
248  return '';
249  }
250 
251  $path = $this->locations[$viewtype][$view];
252  if ($this->fileExists($path)) {
253  return $path;
254  }
255 
256  return '';
257  }
258 
270  public function setViewDir(string $view, string $location, string $viewtype = ''): void {
271  $view = self::canonicalizeViewName($view);
272 
273  if (empty($viewtype)) {
274  $viewtype = 'default';
275  }
276 
277  $location = rtrim($location, '/\\');
278 
279  if ($this->fileExists("$location/$viewtype/$view.php")) {
280  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view.php");
281  } else if ($this->fileExists("$location/$viewtype/$view")) {
282  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view");
283  }
284  }
285 
296  public function registerViewtypeFallback(string $viewtype): void {
297  $this->fallbacks[] = $viewtype;
298  }
299 
307  public function doesViewtypeFallback(string $viewtype): bool {
308  return in_array($viewtype, $this->fallbacks);
309  }
310 
323  public function renderDeprecatedView(string $view, array $vars, string $suggestion, string $version): string {
324  $view = self::canonicalizeViewName($view);
325 
326  $rendered = $this->renderView($view, $vars, '', false);
327  if ($rendered) {
328  $this->logDeprecatedMessage("The '{$view}' view has been deprecated. {$suggestion}", $version);
329  }
330 
331  return $rendered;
332  }
333 
343  public function getViewList(string $view): array {
344  return $this->extensions[$view] ?? [self::BASE_VIEW_PRIORITY => $view];
345  }
346 
360  public function renderView(string $view, array $vars = [], string $viewtype = '', bool $issue_missing_notice = null, array $extensions_tree = []): string {
361  $view = self::canonicalizeViewName($view);
362 
363  // basic checking for bad paths
364  if (str_contains($view, '..')) {
365  return '';
366  }
367 
368  // check for extension deadloops
369  if (in_array($view, $extensions_tree)) {
370  $this->getLogger()->error("View $view is detected as an extension of itself. This is not allowed");
371 
372  return '';
373  }
374 
375  $extensions_tree[] = $view;
376 
377  // Get the current viewtype
378  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
379  $viewtype = $this->getViewtype();
380  }
381 
382  if (!isset($issue_missing_notice)) {
383  $issue_missing_notice = $viewtype === 'default';
384  }
385 
386  // allow altering $vars
387  $vars_event_params = [
388  'view' => $view,
389  'vars' => $vars,
390  'viewtype' => $viewtype,
391  ];
392  $vars = $this->events->triggerResults(self::VIEW_VARS_HOOK, $view, $vars_event_params, $vars);
393 
394  // allow $vars to hijack output
395  if (isset($vars[self::OUTPUT_KEY])) {
396  return (string) $vars[self::OUTPUT_KEY];
397  }
398 
399  $viewlist = $this->getViewList($view);
400 
401  $content = '';
402  foreach ($viewlist as $priority => $view_name) {
403  if ($priority !== self::BASE_VIEW_PRIORITY) {
404  // the others are extensions
405  $content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
406  continue;
407  }
408 
409  // actual rendering of a single view
410  $rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
411  if ($rendering !== false) {
412  $content .= $rendering;
413  continue;
414  }
415 
416  // attempt to load default view
417  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
418  $rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
419  if ($rendering !== false) {
420  $content .= $rendering;
421  }
422  }
423  }
424 
425  $params = [
426  'view' => $view,
427  'vars' => $vars,
428  'viewtype' => $viewtype,
429  ];
430 
431  return (string) $this->events->triggerResults(self::VIEW_HOOK, $view, $params, $content);
432  }
433 
442  protected function fileExists(string $path): bool {
443  if (!isset($this->file_exists_cache[$path])) {
444  $this->file_exists_cache[$path] = file_exists($path);
445  }
446 
447  return $this->file_exists_cache[$path];
448  }
449 
460  protected function renderViewFile(string $view, array $vars, string $viewtype, bool $issue_missing_notice): string|false {
461  $file = $this->findViewFile($view, $viewtype);
462  if (!$file) {
463  if ($issue_missing_notice) {
464  $this->getLogger()->notice("$viewtype/$view view does not exist.");
465  }
466 
467  return false;
468  }
469 
470  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
471  ob_start();
472 
473  try {
474  // don't isolate, scripts use the local $vars
475  include $file;
476 
477  return ob_get_clean();
478  } catch (\Exception $e) {
479  ob_get_clean();
480  throw $e;
481  }
482  }
483 
484  return file_get_contents($file);
485  }
486 
498  public function viewExists(string $view, string $viewtype = '', bool $recurse = true): bool {
499  $view = self::canonicalizeViewName($view);
500 
501  if (empty($view)) {
502  return false;
503  }
504 
505  // Detect view type
506  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
507  $viewtype = $this->getViewtype();
508  }
509 
510 
511  $file = $this->findViewFile($view, $viewtype);
512  if ($file) {
513  return true;
514  }
515 
516  // If we got here then check whether this exists as an extension
517  // We optionally recursively check whether the extended view exists also for the viewtype
518  if ($recurse && isset($this->extensions[$view])) {
519  foreach ($this->extensions[$view] as $view_extension) {
520  // do not recursively check to stay away from infinite loops
521  if ($this->viewExists($view_extension, $viewtype, false)) {
522  return true;
523  }
524  }
525  }
526 
527  // Now check if the default view exists if the view is registered as a fallback
528  if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
529  return $this->viewExists($view, 'default');
530  }
531 
532  return false;
533  }
534 
546  public function extendView(string $view, string $view_extension, int $priority = 501): void {
547  $view = self::canonicalizeViewName($view);
548  $view_extension = self::canonicalizeViewName($view_extension);
549 
550  if ($view === $view_extension) {
551  // do not allow direct extension on self with self
552  return;
553  }
554 
555  if (!isset($this->extensions[$view])) {
556  $this->extensions[$view][self::BASE_VIEW_PRIORITY] = (string) $view;
557  }
558 
559  // raise priority until it doesn't match one already registered
560  while (isset($this->extensions[$view][$priority])) {
561  $priority++;
562  }
563 
564  $this->extensions[$view][$priority] = (string) $view_extension;
565  ksort($this->extensions[$view]);
566  }
567 
578  public function unextendView(string $view, string $view_extension): bool {
579  $view = self::canonicalizeViewName($view);
580  $view_extension = self::canonicalizeViewName($view_extension);
581 
582  if (!isset($this->extensions[$view])) {
583  return false;
584  }
585 
586  $extensions = $this->extensions[$view];
587  unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
588 
589  $priority = array_search($view_extension, $extensions);
590  if ($priority === false) {
591  return false;
592  }
593 
594  unset($this->extensions[$view][$priority]);
595 
596  return true;
597  }
598 
606  public function registerCacheableView(string $view): void {
607  $view = self::canonicalizeViewName($view);
608 
609  $this->simplecache_views[$view] = true;
610  }
611 
619  public function isCacheableView(string $view): bool {
620  $view = self::canonicalizeViewName($view);
621  if (isset($this->simplecache_views[$view])) {
622  return true;
623  }
624 
625  // build list of viewtypes to check
626  $current_viewtype = $this->getViewtype();
627  $viewtypes = [$current_viewtype];
628 
629  if ($this->doesViewtypeFallback($current_viewtype) && $current_viewtype != 'default') {
630  $viewtypes[] = 'default';
631  }
632 
633  // If a static view file is found in any viewtype, it's considered cacheable
634  foreach ($viewtypes as $viewtype) {
635  $file = $this->findViewFile($view, $viewtype);
636 
637  if ($file && pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
638  $this->simplecache_views[$view] = true;
639 
640  return true;
641  }
642  }
643 
644  // Assume not-cacheable by default
645  return false;
646  }
647 
655  public function registerViewsFromPath(string $path): bool {
656  $path = Paths::sanitize($path);
657  $view_dir = "{$path}views/";
658 
659  // do not fail on non existing views folder
660  if (!is_dir($view_dir)) {
661  return true;
662  }
663 
664  // but if folder exists it has to be readable
665  $handle = opendir($view_dir);
666  if ($handle === false) {
667  $this->getLogger()->notice("Unable to register views from the directory: {$view_dir}");
668 
669  return false;
670  }
671 
672  while (($view_type = readdir($handle)) !== false) {
673  $view_type_dir = $view_dir . $view_type;
674 
675  if (!str_starts_with($view_type, '.') && is_dir($view_type_dir)) {
676  if (!$this->autoregisterViews('', $view_type_dir, $view_type)) {
677  return false;
678  }
679  }
680  }
681 
682  return true;
683  }
684 
695  public function mergeViewsSpec(array $spec): void {
696  foreach ($spec as $viewtype => $list) {
697  foreach ($list as $view => $paths) {
698  if (!is_array($paths)) {
699  $paths = [$paths];
700  }
701 
702  foreach ($paths as $path) {
703  if (!preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
704  // relative path
705  $path = Directory\Local::projectRoot()->getPath($path);
706  }
707 
708  if (str_ends_with($view, '/')) {
709  // prefix
710  $this->autoregisterViews($view, $path, $viewtype);
711  } else {
712  $this->setViewLocation($view, $viewtype, $path);
713  }
714  }
715  }
716  }
717  }
718 
726  public function listViews(string $viewtype = 'default'): array {
727  if (empty($this->locations[$viewtype])) {
728  return [];
729  }
730 
731  return array_keys($this->locations[$viewtype]);
732  }
733 
739  public function getInspectorData(): array {
740  $overrides = $this->overrides;
741 
742  if ($this->server_cache) {
743  $data = $this->server_cache->load('view_overrides');
744  if (is_array($data)) {
745  $overrides = $data;
746  }
747  }
748 
749  return [
750  'locations' => $this->locations,
751  'overrides' => $overrides,
752  'extensions' => $this->extensions,
753  'simplecache' => $this->simplecache_views,
754  ];
755  }
756 
762  public function configureFromCache(): void {
763  if (!$this->server_cache->isEnabled()) {
764  return;
765  }
766 
767  $data = $this->server_cache->load('view_locations');
768  if (!is_array($data)) {
769  return;
770  }
771 
772  $this->locations = $data['locations'];
773  $this->locations_loaded_from_cache = true;
774  }
775 
781  public function cacheConfiguration(): void {
782  if (!$this->server_cache->isEnabled()) {
783  return;
784  }
785 
786  // only cache if not already loaded
787  if ($this->isViewLocationsLoadedFromCache()) {
788  return;
789  }
790 
791  if (empty($this->locations)) {
792  $this->server_cache->delete('view_locations');
793  return;
794  }
795 
796  $this->server_cache->save('view_locations', ['locations' => $this->locations]);
797 
798  // this is saved just for the inspector and is not loaded in loadAll()
799  $this->server_cache->save('view_overrides', $this->overrides);
800  }
801 
808  public function isViewLocationsLoadedFromCache(): bool {
809  return $this->locations_loaded_from_cache;
810  }
811 
817  public function getESModules(): array {
818  $modules = $this->server_cache->load('esmodules');
819  if (is_array($modules)) {
820  return $modules;
821  }
822 
823  $modules = [];
824  foreach ($this->locations['default'] as $name => $path) {
825  if (!str_ends_with($name, '.mjs')) {
826  continue;
827  }
828 
829  $modules[] = $name;
830  }
831 
832  $this->server_cache->save('esmodules', $modules);
833 
834  return $modules;
835  }
836 
846  protected function setViewLocation(string $view, string $viewtype, string $path): void {
847  $view = self::canonicalizeViewName($view);
848  $path = strtr($path, '\\', '/');
849 
850  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
851  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
852  }
853 
854  $this->locations[$viewtype][$view] = $path;
855 
856  // Test if view is cacheable and push it to the cacheable views stack,
857  // if it's not registered as cacheable explicitly
858  $this->isCacheableView($view);
859  }
860 }
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
if(parse_url(elgg_get_site_url(), PHP_URL_PATH)!== '/') if(file_exists(elgg_get_root_path(). 'robots.txt'))
Set robots.txt.
Definition: robots.php:10
isValidViewtype(string $viewtype)
Checks if $viewtype is a string suitable for use as a viewtype name.
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
listViews(string $viewtype= 'default')
List all views in a viewtype.
$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.
registerViewsFromPath(string $path)
Register all views in a given path.
cacheConfiguration()
Cache the configuration.
renderView(string $view, array $vars=[], string $viewtype= '', bool $issue_missing_notice=null, array $extensions_tree=[])
Renders a view.
configureFromCache()
Configure locations from the cache.
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
resolveViewtype()
Resolve the initial viewtype.
$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.
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.
$viewtype
Definition: default.php:11
isViewLocationsLoadedFromCache()
Checks if view_locations have been loaded from cache.
$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
$content
Set robots.txt action.
Definition: set_robots.php:6
autoregisterViews(string $view_base, string $folder, string $viewtype)
Auto-registers views from a location.
__construct(protected EventsService $events, protected HttpRequest $request, protected Config $config, protected SystemCache $server_cache)
Constructor.
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)...
getESModules()
Returns an array of names of ES modules detected based on view location.
renderViewFile(string $view, array $vars, string $viewtype, bool $issue_missing_notice)
Includes view PHP or static file.
$priority
$extension
Definition: default.php:25
setViewLocation(string $view, string $viewtype, string $path)
Update the location of a view file.
logDeprecatedMessage(string $message, string $version)
Sends a message about deprecated use of a function, view, etc.
Definition: Loggable.php:80