Elgg  Version master
ViewsService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
10 
17 class ViewsService {
18 
19  use Loggable;
20 
21  const VIEW_HOOK = 'view';
22  const VIEW_VARS_HOOK = 'view_vars';
23  const OUTPUT_KEY = '__view_output';
24  const BASE_VIEW_PRIORITY = 500;
25 
30  protected array $file_exists_cache = [];
31 
37  protected array $locations = [];
38 
44  protected array $overrides = [];
45 
51  protected array $extensions = [];
52 
56  protected array $fallbacks = [];
57 
58  protected ?string $viewtype;
59 
60  protected bool $locations_loaded_from_cache = false;
61 
70  public function __construct(
71  protected EventsService $events,
72  protected HttpRequest $request,
73  protected Config $config,
74  protected SystemCache $server_cache
75  ) {
76  }
77 
85  public function setViewtype(string $viewtype = ''): bool {
86  if (!$viewtype) {
87  $this->viewtype = null;
88 
89  return true;
90  }
91 
92  if ($this->isValidViewtype($viewtype)) {
93  $this->viewtype = $viewtype;
94 
95  return true;
96  }
97 
98  return false;
99  }
100 
106  public function getViewtype(): string {
107  if (!isset($this->viewtype)) {
108  $this->viewtype = $this->resolveViewtype();
109  }
110 
111  return $this->viewtype;
112  }
113 
119  protected function resolveViewtype(): string {
120  if ($this->request) {
121  $view = $this->request->getParam('view', '', false);
122  if ($this->isValidViewtype($view) && !empty($this->locations[$view])) {
123  return $view;
124  }
125  }
126 
127  $view = (string) $this->config->view;
128  if ($this->isValidViewtype($view) && !empty($this->locations[$view])) {
129  return $view;
130  }
131 
132  return 'default';
133  }
134 
142  public function isValidViewtype(string $viewtype): bool {
143  if ($viewtype === '') {
144  return false;
145  }
146 
147  if (preg_match('/\W/', $viewtype)) {
148  return false;
149  }
150 
151  return true;
152  }
153 
163  public function autoregisterViews(string $view_base, string $folder, string $viewtype): bool {
164  $folder = rtrim($folder, '/\\');
165  $view_base = rtrim($view_base, '/\\');
166 
167  if (!is_dir($folder) || !is_readable($folder)) {
168  $this->getLogger()->notice("Unable to register views from the directory: {$folder}");
169  return false;
170  }
171 
172  try {
173  $dir = new \DirectoryIterator($folder);
174  } catch (\Exception $e) {
175  $this->getLogger()->error($e->getMessage());
176  return false;
177  }
178 
179  $view_base_new = '';
180  if (!empty($view_base)) {
181  $view_base_new = $view_base . '/';
182  }
183 
184  /* @var $fileinfo \SplFileInfo */
185  foreach ($dir as $fileinfo) {
186  if ($fileinfo->isDot()) {
187  continue;
188  }
189 
190  $path = $fileinfo->getPathname();
191 
192  if ($fileinfo->isDir()) {
193  // Found a directory so go deeper
194  $this->autoregisterViews($view_base_new . $fileinfo->getFilename(), $path, $viewtype);
195  continue;
196  }
197 
198  // found a file add it to the views
199  $view = $view_base_new . $fileinfo->getBasename('.php');
200  $this->setViewLocation($view, $viewtype, $path);
201  }
202 
203  return true;
204  }
205 
214  public function findViewFile(string $view, string $viewtype): string {
215  if (!isset($this->locations[$viewtype][$view])) {
216  return '';
217  }
218 
219  $path = $this->locations[$viewtype][$view];
220  if ($this->fileExists($path)) {
221  return $path;
222  }
223 
224  return '';
225  }
226 
237  public function registerViewtypeFallback(string $viewtype): void {
238  $this->fallbacks[] = $viewtype;
239  }
240 
248  public function doesViewtypeFallback(string $viewtype): bool {
249  return in_array($viewtype, $this->fallbacks);
250  }
251 
264  public function renderDeprecatedView(string $view, array $vars, string $suggestion, string $version): string {
265  $rendered = $this->renderView($view, $vars, '', false);
266  if ($rendered) {
267  $this->logDeprecatedMessage("The '{$view}' view has been deprecated. {$suggestion}", $version);
268  }
269 
270  return $rendered;
271  }
272 
282  public function getViewList(string $view): array {
283  return $this->extensions[$view] ?? [self::BASE_VIEW_PRIORITY => $view];
284  }
285 
299  public function renderView(string $view, array $vars = [], string $viewtype = '', bool $issue_missing_notice = null, array $extensions_tree = []): string {
300  // basic checking for bad paths
301  if (str_contains($view, '..')) {
302  return '';
303  }
304 
305  // check for extension deadloops
306  if (in_array($view, $extensions_tree)) {
307  $this->getLogger()->error("View $view is detected as an extension of itself. This is not allowed");
308 
309  return '';
310  }
311 
312  $extensions_tree[] = $view;
313 
314  // Get the current viewtype
315  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
316  $viewtype = $this->getViewtype();
317  }
318 
319  if (!isset($issue_missing_notice)) {
320  $issue_missing_notice = $viewtype === 'default';
321  }
322 
323  // allow altering $vars
324  $vars_event_params = [
325  'view' => $view,
326  'vars' => $vars,
327  'viewtype' => $viewtype,
328  ];
329  $vars = $this->events->triggerResults(self::VIEW_VARS_HOOK, $view, $vars_event_params, $vars);
330 
331  // allow $vars to hijack output
332  if (isset($vars[self::OUTPUT_KEY])) {
333  return (string) $vars[self::OUTPUT_KEY];
334  }
335 
336  $viewlist = $this->getViewList($view);
337 
338  $content = '';
339  foreach ($viewlist as $priority => $view_name) {
340  if ($priority !== self::BASE_VIEW_PRIORITY) {
341  // the others are extensions
342  $content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
343  continue;
344  }
345 
346  // actual rendering of a single view
347  $rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
348  if ($rendering !== false) {
349  $content .= $rendering;
350  continue;
351  }
352 
353  // attempt to load default view
354  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
355  $rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
356  if ($rendering !== false) {
357  $content .= $rendering;
358  }
359  }
360  }
361 
362  $params = [
363  'view' => $view,
364  'vars' => $vars,
365  'viewtype' => $viewtype,
366  ];
367 
368  return (string) $this->events->triggerResults(self::VIEW_HOOK, $view, $params, $content);
369  }
370 
379  protected function fileExists(string $path): bool {
380  if (!isset($this->file_exists_cache[$path])) {
381  $this->file_exists_cache[$path] = file_exists($path);
382  }
383 
384  return $this->file_exists_cache[$path];
385  }
386 
397  protected function renderViewFile(string $view, array $vars, string $viewtype, bool $issue_missing_notice): string|false {
398  $file = $this->findViewFile($view, $viewtype);
399  if (!$file) {
400  if ($issue_missing_notice) {
401  $this->getLogger()->notice("$viewtype/$view view does not exist.");
402  }
403 
404  return false;
405  }
406 
407  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
408  ob_start();
409 
410  try {
411  // don't isolate, scripts use the local $vars
412  include $file;
413 
414  return ob_get_clean();
415  } catch (\Exception $e) {
416  ob_get_clean();
417  throw $e;
418  }
419  }
420 
421  return file_get_contents($file);
422  }
423 
435  public function viewExists(string $view, string $viewtype = '', bool $recurse = true): bool {
436  if (empty($view)) {
437  return false;
438  }
439 
440  // Detect view type
441  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
442  $viewtype = $this->getViewtype();
443  }
444 
445 
446  $file = $this->findViewFile($view, $viewtype);
447  if ($file) {
448  return true;
449  }
450 
451  // If we got here then check whether this exists as an extension
452  // We optionally recursively check whether the extended view exists also for the viewtype
453  if ($recurse && isset($this->extensions[$view])) {
454  foreach ($this->extensions[$view] as $view_extension) {
455  // do not recursively check to stay away from infinite loops
456  if ($this->viewExists($view_extension, $viewtype, false)) {
457  return true;
458  }
459  }
460  }
461 
462  // Now check if the default view exists if the view is registered as a fallback
463  if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
464  return $this->viewExists($view, 'default');
465  }
466 
467  return false;
468  }
469 
481  public function extendView(string $view, string $view_extension, int $priority = 501): void {
482  if ($view === $view_extension) {
483  // do not allow direct extension on self with self
484  return;
485  }
486 
487  if (!isset($this->extensions[$view])) {
488  $this->extensions[$view][self::BASE_VIEW_PRIORITY] = (string) $view;
489  }
490 
491  // raise priority until it doesn't match one already registered
492  while (isset($this->extensions[$view][$priority])) {
493  $priority++;
494  }
495 
496  $this->extensions[$view][$priority] = (string) $view_extension;
497  ksort($this->extensions[$view]);
498  }
499 
510  public function unextendView(string $view, string $view_extension): bool {
511  if (!isset($this->extensions[$view])) {
512  return false;
513  }
514 
515  $extensions = $this->extensions[$view];
516  unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
517 
518  $priority = array_search($view_extension, $extensions);
519  if ($priority === false) {
520  return false;
521  }
522 
523  unset($this->extensions[$view][$priority]);
524 
525  return true;
526  }
527 
535  public function registerViewsFromPath(string $path): bool {
536  $path = Paths::sanitize($path);
537  $view_dir = "{$path}views/";
538 
539  // do not fail on non existing views folder
540  if (!is_dir($view_dir)) {
541  return true;
542  }
543 
544  // but if folder exists it has to be readable
545  $handle = opendir($view_dir);
546  if ($handle === false) {
547  $this->getLogger()->notice("Unable to register views from the directory: {$view_dir}");
548 
549  return false;
550  }
551 
552  while (($view_type = readdir($handle)) !== false) {
553  $view_type_dir = $view_dir . $view_type;
554 
555  if (!str_starts_with($view_type, '.') && is_dir($view_type_dir)) {
556  if (!$this->autoregisterViews('', $view_type_dir, $view_type)) {
557  return false;
558  }
559  }
560  }
561 
562  return true;
563  }
564 
575  public function mergeViewsSpec(array $spec): void {
576  foreach ($spec as $viewtype => $list) {
577  foreach ($list as $view => $paths) {
578  if (!is_array($paths)) {
579  $paths = [$paths];
580  }
581 
582  foreach ($paths as $path) {
583  if (!preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
584  // relative path
585  $path = Directory\Local::projectRoot()->getPath($path);
586  }
587 
588  if (str_ends_with($view, '/')) {
589  // prefix
590  $this->autoregisterViews($view, $path, $viewtype);
591  } else {
592  $this->setViewLocation($view, $viewtype, $path);
593  }
594  }
595  }
596  }
597  }
598 
606  public function listViews(string $viewtype = 'default'): array {
607  if (empty($this->locations[$viewtype])) {
608  return [];
609  }
610 
611  return array_keys($this->locations[$viewtype]);
612  }
613 
619  public function getInspectorData(): array {
620  $overrides = $this->overrides;
621 
622  if ($this->server_cache) {
623  $data = $this->server_cache->load('view_overrides');
624  if (is_array($data)) {
625  $overrides = $data;
626  }
627  }
628 
629  return [
630  'locations' => $this->locations,
631  'overrides' => $overrides,
632  'extensions' => $this->extensions,
633  'simplecache' => _elgg_services()->simpleCache->getCacheableViews(),
634  ];
635  }
636 
642  public function configureFromCache(): void {
643  if (!$this->server_cache->isEnabled()) {
644  return;
645  }
646 
647  $data = $this->server_cache->load('view_locations');
648  if (!is_array($data)) {
649  return;
650  }
651 
652  $this->locations = $data['locations'];
653  $this->locations_loaded_from_cache = true;
654  }
655 
661  public function cacheConfiguration(): void {
662  if (!$this->server_cache->isEnabled()) {
663  return;
664  }
665 
666  // only cache if not already loaded
667  if ($this->isViewLocationsLoadedFromCache()) {
668  return;
669  }
670 
671  if (empty($this->locations)) {
672  $this->server_cache->delete('view_locations');
673  return;
674  }
675 
676  $this->server_cache->save('view_locations', ['locations' => $this->locations]);
677 
678  // this is saved just for the inspector and is not loaded in loadAll()
679  $this->server_cache->save('view_overrides', $this->overrides);
680  }
681 
688  public function isViewLocationsLoadedFromCache(): bool {
689  return $this->locations_loaded_from_cache;
690  }
691 
697  public function getESModules(): array {
698  $modules = $this->server_cache->load('esmodules');
699  if (is_array($modules)) {
700  return $modules;
701  }
702 
703  $modules = [];
704  foreach ($this->locations['default'] as $name => $path) {
705  if (!str_ends_with($name, '.mjs')) {
706  continue;
707  }
708 
709  $modules[] = $name;
710  }
711 
712  $this->server_cache->save('esmodules', $modules);
713 
714  return $modules;
715  }
716 
726  protected function setViewLocation(string $view, string $viewtype, string $path): void {
727  $path = strtr($path, '\\', '/');
728 
729  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
730  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
731  }
732 
733  $this->locations[$viewtype][$view] = $path;
734 
735  // Test if view is cacheable and push it to the cacheable views stack,
736  // if it's not registered as cacheable explicitly
737  _elgg_services()->simpleCache->isCacheableView($view);
738  }
739 }
list extensions
Definition: conf.py:31
viewExists(string $view, string $viewtype= '', bool $recurse=true)
Returns whether the specified view exists.
$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.
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
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.
$vars
Definition: theme.php:5
$content
Set robots.txt action.
Definition: set_robots.php:6
_elgg_services()
Get the global service provider.
Definition: elgglib.php:346
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.
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
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