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/otfs/'] = elgg_get_data_path() . 'fontawesome/webfont/otfs/';
174  $spec['default']['font-awesome/webfonts/'] = elgg_get_data_path() . 'fontawesome/webfont/webfonts/';
175  }
176 
177  $this->mergeViewsSpec($spec);
178  }
179  }
180 
190  public function autoregisterViews(string $view_base, string $folder, string $viewtype): bool {
191  $folder = Paths::sanitize($folder);
192  $view_base = Paths::sanitize($view_base, false);
193  $view_base = $view_base ? $view_base . '/' : $view_base;
194 
195  try {
196  $dir = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS));
197  } catch (\Throwable $t) {
198  $this->getLogger()->error($t->getMessage());
199  return false;
200  }
201 
202  /* @var $file \SplFileInfo */
203  foreach ($dir as $file) {
204  $path = $file->getPath() . '/' . $file->getBasename('.php');
205  $path = Paths::sanitize($path, false);
206 
207  // found a file add it to the views
208  $view = $view_base . substr($path, strlen($folder));
209  $this->setViewLocation($view, $viewtype, $file->getPathname());
210  }
211 
212  return true;
213  }
214 
223  public function findViewFile(string $view, string $viewtype): string {
224  if (!isset($this->locations[$viewtype][$view])) {
225  return '';
226  }
227 
228  $path = $this->locations[$viewtype][$view];
229 
230  return $this->fileExists($path) ? $path : '';
231  }
232 
243  public function registerViewtypeFallback(string $viewtype): void {
244  $this->fallbacks[] = $viewtype;
245  }
246 
254  public function doesViewtypeFallback(string $viewtype): bool {
255  return in_array($viewtype, $this->fallbacks);
256  }
257 
270  public function renderDeprecatedView(string $view, array $vars, string $suggestion, string $version): string {
271  $rendered = $this->renderView($view, $vars, '', false);
272  if ($rendered) {
273  $this->logDeprecatedMessage("The '{$view}' view has been deprecated. {$suggestion}", $version);
274  }
275 
276  return $rendered;
277  }
278 
288  public function getViewList(string $view): array {
289  return $this->extensions[$view] ?? [self::BASE_VIEW_PRIORITY => $view];
290  }
291 
305  public function renderView(string $view, array $vars = [], string $viewtype = '', ?bool $issue_missing_notice = null, array $extensions_tree = []): string {
306  // basic checking for bad paths
307  if (str_contains($view, '..')) {
308  return '';
309  }
310 
311  // check for extension deadloops
312  if (in_array($view, $extensions_tree)) {
313  $this->getLogger()->error("View {$view} is detected as an extension of itself. This is not allowed");
314 
315  return '';
316  }
317 
318  $extensions_tree[] = $view;
319 
320  // Get the current viewtype
321  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
322  $viewtype = $this->getViewtype();
323  }
324 
325  if (!isset($issue_missing_notice)) {
326  $issue_missing_notice = $viewtype === 'default';
327  }
328 
329  // allow altering $vars
330  $vars_event_params = [
331  'view' => $view,
332  'vars' => $vars,
333  'viewtype' => $viewtype,
334  ];
335  $vars = $this->events->triggerResults(self::VIEW_VARS_HOOK, $view, $vars_event_params, $vars);
336 
337  // allow $vars to hijack output
338  if (isset($vars[self::OUTPUT_KEY])) {
339  return (string) $vars[self::OUTPUT_KEY];
340  }
341 
342  $viewlist = $this->getViewList($view);
343 
344  $content = '';
345  foreach ($viewlist as $priority => $view_name) {
346  if ($priority !== self::BASE_VIEW_PRIORITY) {
347  // the others are extensions
348  $content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
349  continue;
350  }
351 
352  // actual rendering of a single view
353  $rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
354  if ($rendering !== false) {
355  $content .= $rendering;
356  continue;
357  }
358 
359  // attempt to load default view
360  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
361  $rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
362  if ($rendering !== false) {
363  $content .= $rendering;
364  }
365  }
366  }
367 
368  $params = [
369  'view' => $view,
370  'vars' => $vars,
371  'viewtype' => $viewtype,
372  ];
373 
374  return (string) $this->events->triggerResults(self::VIEW_HOOK, $view, $params, $content);
375  }
376 
385  protected function fileExists(string $path): bool {
386  if (!isset($this->file_exists_cache[$path])) {
387  $this->file_exists_cache[$path] = file_exists($path);
388  }
389 
390  return $this->file_exists_cache[$path];
391  }
392 
403  protected function renderViewFile(string $view, array $vars, string $viewtype, bool $issue_missing_notice): string|false {
404  $file = $this->findViewFile($view, $viewtype);
405  if (!$file) {
406  if ($issue_missing_notice) {
407  $this->getLogger()->notice("{$viewtype}/{$view} view does not exist.");
408  }
409 
410  return false;
411  }
412 
413  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
414  ob_start();
415 
416  try {
417  // don't isolate, scripts use the local $vars
418  include $file;
419 
420  return ob_get_clean();
421  } catch (\Exception $e) {
422  ob_get_clean();
423  throw $e;
424  }
425  }
426 
427  return file_get_contents($file);
428  }
429 
441  public function viewExists(string $view, string $viewtype = '', bool $recurse = true): bool {
442  if (empty($view)) {
443  return false;
444  }
445 
446  // Detect view type
447  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
448  $viewtype = $this->getViewtype();
449  }
450 
451 
452  $file = $this->findViewFile($view, $viewtype);
453  if ($file) {
454  return true;
455  }
456 
457  // If we got here then check whether this exists as an extension
458  // We optionally recursively check whether the extended view exists also for the viewtype
459  if ($recurse && isset($this->extensions[$view])) {
460  foreach ($this->extensions[$view] as $view_extension) {
461  // do not recursively check to stay away from infinite loops
462  if ($this->viewExists($view_extension, $viewtype, false)) {
463  return true;
464  }
465  }
466  }
467 
468  // Now check if the default view exists if the view is registered as a fallback
469  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
470  return $this->viewExists($view, 'default');
471  }
472 
473  return false;
474  }
475 
487  public function extendView(string $view, string $view_extension, int $priority = 501): void {
488  if ($view === $view_extension) {
489  // do not allow direct extension on self with self
490  return;
491  }
492 
493  if (!isset($this->extensions[$view])) {
494  $this->extensions[$view][self::BASE_VIEW_PRIORITY] = $view;
495  }
496 
497  // raise priority until it doesn't match one already registered
498  while (isset($this->extensions[$view][$priority])) {
499  $priority++;
500  }
501 
502  $this->extensions[$view][$priority] = $view_extension;
503  ksort($this->extensions[$view]);
504  }
505 
516  public function unextendView(string $view, string $view_extension): bool {
517  if (!isset($this->extensions[$view])) {
518  return false;
519  }
520 
521  $extensions = $this->extensions[$view];
522  unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
523 
524  $priority = array_search($view_extension, $extensions);
525  if ($priority === false) {
526  return false;
527  }
528 
529  unset($this->extensions[$view][$priority]);
530 
531  return true;
532  }
533 
541  public function registerViewsFromPath(string $path): bool {
542  $path = Paths::sanitize($path) . 'views/';
543 
544  // do not fail on non existing views folder
545  if (!is_dir($path)) {
546  return true;
547  }
548 
549  try {
550  $dir = new \DirectoryIterator($path);
551  } catch (\Throwable $t) {
552  $this->getLogger()->error($t->getMessage());
553  return false;
554  }
555 
556  foreach ($dir as $folder) {
557  $folder_name = $folder->getBasename();
558  if (!$folder->isDir() || str_starts_with($folder_name, '.')) {
559  continue;
560  }
561 
562  if (!$this->autoregisterViews('', $folder->getPathname(), $folder_name)) {
563  return false;
564  }
565  }
566 
567  return true;
568  }
569 
580  public function mergeViewsSpec(array $spec): void {
581  foreach ($spec as $viewtype => $list) {
582  foreach ($list as $view => $paths) {
583  if (!is_array($paths)) {
584  $paths = [$paths];
585  }
586 
587  foreach ($paths as $path) {
588  if (!preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
589  // relative path
590  $path = Paths::project() . $path;
591  }
592 
593  if (str_ends_with($view, '/')) {
594  // prefix
595  $this->autoregisterViews($view, $path, $viewtype);
596  } else {
597  $this->setViewLocation($view, $viewtype, $path);
598  }
599  }
600  }
601  }
602  }
603 
611  public function listViews(string $viewtype = 'default'): array {
612  return array_keys($this->locations[$viewtype] ?? []);
613  }
614 
620  public function getInspectorData(): array {
621  $cached_overrides = $this->server_cache->load('view_overrides');
622 
623  return [
624  'locations' => $this->locations,
625  'overrides' => is_array($cached_overrides) ? $cached_overrides : $this->overrides,
626  'extensions' => $this->extensions,
627  'simplecache' => _elgg_services()->simpleCache->getCacheableViews(),
628  ];
629  }
630 
636  public function configureFromCache(): void {
637  if (!$this->server_cache->isEnabled()) {
638  return;
639  }
640 
641  $data = $this->server_cache->load('view_locations');
642  if (!is_array($data)) {
643  return;
644  }
645 
646  $this->locations = $data['locations'];
647  $this->locations_loaded_from_cache = true;
648  }
649 
655  public function cacheConfiguration(): void {
656  if (!$this->server_cache->isEnabled()) {
657  return;
658  }
659 
660  // only cache if not already loaded
661  if ($this->isViewLocationsLoadedFromCache()) {
662  return;
663  }
664 
665  if (empty($this->locations)) {
666  $this->server_cache->delete('view_locations');
667  return;
668  }
669 
670  $this->server_cache->save('view_locations', ['locations' => $this->locations]);
671 
672  // this is saved just for the inspector and is not loaded in loadAll()
673  $this->server_cache->save('view_overrides', $this->overrides);
674  }
675 
682  public function isViewLocationsLoadedFromCache(): bool {
683  return $this->locations_loaded_from_cache;
684  }
685 
691  public function getESModules(): array {
692  $modules = $this->server_cache->load('esmodules');
693  if (is_array($modules)) {
694  return $modules;
695  }
696 
697  $modules = [];
698  foreach ($this->locations['default'] as $name => $path) {
699  if (!str_ends_with($name, '.mjs')) {
700  continue;
701  }
702 
703  $modules[] = $name;
704  }
705 
706  $this->server_cache->save('esmodules', $modules);
707 
708  return $modules;
709  }
710 
720  protected function setViewLocation(string $view, string $viewtype, string $path): void {
721  $path = strtr($path, '\\', '/');
722 
723  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
724  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
725  }
726 
727  $this->locations[$viewtype][$view] = $path;
728 
729  // Test if view is cacheable and push it to the cacheable views stack,
730  // if it's not registered as cacheable explicitly
731  _elgg_services()->simpleCache->isCacheableView($view);
732  }
733 }
$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:343
$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