ViewsService.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. <?php
  2. namespace Elgg;
  3. /**
  4. * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
  5. *
  6. * Use the elgg_* versions instead.
  7. *
  8. * @todo 1.10 remove deprecated view injections
  9. * @todo inject/remove dependencies: $CONFIG, hooks, site_url
  10. *
  11. * @access private
  12. *
  13. * @package Elgg.Core
  14. * @subpackage Views
  15. * @since 1.9.0
  16. */
  17. class ViewsService {
  18. protected $config_wrapper;
  19. protected $site_url_wrapper;
  20. protected $user_wrapper;
  21. protected $user_wrapped;
  22. /**
  23. * @see \Elgg\ViewsService::fileExists
  24. * @var array
  25. */
  26. protected $file_exists_cache = array();
  27. /**
  28. * Global Elgg configuration
  29. *
  30. * @var \stdClass
  31. */
  32. private $CONFIG;
  33. /**
  34. * @var PluginHooksService
  35. */
  36. private $hooks;
  37. /**
  38. * @var Logger
  39. */
  40. private $logger;
  41. /**
  42. * @var array
  43. */
  44. private $overriden_locations = array();
  45. /**
  46. * Constructor
  47. *
  48. * @param \Elgg\PluginHooksService $hooks The hooks service
  49. * @param \Elgg\Logger $logger Logger
  50. */
  51. public function __construct(\Elgg\PluginHooksService $hooks, \Elgg\Logger $logger) {
  52. global $CONFIG;
  53. $this->CONFIG = $CONFIG;
  54. $this->hooks = $hooks;
  55. $this->logger = $logger;
  56. }
  57. /**
  58. * Get the user object in a wrapper
  59. *
  60. * @return \Elgg\DeprecationWrapper|null
  61. */
  62. protected function getUserWrapper() {
  63. $user = _elgg_services()->session->getLoggedInUser();
  64. if ($user) {
  65. if ($user !== $this->user_wrapped) {
  66. $warning = 'Use elgg_get_logged_in_user_entity() rather than assuming elgg_view() '
  67. . 'populates $vars["user"]';
  68. $this->user_wrapper = new \Elgg\DeprecationWrapper($user, $warning, 1.8);
  69. $this->user_wrapped = $user;
  70. }
  71. $user = $this->user_wrapper;
  72. }
  73. return $user;
  74. }
  75. /**
  76. * @access private
  77. */
  78. public function autoregisterViews($view_base, $folder, $base_location_path, $viewtype) {
  79. $handle = opendir($folder);
  80. if ($handle) {
  81. while ($view = readdir($handle)) {
  82. if (!empty($view_base)) {
  83. $view_base_new = $view_base . "/";
  84. } else {
  85. $view_base_new = "";
  86. }
  87. if (substr($view, 0, 1) !== '.') {
  88. if (is_dir($folder . "/" . $view)) {
  89. $this->autoregisterViews($view_base_new . $view, $folder . "/" . $view,
  90. $base_location_path, $viewtype);
  91. } else {
  92. $this->setViewLocation($view_base_new . basename($view, '.php'),
  93. $base_location_path, $viewtype);
  94. }
  95. }
  96. }
  97. return true;
  98. }
  99. return false;
  100. }
  101. /**
  102. * @access private
  103. */
  104. public function getViewLocation($view, $viewtype = '') {
  105. if (empty($viewtype)) {
  106. $viewtype = elgg_get_viewtype();
  107. }
  108. if (!isset($this->CONFIG->views->locations[$viewtype][$view])) {
  109. if (!isset($this->CONFIG->viewpath)) {
  110. return dirname(dirname(dirname(__FILE__))) . "/views/";
  111. } else {
  112. return $this->CONFIG->viewpath;
  113. }
  114. } else {
  115. return $this->CONFIG->views->locations[$viewtype][$view];
  116. }
  117. }
  118. /**
  119. * @access private
  120. */
  121. public function setViewLocation($view, $location, $viewtype = '') {
  122. if (empty($viewtype)) {
  123. $viewtype = 'default';
  124. }
  125. if (!isset($this->CONFIG->views)) {
  126. $this->CONFIG->views = new \stdClass;
  127. }
  128. if (!isset($this->CONFIG->views->locations)) {
  129. $this->CONFIG->views->locations = array($viewtype => array($view => $location));
  130. } else if (!isset($this->CONFIG->views->locations[$viewtype])) {
  131. $this->CONFIG->views->locations[$viewtype] = array($view => $location);
  132. } else {
  133. if (isset($this->CONFIG->views->locations[$viewtype][$view])) {
  134. $this->overriden_locations[$viewtype][$view][] = $this->CONFIG->views->locations[$viewtype][$view];
  135. }
  136. $this->CONFIG->views->locations[$viewtype][$view] = $location;
  137. }
  138. }
  139. /**
  140. * @access private
  141. */
  142. public function registerViewtypeFallback($viewtype) {
  143. if (!isset($this->CONFIG->viewtype)) {
  144. $this->CONFIG->viewtype = new \stdClass;
  145. }
  146. if (!isset($this->CONFIG->viewtype->fallback)) {
  147. $this->CONFIG->viewtype->fallback = array();
  148. }
  149. $this->CONFIG->viewtype->fallback[] = $viewtype;
  150. }
  151. /**
  152. * @access private
  153. */
  154. public function doesViewtypeFallback($viewtype) {
  155. if (isset($this->CONFIG->viewtype) && isset($this->CONFIG->viewtype->fallback)) {
  156. return in_array($viewtype, $this->CONFIG->viewtype->fallback);
  157. }
  158. return false;
  159. }
  160. /**
  161. * Display a view with a deprecation notice. No missing view NOTICE is logged
  162. *
  163. * @see elgg_view()
  164. *
  165. * @param string $view The name and location of the view to use
  166. * @param array $vars Variables to pass to the view
  167. * @param string $suggestion Suggestion with the deprecation message
  168. * @param string $version Human-readable *release* version: 1.7, 1.8, ...
  169. *
  170. * @return string The parsed view
  171. * @access private
  172. */
  173. public function renderDeprecatedView($view, array $vars, $suggestion, $version) {
  174. $rendered = $this->renderView($view, $vars, false, '', false);
  175. if ($rendered) {
  176. elgg_deprecated_notice("The $view view has been deprecated. $suggestion", $version, 3);
  177. }
  178. return $rendered;
  179. }
  180. /**
  181. * @access private
  182. */
  183. public function renderView($view, array $vars = array(), $bypass = false, $viewtype = '', $issue_missing_notice = true) {
  184. if (!is_string($view) || !is_string($viewtype)) {
  185. $this->logger->log("View and Viewtype in views must be a strings: $view", 'NOTICE');
  186. return '';
  187. }
  188. // basic checking for bad paths
  189. if (strpos($view, '..') !== false) {
  190. return '';
  191. }
  192. if (!is_array($vars)) {
  193. $this->logger->log("Vars in views must be an array: $view", 'ERROR');
  194. $vars = array();
  195. }
  196. // Get the current viewtype
  197. if ($viewtype === '' || !_elgg_is_valid_viewtype($viewtype)) {
  198. $viewtype = elgg_get_viewtype();
  199. }
  200. // allow altering $vars
  201. $vars_hook_params = [
  202. 'view' => $view,
  203. 'vars' => $vars,
  204. 'viewtype' => $viewtype,
  205. ];
  206. $vars = $this->hooks->trigger('view_vars', $view, $vars_hook_params, $vars);
  207. $view_orig = $view;
  208. // Trigger the pagesetup event
  209. if (!isset($this->CONFIG->pagesetupdone) && $this->CONFIG->boot_complete) {
  210. $this->CONFIG->pagesetupdone = true;
  211. _elgg_services()->events->trigger('pagesetup', 'system');
  212. }
  213. // @warning - plugin authors: do not expect user, config, and url to be
  214. // set by elgg_view() in the future. Instead, use elgg_get_logged_in_user_entity(),
  215. // elgg_get_config(), and elgg_get_site_url() in your views.
  216. if (!isset($vars['user'])) {
  217. $vars['user'] = $this->getUserWrapper();
  218. }
  219. if (!isset($vars['config'])) {
  220. if (!$this->config_wrapper) {
  221. $warning = 'Do not rely on $vars["config"] or $CONFIG being available in views';
  222. $this->config_wrapper = new \Elgg\DeprecationWrapper($this->CONFIG, $warning, 1.8);
  223. }
  224. $vars['config'] = $this->config_wrapper;
  225. }
  226. if (!isset($vars['url'])) {
  227. if (!$this->site_url_wrapper) {
  228. $warning = 'Do not rely on $vars["url"] being available in views';
  229. $this->site_url_wrapper = new \Elgg\DeprecationWrapper(elgg_get_site_url(), $warning, 1.8);
  230. }
  231. $vars['url'] = $this->site_url_wrapper;
  232. }
  233. // full_view is the new preferred key for full view on entities @see elgg_view_entity()
  234. // check if full_view is set because that means we've already rewritten it and this is
  235. // coming from another view passing $vars directly.
  236. if (isset($vars['full']) && !isset($vars['full_view'])) {
  237. elgg_deprecated_notice("Use \$vars['full_view'] instead of \$vars['full']", 1.8, 2);
  238. $vars['full_view'] = $vars['full'];
  239. }
  240. if (isset($vars['full_view'])) {
  241. $vars['full'] = $vars['full_view'];
  242. }
  243. // internalname => name (1.8)
  244. if (isset($vars['internalname']) && !isset($vars['__ignoreInternalname']) && !isset($vars['name'])) {
  245. elgg_deprecated_notice('You should pass $vars[\'name\'] now instead of $vars[\'internalname\']', 1.8, 2);
  246. $vars['name'] = $vars['internalname'];
  247. } elseif (isset($vars['name'])) {
  248. if (!isset($vars['internalname'])) {
  249. $vars['__ignoreInternalname'] = '';
  250. }
  251. $vars['internalname'] = $vars['name'];
  252. }
  253. // internalid => id (1.8)
  254. if (isset($vars['internalid']) && !isset($vars['__ignoreInternalid']) && !isset($vars['name'])) {
  255. elgg_deprecated_notice('You should pass $vars[\'id\'] now instead of $vars[\'internalid\']', 1.8, 2);
  256. $vars['id'] = $vars['internalid'];
  257. } elseif (isset($vars['id'])) {
  258. if (!isset($vars['internalid'])) {
  259. $vars['__ignoreInternalid'] = '';
  260. }
  261. $vars['internalid'] = $vars['id'];
  262. }
  263. // If it's been requested, pass off to a template handler instead
  264. if ($bypass == false && isset($this->CONFIG->template_handler) && !empty($this->CONFIG->template_handler)) {
  265. $template_handler = $this->CONFIG->template_handler;
  266. if (is_callable($template_handler)) {
  267. return call_user_func($template_handler, $view, $vars);
  268. }
  269. }
  270. // Set up any extensions to the requested view
  271. if (isset($this->CONFIG->views->extensions[$view])) {
  272. $viewlist = $this->CONFIG->views->extensions[$view];
  273. } else {
  274. $viewlist = array(500 => $view);
  275. }
  276. $content = '';
  277. foreach ($viewlist as $view) {
  278. $rendering = $this->renderViewFile($view, $vars, $viewtype, $issue_missing_notice);
  279. if ($rendering !== false) {
  280. $content .= $rendering;
  281. continue;
  282. }
  283. // attempt to load default view
  284. if ($viewtype !== 'default' && $this->doesViewtypeFallback($viewtype)) {
  285. $rendering = $this->renderViewFile($view, $vars, 'default', $issue_missing_notice);
  286. if ($rendering !== false) {
  287. $content .= $rendering;
  288. }
  289. }
  290. }
  291. // Plugin hook
  292. $params = array('view' => $view_orig, 'vars' => $vars, 'viewtype' => $viewtype);
  293. $content = $this->hooks->trigger('view', $view_orig, $params, $content);
  294. // backward compatibility with less granular hook will be gone in 2.0
  295. $content_tmp = $this->hooks->trigger('display', 'view', $params, $content);
  296. if ($content_tmp !== $content) {
  297. $content = $content_tmp;
  298. elgg_deprecated_notice('The display:view plugin hook is deprecated by view:view_name', 1.8);
  299. }
  300. return $content;
  301. }
  302. /**
  303. * Wrapper for file_exists() that caches false results (the stat cache only caches true results).
  304. * This saves us from many unneeded file stat calls when a common view uses a fallback.
  305. *
  306. * @param string $path Path to the file
  307. * @return bool
  308. */
  309. protected function fileExists($path) {
  310. if (!isset($this->file_exists_cache[$path])) {
  311. $this->file_exists_cache[$path] = file_exists($path);
  312. }
  313. return $this->file_exists_cache[$path];
  314. }
  315. /**
  316. * Includes view PHP or static file
  317. *
  318. * @param string $view The view name
  319. * @param array $vars Variables passed to view
  320. * @param string $viewtype The viewtype
  321. * @param bool $issue_missing_notice Log a notice if the view is missing
  322. *
  323. * @return string|false output generated by view file inclusion or false
  324. */
  325. private function renderViewFile($view, array $vars, $viewtype, $issue_missing_notice) {
  326. $view_location = $this->getViewLocation($view, $viewtype);
  327. // @warning - plugin authors: do not expect $CONFIG to be available in views
  328. // in the future. Instead, use elgg_get_config() in your views.
  329. // Note: this is intentionally a local var.
  330. $CONFIG = $this->config_wrapper;
  331. if ($this->fileExists("{$view_location}$viewtype/$view.php")) {
  332. ob_start();
  333. include("{$view_location}$viewtype/$view.php");
  334. return ob_get_clean();
  335. } else if ($this->fileExists("{$view_location}$viewtype/$view")) {
  336. return file_get_contents("{$view_location}$viewtype/$view");
  337. } else {
  338. if ($issue_missing_notice) {
  339. $this->logger->log("$viewtype/$view view does not exist.", 'NOTICE');
  340. }
  341. return false;
  342. }
  343. }
  344. /**
  345. * @access private
  346. */
  347. public function viewExists($view, $viewtype = '', $recurse = true) {
  348. if (empty($view) || !is_string($view)) {
  349. return false;
  350. }
  351. // Detect view type
  352. if ($viewtype === '' || !_elgg_is_valid_viewtype($viewtype)) {
  353. $viewtype = elgg_get_viewtype();
  354. }
  355. if (!isset($this->CONFIG->views->locations[$viewtype][$view])) {
  356. if (!isset($this->CONFIG->viewpath)) {
  357. $location = dirname(dirname(dirname(__FILE__))) . "/views/";
  358. } else {
  359. $location = $this->CONFIG->viewpath;
  360. }
  361. } else {
  362. $location = $this->CONFIG->views->locations[$viewtype][$view];
  363. }
  364. if ($this->fileExists("{$location}$viewtype/$view.php") ||
  365. $this->fileExists("{$location}$viewtype/$view")) {
  366. return true;
  367. }
  368. // If we got here then check whether this exists as an extension
  369. // We optionally recursively check whether the extended view exists also for the viewtype
  370. if ($recurse && isset($this->CONFIG->views->extensions[$view])) {
  371. foreach ($this->CONFIG->views->extensions[$view] as $view_extension) {
  372. // do not recursively check to stay away from infinite loops
  373. if ($this->viewExists($view_extension, $viewtype, false)) {
  374. return true;
  375. }
  376. }
  377. }
  378. // Now check if the default view exists if the view is registered as a fallback
  379. if ($viewtype != 'default' && $this->doesViewtypeFallback($viewtype)) {
  380. return $this->viewExists($view, 'default');
  381. }
  382. return false;
  383. }
  384. /**
  385. * @access private
  386. */
  387. public function extendView($view, $view_extension, $priority = 501, $viewtype = '') {
  388. if (!isset($this->CONFIG->views)) {
  389. $this->CONFIG->views = (object) array(
  390. 'extensions' => array(),
  391. );
  392. $this->CONFIG->views->extensions[$view][500] = (string) $view;
  393. } else {
  394. if (!isset($this->CONFIG->views->extensions[$view])) {
  395. $this->CONFIG->views->extensions[$view][500] = (string) $view;
  396. }
  397. }
  398. // raise priority until it doesn't match one already registered
  399. while (isset($this->CONFIG->views->extensions[$view][$priority])) {
  400. $priority++;
  401. }
  402. $this->CONFIG->views->extensions[$view][$priority] = (string) $view_extension;
  403. ksort($this->CONFIG->views->extensions[$view]);
  404. }
  405. /**
  406. * @access private
  407. */
  408. public function unextendView($view, $view_extension) {
  409. if (!isset($this->CONFIG->views)) {
  410. return false;
  411. }
  412. if (!isset($this->CONFIG->views->extensions)) {
  413. return false;
  414. }
  415. if (!isset($this->CONFIG->views->extensions[$view])) {
  416. return false;
  417. }
  418. $priority = array_search($view_extension, $this->CONFIG->views->extensions[$view]);
  419. if ($priority === false) {
  420. return false;
  421. }
  422. unset($this->CONFIG->views->extensions[$view][$priority]);
  423. return true;
  424. }
  425. /**
  426. * @access private
  427. */
  428. public function registerCacheableView($view) {
  429. if (!isset($this->CONFIG->views)) {
  430. $this->CONFIG->views = new \stdClass;
  431. }
  432. if (!isset($this->CONFIG->views->simplecache)) {
  433. $this->CONFIG->views->simplecache = array();
  434. }
  435. $this->CONFIG->views->simplecache[$view] = true;
  436. }
  437. /**
  438. * @access private
  439. */
  440. public function isCacheableView($view) {
  441. if (!isset($this->CONFIG->views)) {
  442. $this->CONFIG->views = new \stdClass;
  443. }
  444. if (!isset($this->CONFIG->views->simplecache)) {
  445. $this->CONFIG->views->simplecache = array();
  446. }
  447. if (isset($this->CONFIG->views->simplecache[$view])) {
  448. return true;
  449. } else {
  450. $currentViewtype = elgg_get_viewtype();
  451. $viewtypes = array($currentViewtype);
  452. if ($this->doesViewtypeFallback($currentViewtype) && $currentViewtype != 'default') {
  453. $viewtypes[] = 'defaut';
  454. }
  455. // If a static view file is found in any viewtype, it's considered cacheable
  456. foreach ($viewtypes as $viewtype) {
  457. $view_file = $this->getViewLocation($view, $viewtype) . "$viewtype/$view";
  458. if ($this->fileExists($view_file)) {
  459. return true;
  460. }
  461. }
  462. // Assume not-cacheable by default
  463. return false;
  464. }
  465. }
  466. /**
  467. * Register a plugin's views
  468. *
  469. * @param string $path Base path of the plugin
  470. * @param string $failed_dir This var is set to the failed directory if registration fails
  471. * @return bool
  472. *
  473. * @access private
  474. */
  475. public function registerPluginViews($path, &$failed_dir = '') {
  476. $view_dir = "$path/views/";
  477. // plugins don't have to have views.
  478. if (!is_dir($view_dir)) {
  479. return true;
  480. }
  481. // but if they do, they have to be readable
  482. $handle = opendir($view_dir);
  483. if (!$handle) {
  484. $failed_dir = $view_dir;
  485. return false;
  486. }
  487. while (false !== ($view_type = readdir($handle))) {
  488. $view_type_dir = $view_dir . $view_type;
  489. if ('.' !== substr($view_type, 0, 1) && is_dir($view_type_dir)) {
  490. if ($this->autoregisterViews('', $view_type_dir, $view_dir, $view_type)) {
  491. elgg_register_viewtype($view_type);
  492. } else {
  493. $failed_dir = $view_type_dir;
  494. return false;
  495. }
  496. }
  497. }
  498. return true;
  499. }
  500. /**
  501. * Get views overridden by setViewLocation() calls.
  502. *
  503. * @return array
  504. *
  505. * @access private
  506. */
  507. public function getOverriddenLocations() {
  508. return $this->overriden_locations;
  509. }
  510. /**
  511. * Set views overridden by setViewLocation() calls.
  512. *
  513. * @param array $locations
  514. * @return void
  515. *
  516. * @access private
  517. */
  518. public function setOverriddenLocations(array $locations) {
  519. $this->overriden_locations = $locations;
  520. }
  521. }