Elgg  Version master
ViewsService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
11 
18 class ViewsService {
19 
20  use Loggable;
21 
22  const VIEW_HOOK = 'view';
23  const VIEW_VARS_HOOK = 'view_vars';
24  const OUTPUT_KEY = '__view_output';
25  const BASE_VIEW_PRIORITY = 500;
26 
31  protected $file_exists_cache = [];
32 
38  private $locations = [];
39 
45  private $overrides = [];
46 
52  private $simplecache_views = [];
53 
59  private $extensions = [];
60 
64  private $fallbacks = [];
65 
69  private $events;
70 
74  private $cache;
75 
79  private $request;
80 
84  private $viewtype;
85 
92  public function __construct(EventsService $events, HttpRequest $request) {
93  $this->events = $events;
94  $this->request = $request;
95  }
96 
104  public function setViewtype(string $viewtype = ''): bool {
105  if (!$viewtype) {
106  $this->viewtype = null;
107 
108  return true;
109  }
110 
111  if ($this->isValidViewtype($viewtype)) {
112  $this->viewtype = $viewtype;
113 
114  return true;
115  }
116 
117  return false;
118  }
119 
125  public function getViewtype(): string {
126  if (!isset($this->viewtype)) {
127  $this->viewtype = $this->resolveViewtype();
128  }
129 
130  return $this->viewtype;
131  }
132 
138  public function clampViewtypeToPopulatedViews(): void {
139  $viewtype = $this->getViewtype();
140  if (empty($this->locations[$viewtype])) {
141  $this->viewtype = 'default';
142  }
143  }
144 
150  private function resolveViewtype(): string {
151  if ($this->request) {
152  $view = $this->request->getParam('view', '', false);
153  if ($this->isValidViewtype($view)) {
154  return $view;
155  }
156  }
157 
158  $view = (string) elgg_get_config('view');
159  if ($this->isValidViewtype($view)) {
160  return $view;
161  }
162 
163  return 'default';
164  }
165 
173  public function isValidViewtype(string $viewtype): bool {
174  if ($viewtype === '') {
175  return false;
176  }
177 
178  if (preg_match('/\W/', $viewtype)) {
179  return false;
180  }
181 
182  return true;
183  }
184 
192  public static function canonicalizeViewName(string $alias): string {
193 
194  $canonical = $alias;
195 
196  $extension = pathinfo($canonical, PATHINFO_EXTENSION);
197  $hasValidFileExtension = isset(CacheHandler::$extensions[$extension]);
198 
199  if (str_starts_with($canonical, 'js/')) {
200  $canonical = substr($canonical, 3);
201  if (!$hasValidFileExtension) {
202  $canonical .= '.js';
203  }
204  } else if (str_starts_with($canonical, 'css/')) {
205  $canonical = substr($canonical, 4);
206  if (!$hasValidFileExtension) {
207  $canonical .= '.css';
208  }
209  }
210 
211  return $canonical;
212  }
213 
223  public function autoregisterViews(string $view_base, string $folder, string $viewtype): bool {
224  $folder = rtrim($folder, '/\\');
225  $view_base = rtrim($view_base, '/\\');
226 
227  if (!is_dir($folder) || !is_readable($folder)) {
228  $this->getLogger()->notice("Unable to register views from the directory: {$folder}");
229  return false;
230  }
231 
232  try {
233  $dir = new \DirectoryIterator($folder);
234  } catch (\Exception $e) {
235  $this->getLogger()->error($e->getMessage());
236  return false;
237  }
238 
239  $view_base_new = '';
240  if (!empty($view_base)) {
241  $view_base_new = $view_base . '/';
242  }
243 
244  /* @var $fileinfo \SplFileInfo */
245  foreach ($dir as $fileinfo) {
246  if ($fileinfo->isDot()) {
247  continue;
248  }
249 
250  $path = $fileinfo->getPathname();
251 
252  if ($fileinfo->isDir()) {
253  // Found a directory so go deeper
254  $this->autoregisterViews($view_base_new . $fileinfo->getFilename(), $path, $viewtype);
255  continue;
256  }
257 
258  // found a file add it to the views
259  $view = $view_base_new . $fileinfo->getBasename('.php');
260  $this->setViewLocation($view, $viewtype, $path);
261  }
262 
263  return true;
264  }
265 
274  public function findViewFile(string $view, string $viewtype): string {
275  if (!isset($this->locations[$viewtype][$view])) {
276  return '';
277  }
278 
279  $path = $this->locations[$viewtype][$view];
280  if ($this->fileExists($path)) {
281  return $path;
282  }
283 
284  return '';
285  }
286 
298  public function setViewDir(string $view, string $location, string $viewtype = ''): void {
299  $view = self::canonicalizeViewName($view);
300 
301  if (empty($viewtype)) {
302  $viewtype = 'default';
303  }
304 
305  $location = rtrim($location, '/\\');
306 
307  if ($this->fileExists("$location/$viewtype/$view.php")) {
308  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view.php");
309  } else if ($this->fileExists("$location/$viewtype/$view")) {
310  $this->setViewLocation($view, $viewtype, "$location/$viewtype/$view");
311  }
312  }
313 
324  public function registerViewtypeFallback(string $viewtype): void {
325  $this->fallbacks[] = $viewtype;
326  }
327 
335  public function doesViewtypeFallback(string $viewtype): bool {
336  return in_array($viewtype, $this->fallbacks);
337  }
338 
351  public function renderDeprecatedView(string $view, array $vars, string $suggestion, string $version): string {
352  $view = self::canonicalizeViewName($view);
353 
354  $rendered = $this->renderView($view, $vars, '', false);
355  if ($rendered) {
356  $this->logDeprecatedMessage("The '{$view}' view has been deprecated. {$suggestion}", $version);
357  }
358 
359  return $rendered;
360  }
361 
371  public function getViewList(string $view): array {
372  return $this->extensions[$view] ?? [self::BASE_VIEW_PRIORITY => $view];
373  }
374 
388  public function renderView(string $view, array $vars = [], string $viewtype = '', bool $issue_missing_notice = true, array $extensions_tree = []): string {
389  $view = self::canonicalizeViewName($view);
390 
391  // basic checking for bad paths
392  if (str_contains($view, '..')) {
393  return '';
394  }
395 
396  // check for extension deadloops
397  if (in_array($view, $extensions_tree)) {
398  $this->getLogger()->error("View $view is detected as an extension of itself. This is not allowed");
399 
400  return '';
401  }
402 
403  $extensions_tree[] = $view;
404 
405  // Get the current viewtype
406  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
407  $viewtype = $this->getViewtype();
408  }
409 
410  // allow altering $vars
411  $vars_event_params = [
412  'view' => $view,
413  'vars' => $vars,
414  'viewtype' => $viewtype,
415  ];
416  $vars = $this->events->triggerResults(self::VIEW_VARS_HOOK, $view, $vars_event_params, $vars);
417 
418  // allow $vars to hijack output
419  if (isset($vars[self::OUTPUT_KEY])) {
420  return (string) $vars[self::OUTPUT_KEY];
421  }
422 
423  $viewlist = $this->getViewList($view);
424 
425  $content = '';
426  foreach ($viewlist as $priority => $view_name) {
427  if ($priority !== self::BASE_VIEW_PRIORITY) {
428  // the others are extensions
429  $content .= $this->renderView($view_name, $vars, $viewtype, $issue_missing_notice, $extensions_tree);
430  continue;
431  }
432 
433  // actual rendering of a single view
434  $rendering = $this->renderViewFile($view_name, $vars, $viewtype, $issue_missing_notice);
435  if ($rendering !== false) {
436  $content .= $rendering;
437  continue;
438  }
439 
440  // attempt to load default view
441  if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
442  $rendering = $this->renderViewFile($view_name, $vars, 'default', $issue_missing_notice);
443  if ($rendering !== false) {
444  $content .= $rendering;
445  }
446  }
447  }
448 
449  $params = [
450  'view' => $view,
451  'vars' => $vars,
452  'viewtype' => $viewtype,
453  ];
454 
455  return (string) $this->events->triggerResults(self::VIEW_HOOK, $view, $params, $content);
456  }
457 
466  protected function fileExists(string $path): bool {
467  if (!isset($this->file_exists_cache[$path])) {
468  $this->file_exists_cache[$path] = file_exists($path);
469  }
470 
471  return $this->file_exists_cache[$path];
472  }
473 
484  private function renderViewFile(string $view, array $vars, string $viewtype, bool $issue_missing_notice): string|false {
485  $file = $this->findViewFile($view, $viewtype);
486  if (!$file) {
487  if ($issue_missing_notice) {
488  $this->getLogger()->notice("$viewtype/$view view does not exist.");
489  }
490 
491  return false;
492  }
493 
494  if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
495  ob_start();
496 
497  try {
498  // don't isolate, scripts use the local $vars
499  include $file;
500 
501  return ob_get_clean();
502  } catch (\Exception $e) {
503  ob_get_clean();
504  throw $e;
505  }
506  }
507 
508  return file_get_contents($file);
509  }
510 
522  public function viewExists(string $view, string $viewtype = '', bool $recurse = true): bool {
523  $view = self::canonicalizeViewName($view);
524 
525  if (empty($view)) {
526  return false;
527  }
528 
529  // Detect view type
530  if ($viewtype === '' || !$this->isValidViewtype($viewtype)) {
531  $viewtype = $this->getViewtype();
532  }
533 
534 
535  $file = $this->findViewFile($view, $viewtype);
536  if ($file) {
537  return true;
538  }
539 
540  // If we got here then check whether this exists as an extension
541  // We optionally recursively check whether the extended view exists also for the viewtype
542  if ($recurse && isset($this->extensions[$view])) {
543  foreach ($this->extensions[$view] as $view_extension) {
544  // do not recursively check to stay away from infinite loops
545  if ($this->viewExists($view_extension, $viewtype, false)) {
546  return true;
547  }
548  }
549  }
550 
551  // Now check if the default view exists if the view is registered as a fallback
552  if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
553  return $this->viewExists($view, 'default');
554  }
555 
556  return false;
557  }
558 
570  public function extendView(string $view, string $view_extension, int $priority = 501): void {
571  $view = self::canonicalizeViewName($view);
572  $view_extension = self::canonicalizeViewName($view_extension);
573 
574  if ($view === $view_extension) {
575  // do not allow direct extension on self with self
576  return;
577  }
578 
579  if (!isset($this->extensions[$view])) {
580  $this->extensions[$view][self::BASE_VIEW_PRIORITY] = (string) $view;
581  }
582 
583  // raise priority until it doesn't match one already registered
584  while (isset($this->extensions[$view][$priority])) {
585  $priority++;
586  }
587 
588  $this->extensions[$view][$priority] = (string) $view_extension;
589  ksort($this->extensions[$view]);
590  }
591 
602  public function unextendView(string $view, string $view_extension): bool {
603  $view = self::canonicalizeViewName($view);
604  $view_extension = self::canonicalizeViewName($view_extension);
605 
606  if (!isset($this->extensions[$view])) {
607  return false;
608  }
609 
610  $extensions = $this->extensions[$view];
611  unset($extensions[self::BASE_VIEW_PRIORITY]); // we do not want the base view to be removed from the list
612 
613  $priority = array_search($view_extension, $extensions);
614  if ($priority === false) {
615  return false;
616  }
617 
618  unset($this->extensions[$view][$priority]);
619 
620  return true;
621  }
622 
630  public function registerCacheableView(string $view): void {
631  $view = self::canonicalizeViewName($view);
632 
633  $this->simplecache_views[$view] = true;
634  }
635 
643  public function isCacheableView(string $view): bool {
644  $view = self::canonicalizeViewName($view);
645  if (isset($this->simplecache_views[$view])) {
646  return true;
647  }
648 
649  // build list of viewtypes to check
650  $current_viewtype = $this->getViewtype();
651  $viewtypes = [$current_viewtype];
652 
653  if ($this->doesViewtypeFallback($current_viewtype) && $current_viewtype != 'default') {
654  $viewtypes[] = 'default';
655  }
656 
657  // If a static view file is found in any viewtype, it's considered cacheable
658  foreach ($viewtypes as $viewtype) {
659  $file = $this->findViewFile($view, $viewtype);
660 
661  if ($file && pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
662  $this->simplecache_views[$view] = true;
663 
664  return true;
665  }
666  }
667 
668  // Assume not-cacheable by default
669  return false;
670  }
671 
679  public function registerPluginViews(string $path): bool {
680  $path = Paths::sanitize($path);
681  $view_dir = "{$path}views/";
682 
683  // plugins don't have to have views.
684  if (!is_dir($view_dir)) {
685  return true;
686  }
687 
688  // but if they do, they have to be readable
689  $handle = opendir($view_dir);
690  if ($handle === false) {
691  $this->getLogger()->notice("Unable to register views from the directory: {$view_dir}");
692 
693  return false;
694  }
695 
696  while (($view_type = readdir($handle)) !== false) {
697  $view_type_dir = $view_dir . $view_type;
698 
699  if (!str_starts_with($view_type, '.') && is_dir($view_type_dir)) {
700  if (!$this->autoregisterViews('', $view_type_dir, $view_type)) {
701  return false;
702  }
703  }
704  }
705 
706  return true;
707  }
708 
719  public function mergeViewsSpec(array $spec): void {
720  foreach ($spec as $viewtype => $list) {
721  foreach ($list as $view => $paths) {
722  if (!is_array($paths)) {
723  $paths = [$paths];
724  }
725 
726  foreach ($paths as $path) {
727  if (!preg_match('~^([/\\\\]|[a-zA-Z]\:)~', $path)) {
728  // relative path
729  $path = Directory\Local::projectRoot()->getPath($path);
730  }
731 
732  if (str_ends_with($view, '/')) {
733  // prefix
734  $this->autoregisterViews($view, $path, $viewtype);
735  } else {
736  $this->setViewLocation($view, $viewtype, $path);
737  }
738  }
739  }
740  }
741  }
742 
750  public function listViews(string $viewtype = 'default'): array {
751  if (empty($this->locations[$viewtype])) {
752  return [];
753  }
754 
755  return array_keys($this->locations[$viewtype]);
756  }
757 
763  public function getInspectorData(): array {
764  $overrides = $this->overrides;
765 
766  if ($this->cache) {
767  $data = $this->cache->load('view_overrides');
768  if (is_array($data)) {
769  $overrides = $data;
770  }
771  }
772 
773  return [
774  'locations' => $this->locations,
775  'overrides' => $overrides,
776  'extensions' => $this->extensions,
777  'simplecache' => $this->simplecache_views,
778  ];
779  }
780 
788  public function configureFromCache(SystemCache $cache): bool {
789  $data = $cache->load('view_locations');
790  if (!is_array($data)) {
791  return false;
792  }
793 
794  // format changed, check version
795  if (empty($data['version']) || $data['version'] !== '2.0') {
796  return false;
797  }
798 
799  $this->locations = $data['locations'];
800  $this->cache = $cache;
801 
802  return true;
803  }
804 
812  public function cacheConfiguration(SystemCache $cache): void {
813  $cache->save('view_locations', [
814  'version' => '2.0',
815  'locations' => $this->locations,
816  ]);
817 
818  // this is saved just for the inspector and is not loaded in loadAll()
819  $cache->save('view_overrides', $this->overrides);
820  }
821 
831  private function setViewLocation(string $view, string $viewtype, string $path): void {
832  $view = self::canonicalizeViewName($view);
833  $path = strtr($path, '\\', '/');
834 
835  if (isset($this->locations[$viewtype][$view]) && $path !== $this->locations[$viewtype][$view]) {
836  $this->overrides[$viewtype][$view][] = $this->locations[$viewtype][$view];
837  }
838 
839  $this->locations[$viewtype][$view] = $path;
840 
841  // Test if view is cacheable and push it to the cacheable views stack,
842  // if it's not registered as cacheable explicitly
843  $this->isCacheableView($view);
844  }
845 }
save(string $type, $data, int $expire_after=null)
Saves a system cache.
Definition: SystemCache.php:52
list extensions
Definition: conf.py:31
viewExists(string $view, string $viewtype= '', bool $recurse=true)
Returns whether the specified view exists.
isCacheableView(string $view)
Is the view cacheable.
$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
isValidViewtype(string $viewtype)
Checks if $viewtype is a string suitable for use as a viewtype name.
elgg_get_config(string $name, $default=null)
Get an Elgg configuration value.
listViews(string $viewtype= 'default')
List all views in a viewtype.
cacheConfiguration(SystemCache $cache)
Cache the configuration.
$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.
setViewDir(string $view, string $location, string $viewtype= '')
Set an alternative base location for a view.
renderView(string $view, array $vars=[], string $viewtype= '', bool $issue_missing_notice=true, array $extensions_tree=[])
Renders a view.
$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.
load(string $type)
Retrieve the contents of a system cache.
Definition: SystemCache.php:67
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
static canonicalizeViewName(string $alias)
Takes a view name and returns the canonical name for that view.
load(stdClass $row)
Loads attributes from the entities table into the object.
clampViewtypeToPopulatedViews()
If the current viewtype has no views, reset it to "default".
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.
__construct(EventsService $events, HttpRequest $request)
Constructor.
$viewtype
Definition: default.php:11
$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.
$location
Definition: member.php:29
configureFromCache(SystemCache $cache)
Configure locations from the cache.
$content
Set robots.txt action.
Definition: set_robots.php:6
$vars['head']
Definition: html.php:24
autoregisterViews(string $view_base, string $folder, string $viewtype)
Auto-registers views from a location.
registerCacheableView(string $view)
Register a view a cacheable.
fileExists(string $path)
Wrapper for file_exists() that caches false results (the stat cache only caches true results)...
$priority
registerPluginViews(string $path)
Register a plugin&#39;s views.
$extension
Definition: default.php:25
logDeprecatedMessage(string $message, string $version)
Sends a message about deprecated use of a function, view, etc.
Definition: Loggable.php:80