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