Elgg  Version master
ViewsService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
6 use Elgg\Http\Request as HttpRequest;
8 use Elgg\Traits\Loggable;
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 
28  protected array $file_exists_cache = [];
29 
33  protected array $locations = [];
34 
40  protected array $overrides = [];
41 
45  protected array $extensions = [];
46 
50  protected array $fallbacks = [];
51 
52  protected ?string $viewtype;
53 
54  protected bool $locations_loaded_from_cache = false;
55 
64  public function __construct(
65  protected EventsService $events,
66  protected HttpRequest $request,
67  protected Config $config,
68  protected ServerCache $server_cache
69  ) {
70  }
71 
79  public function setViewtype(string $viewtype = ''): bool {
80  if (!$viewtype) {
81  $this->viewtype = null;
82 
83  return true;
84  }
85 
86  if ($this->isValidViewtype($viewtype)) {
87  $this->viewtype = $viewtype;
88 
89  return true;
90  }
91 
92  return false;
93  }
94 
100  public function getViewtype(): string {
101  if (!isset($this->viewtype)) {
102  $this->viewtype = $this->resolveViewtype();
103  }
104 
105  return $this->viewtype;
106  }
107 
113  protected function resolveViewtype(): string {
114  if ($this->request) {
115  $view = $this->request->getParam('view', '', false);
116  if ($this->isValidViewtype($view) && !empty($this->locations[$view])) {
117  return $view;
118  }
119  }
120 
121  $view = (string) $this->config->view;
122  if ($this->isValidViewtype($view) && !empty($this->locations[$view])) {
123  return $view;
124  }
125 
126  return 'default';
127  }
128 
136  public function isValidViewtype(string $viewtype): bool {
137  if ($viewtype === '') {
138  return false;
139  }
140 
141  if (preg_match('/\W/', $viewtype)) {
142  return false;
143  }
144 
145  return true;
146  }
147 
154  public function registerCoreViews(): void {
155  if ($this->isViewLocationsLoadedFromCache()) {
156  return;
157  }
158 
159  // Core view files in /views
160  $this->registerViewsFromPath(Paths::elgg());
161 
162  // Core view definitions in /engine/views.php
163  $file = Paths::elgg() . 'engine/views.php';
164  if (!is_file($file)) {
165  return;
166  }
167 
168  $spec = Includer::includeFile($file);
169  if (is_array($spec)) {
170  // check for uploaded fontawesome font
171  if ($this->config->font_awesome_zip) {
172  $spec['default']['font-awesome/css/'] = elgg_get_data_path() . 'fontawesome/webfont/css/';
173  $spec['default']['font-awesome/webfonts/'] = elgg_get_data_path() . 'fontawesome/webfont/webfonts/';
174  }
175 
176  $this->mergeViewsSpec($spec);
177  }
178  }
179 
189  public function autoregisterViews(string $view_base, string $folder, string $viewtype): bool {
190  $folder = Paths::sanitize($folder);
191  $view_base = Paths::sanitize($view_base, false);
192  $view_base = $view_base ? $view_base . '/' : $view_base;
193 
194  try {
195  $dir = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS));
196  } catch (\Throwable $t) {
197  $this->getLogger()->error($t->getMessage());
198  return false;
199  }
200 
201  /* @var $file \SplFileInfo */
202  foreach ($dir as $file) {
203  $path = $file->getPath() . '/' . $file->getBasename('.php');
204  $path = Paths::sanitize($path, false);
205 
206  // found a file add it to the views
207  $view = $view_base . substr($path, strlen($folder));
208  $this->setViewLocation($view, $viewtype, $file->getPathname());
209  }
210 
211  return true;
212  }
213 
222  public function findViewFile(string $view, string $viewtype): string {
223  if (!isset($this->locations[$viewtype][$view])) {
224  return '';
225  }
226 
227  $path = $this->locations[$viewtype][$view];
228 
229  return $this->fileExists($path) ? $path : '';
230  }
231 
242  public function registerViewtypeFallback(string $viewtype): void {
243  $this->fallbacks[] = $viewtype;
244  }
245 
253  public function doesViewtypeFallback(string $viewtype): bool {
254  return in_array($viewtype, $this->fallbacks);
255  }
256 
269  public function renderDeprecatedView(string $view, array $vars, string $suggestion, string $version): string {
270  $rendered = $this->renderView($view, $vars, '', false);
271  if ($rendered) {
272  $this->logDeprecatedMessage("The '{$view}' view has been deprecated. {$suggestion}", $version);
273  }
274 
275  return $rendered;
276  }
277 
287  public function getViewList(string $view): array {
288  return $this->extensions[$view] ?? [self::BASE_VIEW_PRIORITY => $view];
289  }
290 
304  public function renderView(string $view, array $vars = [], string $viewtype = '', ?bool $issue_missing_notice = null, array $extensions_tree = []): string {
305  // basic checking for bad paths
306  if (str_contains($view, '..')) {
307  return '';
308  }
309 
310  // check for extension deadloops
311  if (in_array($view, $extensions_tree)) {
312  $this->getLogger()->error("View {$view} is detected as an extension of itself. This is not allowed");
313 
314  return '';
315  }
316 
317  $extensions_tree[] = $view;
318 
319  // Get the current viewtype
320  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
321  $viewtype = $this->getViewtype();
322  }
323 
324  if (!isset($issue_missing_notice)) {
325  $issue_missing_notice = $viewtype === 'default';
326  }
327 
328  // allow altering $vars
329  $vars_event_params = [
330  'view' => $view,
331  'vars' => $vars,
332  'viewtype' => $viewtype,
333  ];
334  $vars = $this->events->triggerResults(self::VIEW_VARS_HOOK, $view, $vars_event_params, $vars);
335 
336  // allow $vars to hijack output
337  if (isset($vars[self::OUTPUT_KEY])) {
338  return (string) $vars[self::OUTPUT_KEY];
339  }
340 
341  $viewlist = $this->getViewList($view);
342 
343  $content = '';
344  foreach ($viewlist as $priority => $view_name) {
345  if ($priority !== self::BASE_VIEW_PRIORITY) {
346  // the others are extensions
347  $content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
348  continue;
349  }
350 
351  // actual rendering of a single view
352  $rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
353  if ($rendering !== false) {
354  $content .= $rendering;
355  continue;
356  }
357 
358  // attempt to load default view
359  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
360  $rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
361  if ($rendering !== false) {
362  $content .= $rendering;
363  }
364  }
365  }
366 
367  $params = [
368  'view' => $view,
369  'vars' => $vars,
370  'viewtype' => $viewtype,
371  ];
372 
373  return (string) $this->events->triggerResults(self::VIEW_HOOK, $view, $params, $content);
374  }
375 
384  protected function fileExists(string $path): bool {
385  if (!isset($this->file_exists_cache[$path])) {
386  $this->file_exists_cache[$path] = file_exists($path);
387  }
388 
389  return $this->file_exists_cache[$path];
390  }
391 
402  protected function renderViewFile(string $view, array $vars, string $viewtype, bool $issue_missing_notice): string|false {
403  $file = $this->findViewFile($view, $viewtype);
404  if (!$file) {
405  if ($issue_missing_notice) {
406  $this->getLogger()->notice("{$viewtype}/{$view} view does not exist.");
407  }
408 
409  return false;
410  }
411 
412  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
413  ob_start();
414 
415  try {
416  // don't isolate, scripts use the local $vars
417  include $file;
418 
419  return ob_get_clean();
420  } catch (\Exception $e) {
421  ob_get_clean();
422  throw $e;
423  }
424  }
425 
426  return file_get_contents($file);
427  }
428 
440  public function viewExists(string $view, string $viewtype = '', bool $recurse = true): bool {
441  if (empty($view)) {
442  return false;
443  }
444 
445  // Detect view type
446  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
447  $viewtype = $this->getViewtype();
448  }
449 
450 
451  $file = $this->findViewFile($view, $viewtype);
452  if ($file) {
453  return true;
454  }
455 
456  // If we got here then check whether this exists as an extension
457  // We optionally recursively check whether the extended view exists also for the viewtype
458  if ($recurse && isset($this->extensions[$view])) {
459  foreach ($this->extensions[$view] as $view_extension) {
460  // do not recursively check to stay away from infinite loops
461  if ($this->viewExists($view_extension, $viewtype, false)) {
462  return true;
463  }
464  }
465  }
466 
467  // Now check if the default view exists if the view is registered as a fallback
468  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
469  return $this->viewExists($view, 'default');
470  }
471 
472  return false;
473  }
474 
486  public function extendView(string $view, string $view_extension, int $priority = 501): void {
487  if ($view === $view_extension) {
488  // do not allow direct extension on self with self
489  return;
490  }
491 
492  if (!isset($this->extensions[$view])) {
493  $this->extensions[$view][self::BASE_VIEW_PRIORITY] = $view;
494  }
495 
496  // raise priority until it doesn't match one already registered
497  while (isset($this->extensions[$view][$priority])) {
498  $priority++;
499  }
500 
501  $this->extensions[$view][$priority] = $view_extension;
502  ksort($this->extensions[$view]);
503  }
504 
515  public function unextendView(string $view, string $view_extension): bool {
516  if (!isset($this->extensions[$view])) {
517  return false;
518  }
519 
520  $extensions = $this->extensions[$view];
521  unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
522 
523  $priority = array_search($view_extension, $extensions);
524  if ($priority === false) {
525  return false;
526  }
527 
528  unset($this->extensions[$view][$priority]);
529 
530  return true;
531  }
532 
540  public function registerViewsFromPath(string $path): bool {
541  $path = Paths::sanitize($path) . 'views/';
542 
543  // do not fail on non existing views folder
544  if (!is_dir($path)) {
545  return true;
546  }
547 
548  try {
549  $dir = new \DirectoryIterator($path);
550  } catch (\Throwable $t) {
551  $this->getLogger()->error($t->getMessage());
552  return false;
553  }
554 
555  foreach ($dir as $folder) {
556  $folder_name = $folder->getBasename();
557  if (!$folder->isDir() || str_starts_with($folder_name, '.')) {
558  continue;
559  }
560 
561  if (!$this->autoregisterViews('', $folder->getPathname(), $folder_name)) {
562  return false;
563  }
564  }
565 
566  return true;
567  }
568 
579  public function mergeViewsSpec(array $spec): void {
580  foreach ($spec as $viewtype => $list) {
581  foreach ($list as $view => $paths) {
582  if (!is_array($paths)) {
583  $paths = [$paths];
584  }
585 
586  foreach ($paths as $path) {
587  if (!preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
588  // relative path
589  $path = Paths::project() . $path;
590  }
591 
592  if (str_ends_with($view, '/')) {
593  // prefix
594  $this->autoregisterViews($view, $path, $viewtype);
595  } else {
596  $this->setViewLocation($view, $viewtype, $path);
597  }
598  }
599  }
600  }
601  }
602 
610  public function listViews(string $viewtype = 'default'): array {
611  return array_keys($this->locations[$viewtype] ?? []);
612  }
613 
619  public function getInspectorData(): array {
620  $cached_overrides = $this->server_cache->load('view_overrides');
621 
622  return [
623  'locations' => $this->locations,
624  'overrides' => is_array($cached_overrides) ? $cached_overrides : $this->overrides,
625  'extensions' => $this->extensions,
626  'simplecache' => _elgg_services()->simpleCache->getCacheableViews(),
627  ];
628  }
629 
635  public function configureFromCache(): void {
636  if (!$this->server_cache->isEnabled()) {
637  return;
638  }
639 
640  $data = $this->server_cache->load('view_locations');
641  if (!is_array($data)) {
642  return;
643  }
644 
645  $this->locations = $data['locations'];
646  $this->locations_loaded_from_cache = true;
647  }
648 
654  public function cacheConfiguration(): void {
655  if (!$this->server_cache->isEnabled()) {
656  return;
657  }
658 
659  // only cache if not already loaded
660  if ($this->isViewLocationsLoadedFromCache()) {
661  return;
662  }
663 
664  if (empty($this->locations)) {
665  $this->server_cache->delete('view_locations');
666  return;
667  }
668 
669  $this->server_cache->save('view_locations', ['locations' => $this->locations]);
670 
671  // this is saved just for the inspector and is not loaded in loadAll()
672  $this->server_cache->save('view_overrides', $this->overrides);
673  }
674 
681  public function isViewLocationsLoadedFromCache(): bool {
682  return $this->locations_loaded_from_cache;
683  }
684 
690  public function getESModules(): array {
691  $modules = $this->server_cache->load('esmodules');
692  if (is_array($modules)) {
693  return $modules;
694  }
695 
696  $modules = [];
697  foreach ($this->locations['default'] as $name => $path) {
698  if (!str_ends_with($name, '.mjs')) {
699  continue;
700  }
701 
702  $modules[] = $name;
703  }
704 
705  $this->server_cache->save('esmodules', $modules);
706 
707  return $modules;
708  }
709 
719  protected function setViewLocation(string $view, string $viewtype, string $path): void {
720  $path = strtr($path, '\\', '/');
721 
722  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
723  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
724  }
725 
726  $this->locations[$viewtype][$view] = $path;
727 
728  // Test if view is cacheable and push it to the cacheable views stack,
729  // if it's not registered as cacheable explicitly
730  _elgg_services()->simpleCache->isCacheableView($view);
731  }
732 }
$content
Set robots.txt action.
Definition: set_robots.php:6
$vars
Definition: theme.php:3
if(! $user||! $user->canDelete()) $name
Definition: delete.php:22
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
$params
Saves global plugin settings.
Definition: save.php:13
return[ 'admin/delete_admin_notices'=>['access'=> 'admin'], 'admin/menu/save'=>['access'=> 'admin'], 'admin/plugins/activate'=>['access'=> 'admin'], 'admin/plugins/activate_all'=>['access'=> 'admin'], 'admin/plugins/deactivate'=>['access'=> 'admin'], 'admin/plugins/deactivate_all'=>['access'=> 'admin'], 'admin/plugins/set_priority'=>['access'=> 'admin'], 'admin/security/security_txt'=>['access'=> 'admin'], 'admin/security/settings'=>['access'=> 'admin'], 'admin/security/regenerate_site_secret'=>['access'=> 'admin'], 'admin/site/cache/clear'=>['access'=> 'admin'], 'admin/site/cache/invalidate'=>['access'=> 'admin'], 'admin/site/icons'=>['access'=> 'admin'], 'admin/site/set_maintenance_mode'=>['access'=> 'admin'], 'admin/site/set_robots'=>['access'=> 'admin'], 'admin/site/theme'=>['access'=> 'admin'], 'admin/site/unlock_upgrade'=>['access'=> 'admin'], 'admin/site/settings'=>['access'=> 'admin'], 'admin/upgrade'=>['access'=> 'admin'], 'admin/upgrade/reset'=>['access'=> 'admin'], 'admin/user/ban'=>['access'=> 'admin'], 'admin/user/bulk/ban'=>['access'=> 'admin'], 'admin/user/bulk/delete'=>['access'=> 'admin'], 'admin/user/bulk/unban'=>['access'=> 'admin'], 'admin/user/bulk/validate'=>['access'=> 'admin'], 'admin/user/change_email'=>['access'=> 'admin'], 'admin/user/delete'=>['access'=> 'admin'], 'admin/user/login_as'=>['access'=> 'admin'], 'admin/user/logout_as'=>[], 'admin/user/makeadmin'=>['access'=> 'admin'], 'admin/user/resetpassword'=>['access'=> 'admin'], 'admin/user/removeadmin'=>['access'=> 'admin'], 'admin/user/unban'=>['access'=> 'admin'], 'admin/user/validate'=>['access'=> 'admin'], 'annotation/delete'=>[], 'avatar/upload'=>[], 'comment/save'=>[], 'diagnostics/download'=>['access'=> 'admin', 'controller'=> \Elgg\Diagnostics\DownloadController::class,], 'entity/chooserestoredestination'=>[], 'entity/delete'=>[], 'entity/mute'=>[], 'entity/restore'=>[], 'entity/subscribe'=>[], 'entity/trash'=>[], 'entity/unmute'=>[], 'entity/unsubscribe'=>[], 'login'=>['access'=> 'logged_out'], 'logout'=>[], 'notifications/mute'=>['access'=> 'public'], 'plugins/settings/remove'=>['access'=> 'admin'], 'plugins/settings/save'=>['access'=> 'admin'], 'plugins/usersettings/save'=>[], 'register'=>['access'=> 'logged_out', 'middleware'=>[\Elgg\Router\Middleware\RegistrationAllowedGatekeeper::class,],], 'river/delete'=>[], 'settings/notifications'=>[], 'settings/notifications/subscriptions'=>[], 'user/changepassword'=>['access'=> 'public'], 'user/requestnewpassword'=>['access'=> 'public'], 'useradd'=>['access'=> 'admin'], 'usersettings/save'=>[], 'widgets/add'=>[], 'widgets/delete'=>[], 'widgets/move'=>[], 'widgets/save'=>[],]
Definition: actions.php:76
if(! $entity instanceof \ElggUser) $data
Definition: attributes.php:13
$paths
We handle here two possible locations of composer-generated autoload file.
Definition: autoloader.php:7
foreach( $paths as $path)
Definition: autoloader.php:12
load(stdClass $row)
Loads attributes from the entities table into the object.
Definition: ElggEntity.php:824
Events service.
Elgg HTTP request.
Definition: Request.php:17
Find Elgg and project paths.
Definition: Paths.php:8
Views service.
viewExists(string $view, string $viewtype='', bool $recurse=true)
Returns whether the specified view exists.
getViewList(string $view)
Get the views, including extensions, used to render a view.
findViewFile(string $view, string $viewtype)
Find the view file.
getESModules()
Returns an array of names of ES modules detected based on view location.
extendView(string $view, string $view_extension, int $priority=501)
Extends a view with another view.
getViewtype()
Get the viewtype.
fileExists(string $path)
Wrapper for file_exists() that caches false results (the stat cache only caches true results).
renderView(string $view, array $vars=[], string $viewtype='', ?bool $issue_missing_notice=null, array $extensions_tree=[])
Renders a view.
isViewLocationsLoadedFromCache()
Checks if view_locations have been loaded from cache.
registerViewsFromPath(string $path)
Register all views in a given path.
configureFromCache()
Configure locations from the cache.
registerCoreViews()
Discover the core views if the system cache did not load.
setViewLocation(string $view, string $viewtype, string $path)
Update the location of a view file.
renderDeprecatedView(string $view, array $vars, string $suggestion, string $version)
Display a view with a deprecation notice.
listViews(string $viewtype='default')
List all views in a viewtype.
mergeViewsSpec(array $spec)
Merge a specification of absolute view paths.
__construct(protected EventsService $events, protected HttpRequest $request, protected Config $config, protected ServerCache $server_cache)
Constructor.
registerViewtypeFallback(string $viewtype)
Register a viewtype to fall back to a default view if a view isn't found for that viewtype.
unextendView(string $view, string $view_extension)
Unextends a view.
doesViewtypeFallback(string $viewtype)
Checks if a viewtype falls back to default.
cacheConfiguration()
Cache the configuration.
isValidViewtype(string $viewtype)
Checks if $viewtype is a string suitable for use as a viewtype name.
setViewtype(string $viewtype='')
Set the viewtype.
getInspectorData()
Get inspector data.
renderViewFile(string $view, array $vars, string $viewtype, bool $issue_missing_notice)
Includes view PHP or static file.
autoregisterViews(string $view_base, string $folder, string $viewtype)
Auto-registers views from a location.
resolveViewtype()
Resolve the initial viewtype.
elgg_get_data_path()
Get the data directory path for this installation, ending with slash.
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
$version
$extensions
elgg()
Bootstrapping and helper procedural code available for use in Elgg core and plugins.
Definition: elgglib.php:12
_elgg_services()
Get the global service provider.
Definition: elgglib.php:347
$viewtype
Definition: default.php:11
$request
Definition: livesearch.php:12
string project
Definition: conf.py:52
list extensions
Definition: conf.py:34
$path
Definition: details.php:70
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
$priority