SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Page.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Exception;
  17. use InvalidArgumentException;
  18.  
  19. use karmabunny\pdb\Exceptions\QueryException;
  20. use karmabunny\pdb\Exceptions\RowMissingException;
  21.  
  22.  
  23. /**
  24.  * Methods for working with CMS pages
  25.  */
  26. class Page
  27. {
  28.  
  29. /**
  30.   * Get the page id for a given URL
  31.   **/
  32. public static function lookupUrl($url)
  33. {
  34. $root = Navigation::getRootNode();
  35. if (! $root) return null;
  36.  
  37. $matcher = new TreenodePathMatcher($url);
  38. $node = $root->findNode($matcher);
  39. if (! $node) return null;
  40.  
  41. return $node['id'];
  42. }
  43.  
  44.  
  45. /**
  46.   * Return the URL for a page, when given the page id
  47.   * The URLs are relative
  48.   *
  49.   * @param int $id The page id to return the URL for
  50.   * @return string The friendly URL for the page
  51.   **/
  52. public static function url($id)
  53. {
  54. $id = (int) $id;
  55. if (! $id) return null;
  56.  
  57. $root = Navigation::getRootNode();
  58.  
  59. $node = $root->findNodeValue('id', $id);
  60. if (! $node) {
  61. return 'page/view_by_id/' . $id;
  62. }
  63.  
  64. return $node->getFriendlyUrlNoprefix();
  65. }
  66.  
  67.  
  68. /**
  69.   * Fetches the URL for a tool page for a specific controller entrance method
  70.   * @param string|Controller $class Class
  71.   * @param string $method Method
  72.   * @return string The URL to access the tool page
  73.   * @throws Exception if there's no matching tool page in the {@see Navigation} tree
  74.   */
  75. public static function toolUrl($class, $method)
  76. {
  77. if (is_object($class)) $class = get_class($class);
  78. $matcher = new TreenodeFrontendMatcher($class, $method);
  79. $node = Navigation::getRootNode()->findNode($matcher);
  80. if (!$node) throw new Exception('Page for controller entrance not found');
  81. return $node->getFriendlyUrl();
  82. }
  83.  
  84.  
  85. /**
  86.   * Set up metadata and social metadata for a tool page
  87.   *
  88.   * @return array $page Page record from database
  89.   * @return null Current URL is not a matched node
  90.   */
  91. public static function setupToolPage()
  92. {
  93. $node = Navigation::getMatchedNode();
  94. if (!$node) return null;
  95.  
  96. $page = Pdb::get('pages', $node['id']);
  97.  
  98. static::loadPageMeta($page);
  99. static::loadPageSocial($page, $node);
  100.  
  101. return $page;
  102. }
  103.  
  104.  
  105. /**
  106.   * Load page metadata - description and keywords
  107.   *
  108.   * @param array $page Page record from database
  109.   */
  110. public static function loadPageMeta(array $page)
  111. {
  112. if (!empty($page['meta_description'])) {
  113. Needs::addMeta('description', $page['meta_description']);
  114. }
  115. if (!empty($page['meta_keywords'])) {
  116. Needs::addMeta('keywords', $page['meta_keywords']);
  117. }
  118. }
  119.  
  120.  
  121. /**
  122.   * Load page social - title, image, description, url
  123.   *
  124.   * @param array $page Page record from database
  125.   * @param Pagenode $node Node, for generating the URL; optional
  126.   */
  127. public static function loadPageSocial(array $page, Pagenode $node = null)
  128. {
  129. SocialMeta::setTitle($page['name']);
  130.  
  131. if (!empty($page['gallery_thumb'])) {
  132. SocialMeta::setImage($page['gallery_thumb']);
  133. } else if (!empty($page['banner'])) {
  134. SocialMeta::setImage($page['banner']);
  135. }
  136.  
  137. if (!empty($page['meta_description'])) {
  138. SocialMeta::setDescription($page['meta_description']);
  139. }
  140.  
  141. if ($node !== null) {
  142. SocialMeta::setUrl($node->getFriendlyUrlNoPrefix());
  143. }
  144. }
  145.  
  146.  
  147. /**
  148.   * Inject page details -- title and browser title -- into a skin view
  149.   *
  150.   * @param View $skin Skin view to inject details into
  151.   * @param array $page Page to pull details from
  152.   */
  153. public static function injectPageSkin(View $skin, array $page)
  154. {
  155. if (!empty($page['name'])) {
  156. $skin->page_title = $page['name'];
  157. }
  158. if (!empty($page['alt_browser_title'])) {
  159. $skin->browser_title = $page['alt_browser_title'];
  160. }
  161. }
  162.  
  163.  
  164. /**
  165.   * Gets the embedded widgets (i.e. content blocks) for a page
  166.   * @param int $rev_id Page revision ID from database (page_revisions.id)
  167.   * @param string $include 'active' to only include active widgets,
  168.   * 'all' to include disabled widgets as well
  169.   * @return array Rows extracted from the database
  170.   */
  171. public static function getContentWidgets($rev_id, $include)
  172. {
  173. $rev_id = (int) $rev_id;
  174. if (!in_array($include, ['active', 'all'])) {
  175. throw new InvalidArgumentException("\$include must be 'active' or 'all'");
  176. }
  177.  
  178. $active = ($include == 'active' ? 'AND active = 1' : '');
  179.  
  180. $q = "SELECT id, type, settings, active, heading, template
  181. FROM ~page_widgets
  182. WHERE page_revision_id = ? AND area_id = 1 {$active}
  183. ORDER BY record_order";
  184. return Pdb::q($q, [$rev_id], 'map-arr');
  185. }
  186.  
  187.  
  188. /**
  189.   * Get the page text for a page id, in HTML format, with widgets and everything, ready to go
  190.   *
  191.   * @param int $page_id Page ID from database
  192.   * @param int $rev_id If specified, that revision will be used.
  193.   * Otherwise, the current live revision will be used.
  194.   * N.B. if specified, it must be a valid revision for the specified page
  195.   * @param int $subsite_id Optional subsite ID - this is required for admins editing a sub-site
  196.   * that's different from the domain
  197.   * @return string HTML
  198.   * @throws QueryException
  199.   **/
  200. public static function getText($page_id, $rev_id = 0, $subsite_id = null)
  201. {
  202. $page_id = (int) $page_id;
  203. $rev_id = (int) $rev_id;
  204. $subsite_id = (int) $subsite_id;
  205.  
  206. if ($subsite_id < 1) $subsite_id = SubsiteSelector::$content_id;
  207.  
  208. $params = [
  209. 'id' => $page_id,
  210. 'subsite_id' => $subsite_id,
  211. ];
  212. if ($rev_id > 0) {
  213. $clause = "rev.id = :rev_id";
  214. $params['rev_id'] = $rev_id;
  215. } else {
  216. $clause = "rev.status = :live";
  217. $params['live'] = 'live';
  218. }
  219. $q = "SELECT rev.id
  220. FROM ~pages AS page
  221. INNER JOIN ~page_revisions AS rev ON page.id = rev.page_id AND {$clause}
  222. WHERE page.id = :id AND page.subsite_id = :subsite_id
  223. LIMIT 1";
  224. try {
  225. $rev_id = Pdb::query($q, $params, 'val');
  226.  
  227. // No live revision means this is a new, blank page
  228. } catch (RowMissingException $ex) {
  229. return '';
  230. }
  231.  
  232. $text = '';
  233. $widgets = self::getContentWidgets($rev_id, 'active');
  234. foreach ($widgets as $widget) {
  235. $inst = Widgets::instantiate($widget['type']);
  236. $inst->importSettings(json_decode($widget['settings'], true));
  237. $inst->setTitle($widget['heading']);
  238. $widget_text = $inst->render(WidgetArea::ORIENTATION_WIDE);
  239. if (!$widget_text) continue;
  240.  
  241. // Prevent lack of whitespace between final and initial sentences of adjacent blocks
  242. if ($text) $text .= "\n";
  243.  
  244. $text .= $widget_text;
  245. }
  246.  
  247. return ContentReplace::html($text);
  248. }
  249.  
  250.  
  251. /**
  252.   * Returns an array of key-value pairs of all attributes for a page
  253.   **/
  254. public static function attrs($id)
  255. {
  256. $id = (int) $id;
  257.  
  258. $attrs = array();
  259.  
  260. try {
  261. $q = "SELECT name, value FROM ~page_attributes WHERE page_id = ?";
  262. $attrs = Pdb::q($q, [$id], 'map');
  263. } catch (Exception $ex) {}
  264.  
  265. return $attrs;
  266. }
  267.  
  268.  
  269. /**
  270.   * Returns an array pages which contain a given attribute.
  271.   *
  272.   * @param string $attr_name Attribute name to search for
  273.   * @param string $attr_value If set, require the attribute to be this value
  274.   * @return array Page IDs
  275.   * @throws QueryException
  276.   **/
  277. public static function pagesWithAttr($attr_name, $attr_value = null)
  278. {
  279. $conditions = array();
  280. $conditions['name'] = $attr_name;
  281. if ($attr_value) $conditions['value'] = $attr_value;
  282.  
  283. $params = array();
  284. $where = Pdb::buildClause($conditions, $params);
  285.  
  286. $q = "SELECT page_id FROM ~page_attributes WHERE {$where} ORDER BY page_id";
  287. return Pdb::query($q, $params, 'col');
  288. }
  289.  
  290.  
  291. /**
  292.   * Find pages with a given widget, and optionally the specified settings.
  293.   * Only widgets on live revisions of active pages are checked.
  294.   * @param string $class The class of the desired widget.
  295.   * @param array $settings The settings to look for; all of the specified settings must match.
  296.   * @return array Page IDs
  297.   * @throws QueryException
  298.   */
  299. public static function pagesWithWidget($widget_name, array $settings = [])
  300. {
  301. $q = "SELECT page.id, widget.settings
  302. FROM ~page_widgets AS widget
  303. INNER JOIN ~page_revisions AS rev ON widget.page_revision_id = rev.id AND rev.status = 'live'
  304. INNER JOIN ~pages AS page ON rev.page_id = page.id AND page.active = 1
  305. WHERE widget.type = ?";
  306. if (count($settings) == 0) {
  307. return Pdb::query($q, [$widget_name], 'col');
  308. }
  309.  
  310. $res = Pdb::query($q, [$widget_name], 'pdo');
  311. $pages = [];
  312. foreach ($res as $row) {
  313. $widget_settings = json_decode($row['settings'], true);
  314. $diff = array_diff_assoc($settings, $widget_settings);
  315. if (count($diff) == 0) {
  316. $pages[] = $row['id'];
  317. }
  318. }
  319. $res->closeCursor();
  320.  
  321. return $pages;
  322. }
  323.  
  324.  
  325. /**
  326.   * Return the last-modified date of the specified page.
  327.   * Returns NULL on error.
  328.   *
  329.   * If you don't provide a page-id, uses the id of the
  330.   * Navigation::matchedNode()
  331.   *
  332.   * The date is formatted using the php date function.
  333.   * The default date format is "d/m/Y".
  334.   *
  335.   * @param int $page_id The page to get the last-modified date of
  336.   * @param string $date_format The date format to return the date in
  337.   * @return string Date
  338.   * @return null If page could not be found
  339.   **/
  340. public static function lastModified($page_id = null, $date_format = 'd/m/Y')
  341. {
  342. if ($page_id === null) {
  343. $node = Navigation::matchedNode();
  344. if ($node === null) return null;
  345. $page_id = $node['id'];
  346. }
  347.  
  348. try {
  349. $q = "SELECT date_modified
  350. FROM ~pages
  351. WHERE id = ?
  352. ORDER BY date_modified DESC
  353. LIMIT 1";
  354. $date = Pdb::query($q, [$page_id], 'val');
  355. return date($date_format, strtotime($date));
  356.  
  357. } catch (QueryException $ex) {
  358. return null;
  359. }
  360. }
  361.  
  362.  
  363. /**
  364.   * Makes a particular revision live, and changes the status of the previous live revision to 'old'.
  365.   * Should be run inside a transaction.
  366.   * @param $rev_id ID of revision to make live
  367.   * @return void
  368.   */
  369. public static function activateRevision($rev_id) {
  370. $rev_id = (int) $rev_id;
  371.  
  372. $q = "SELECT page_id FROM ~page_revisions WHERE id = ?";
  373. $page_id = Pdb::q($q, [$rev_id], 'val');
  374.  
  375. $old_clause = ['page_id' => $page_id, 'status' => 'live'];
  376. Pdb::update('page_revisions', ['status' => 'old'], $old_clause);
  377.  
  378. Pdb::update('page_revisions', ['status' => 'live'], ['id' => $rev_id]);
  379. }
  380.  
  381.  
  382. /**
  383.   * Return list of related links for given page
  384.   *
  385.   * @param int $page_id
  386.   * @return array [href, text] pairs
  387.   */
  388. public static function determineRelatedLinks($page_id)
  389. {
  390. $list = [];
  391. $root_node = Navigation::getRootNode();
  392. if ($root_node == null) return $list;
  393.  
  394.  
  395. $matcher = new TreenodeIdsMatcher([$page_id]);
  396. if ($matcher == null) return $list;
  397.  
  398. $page_node = $root_node->findNode($matcher);
  399. if ($page_node == null) return $list;
  400.  
  401. $ancestors = $page_node->findAncestors();
  402. $top_anc = $ancestors[0];
  403.  
  404. $top_anc->filterChildren(new TreenodeInMenuMatcher());
  405.  
  406. if (count($top_anc->children) == 0) {
  407. $top_anc->removeFilter();
  408. return $list;
  409. }
  410.  
  411. foreach ($top_anc->children as $page) {
  412. $list = array_merge($list, self::determineNodeLinks($page, 0));
  413. }
  414.  
  415. $top_anc->removeFilter();
  416.  
  417. return $list;
  418. }
  419.  
  420.  
  421. /**
  422.   * Traverse page node to extract page links
  423.   *
  424.   * @param TreeNode $node The node to traverse
  425.   * @param int $depth
  426.   * @return array [href, text] pairs
  427.   */
  428. private static function determineNodeLinks($node, $depth)
  429. {
  430. $list = [];
  431. $new_depth = $depth + 1;
  432.  
  433. $list[] = ['href' => $node->getFriendlyUrl(), 'text' => $node->getNavigationName()];
  434.  
  435. if ($new_depth <= 10 and count($node->children)) {
  436. foreach ($node->children as $node) {
  437. $list = array_merge($list, self::determineNodeLinks($node, $new_depth));
  438. }
  439. }
  440.  
  441. return $list;
  442. }
  443. }
  444.