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 
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 }
elgg
Definition: install.js:27
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.
registerCoreViews()
Discover the core views if the system cache did not load.
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.
Definition: ElggEntity.php:824
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.
__construct(protected EventsService $events, protected HttpRequest $request, protected Config $config, protected ServerCache $server_cache)
Constructor.
$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.
elgg_get_data_path()
Get the data directory path for this installation, ending with slash.
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