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