SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/ContentReplace.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 DOMDocument;
  17.  
  18. use karmabunny\pdb\Exceptions\RowMissingException;
  19.  
  20.  
  21. /**
  22. * No description yet.
  23. **/
  24. class ContentReplace
  25. {
  26. private static $temp;
  27. public static $preloaded_widgets = array();
  28.  
  29.  
  30. /**
  31.   * Execute a content replace chain
  32.   *
  33.   * @example
  34.   * $html = Blogs::getBlogPost($post_id);
  35.   * $html = ContentReplace::executeChain('inner_html', $html);
  36.   *
  37.   * @param string $chain The name of the chain to execute
  38.   * @param string $html The incoming HTML
  39.   */
  40. public static function executeChain($chain, $html)
  41. {
  42. $methods = Register::getContentReplaceMethods($chain);
  43. foreach ($methods as $callable) {
  44. $html = call_user_func($callable, $html);
  45. }
  46. return $html;
  47. }
  48.  
  49.  
  50. /**
  51.   * Do all the replacements normally required for HTML content
  52.   * Internal links, inline addons, the local anchor fix, etc.
  53.   *
  54.   * @param string $html
  55.   * @return string HTML
  56.   **/
  57. public static function html($html)
  58. {
  59. return self::executeChain('inner_html', $html);
  60. }
  61.  
  62.  
  63. /**
  64.   * Replace links to internal pages in the format page/view_by_id/... with the page's friendly url
  65.   **/
  66. public static function intlinks($text)
  67. {
  68. return preg_replace_callback('!<a ([^>]*?)href="page/view_by_id/([0-9]+)"!', array(__CLASS__, '_intlinks'), $text);
  69. }
  70.  
  71. private static function _intlinks($matches)
  72. {
  73. $matches[2] = (int) $matches[2];
  74.  
  75. $root = Navigation::getRootNode();
  76. $node = $root->findNodeValue('id', $matches[2]);
  77.  
  78. if (! $node) {
  79. return '<a ' . $matches[1] . 'href="page/view_by_id/' . $matches[2] . '"';
  80. }
  81.  
  82. return '<a ' . $matches[1] . 'href="' . $node->getFriendlyUrl() . '"';
  83. }
  84.  
  85.  
  86. /**
  87.   * Add some preloaded widgets, e.g. generated by a preview
  88.   * @param array $widgets Format to match $_POST['widgets'] on
  89.   * admin/edit/page/{id}
  90.   * @param array $settings Format to match $_POST['widget_settings']
  91.   * on admin/edit/page/{id}
  92.   */
  93. public static function preloadWidgets(array $widgets, array $settings)
  94. {
  95. foreach ($widgets as $area_name => $widget_list) {
  96. foreach ($widget_list as $widget) {
  97. list($record_order, $type, $widget_id) = explode(',', $widget);
  98. $widget_settings = array();
  99. if (isset($settings[$area_name][$record_order])) {
  100. $widget_settings = $settings[$area_name][$record_order];
  101. }
  102. $area_id = WidgetArea::findAreaByName($area_name)->getIndex();
  103. Widgets::add($area_id, $type, $widget_settings);
  104. self::$preloaded_widgets[] = array(
  105. 'type' => $type,
  106. 'settings' => $widget_settings,
  107. 'embed_key' => $widget_id
  108. );
  109. }
  110. }
  111. }
  112.  
  113.  
  114. /**
  115.   * Replace embed code with the actual widgets.
  116.   *
  117.   * Embed code is in the format ((WIDGET code))
  118.   *
  119.   * @param string $text The text which contains the widget embed codes
  120.   * @param string $widget_table The host table of the widgets, e.g. 'page'
  121.   * @param int $widget_record The record id
  122.   * @return string $text
  123.   **/
  124. public static function embedWidgets($text, $widget_table, $widget_record)
  125. {
  126. $widgets = self::$preloaded_widgets;
  127.  
  128. Pdb::validateIdentifier($widget_table);
  129. $widget_record = (int) $widget_record;
  130.  
  131. $widget_table = addslashes($widget_table);
  132.  
  133. // Find widget references in text
  134. $matches = array();
  135. '!<p>(?:\s|&nbsp;|<code>)*\(\(WIDGET [a-zA-Z]*? ?([0-9A-Za-z]+)\)\)(?:\s|&nbsp;|</code>)*</p>!',
  136. $text,
  137. $matches
  138. );
  139.  
  140. if (count($matches) == 0) return $text;
  141.  
  142. // Map embed codes => widget refs
  143. $embeds = array();
  144. foreach ($matches[0] as $key => $match) {
  145. $embeds[$match] = $matches[1][$key];
  146. }
  147.  
  148. // Replace preloaded widgets first
  149. foreach ($embeds as $pattern => $embed_key) {
  150. foreach ($widgets as $widget) {
  151. if ($embed_key == $widget['embed_key']) {
  152. $text = self::embedWidget($text, $pattern, $widget);
  153. unset($embeds[$pattern]);
  154. break;
  155. }
  156. }
  157. }
  158. if (count($embeds) == 0) return $text;
  159.  
  160. // If any embed codes remain, do a DB lookup and replace them
  161. $widgets = self::lookupWidgets($widget_table, $widget_record, $embeds);
  162. foreach ($embeds as $pattern => $embed_key) {
  163. foreach ($widgets as $widget) {
  164. if ($embed_key == $widget['embed_key']) {
  165. $text = self::embedWidget($text, $pattern, $widget);
  166. unset($embeds[$pattern]);
  167. break;
  168. }
  169. }
  170. }
  171.  
  172. return $text;
  173. }
  174.  
  175. /**
  176.   * @param string $widget_table The host table of the widgets, e.g. 'page'
  177.   * @param int $widget_record The record id
  178.   * @param array $embed_keys The embed keys
  179.   * @return array Keys are the widget reference codes, values are arrays
  180.   * with keys and values matching the DB fields: 'type' (string),
  181.   * 'settings' (array), and 'embed_key' (string)
  182.   */
  183. public static function lookupWidgets($widget_table, $widget_record, array $embed_keys)
  184. {
  185. Pdb::validateIdentifier($widget_table);
  186. $widget_record = (int) $widget_record;
  187. if (count($embed_keys) == 0) return array();
  188.  
  189. $conditions = ["{$widget_table}_id" => $widget_record];
  190. $conditions[] = ['embed_key', 'IN', $embed_keys];
  191. $params = [];
  192. $where = Pdb::buildClause($conditions, $params);
  193.  
  194. $q = "SELECT type, settings, embed_key
  195. FROM ~{$widget_table}_widgets
  196. WHERE {$where}";
  197. $res = Pdb::q($q, $params, 'arr');
  198.  
  199. $widgets = array();
  200. foreach ($res as $row) {
  201. $row['settings'] = json_decode($row['settings'], true);
  202. $widgets[$row['embed_key']] = $row;
  203. }
  204. return $widgets;
  205. }
  206.  
  207. /**
  208.   * Embeds a widget into some text
  209.   * @param string $text The text containing the widget embed
  210.   * @param string $pattern The widget embed to replace
  211.   * @param array $widget as per {@link lookupWidgets()}, has keys and values
  212.   * matching the DB fields: 'type' (string), 'settings' (array),
  213.   * and 'embed_key' (string)
  214.   * @return string The text with the rendered widget
  215.   */
  216. public static function embedWidget($text, $pattern, array $widget)
  217. {
  218. $rendered = Widgets::render(
  219. WidgetArea::ORIENTATION_WIDE,
  220. $widget['type'],
  221. $widget['settings']
  222. );
  223. return str_replace($pattern, $rendered, $text);
  224. }
  225.  
  226.  
  227. /**
  228.   * Remove widget embed code from the specified HTML
  229.   * Embed code is in the format ((WIDGET code))
  230.   **/
  231. public static function removeWidgets($text)
  232. {
  233. return preg_replace(
  234. '!<p>(?:\s|&nbsp;|<code>)*\(\(WIDGET [a-zA-Z]*? ?([0-9A-Za-z]+)\)\)(?:\s|&nbsp;|</code>)*</p>!',
  235. '',
  236. $text
  237. );
  238. }
  239.  
  240.  
  241. /**
  242.   * Replace embed code with the actual widget. This is a tweaked version of embedWidgets, designed for email
  243.   *
  244.   * Embed code is in the format ((WIDGET code))
  245.   *
  246.   * @param string $widget_table The host table of the widgets, e.g. 'page'
  247.   * @param string $widget_record The record id
  248.   **/
  249. public static function emailWidgets($text, $widget_table, $widget_record, $pre_html, $post_html)
  250. {
  251. Pdb::validateIdentifier($widget_table);
  252. $widget_record = (int) $widget_record;
  253.  
  254. self::$temp = array($widget_table, $widget_record, $pre_html, $post_html);
  255.  
  256. return preg_replace_callback('!<p>(?:<code>)?\(\(WIDGET [a-zA-Z]*? ?([0-9A-Za-z]+)\)\)(?:</code>)?</p>!', array('Sprout\Helpers\ContentReplace', '_email_widgets'), $text);
  257. }
  258.  
  259. private static function _emailWidgets($matches)
  260. {
  261. list($widget_table, $widget_record, $pre_html, $post_html) = self::$temp;
  262. $widget_record = (int) $widget_record;
  263.  
  264. $q = "SELECT type, settings
  265. FROM ~{$widget_table}_widgets
  266. WHERE {$widget_table}_id = ? AND embed_key = ?
  267. LIMIT 1";
  268. try {
  269. $widget = Pdb::query($q, [$widget_record, $matches[1]], 'row');
  270. } catch (RowMissingException $ex) {
  271. return;
  272. }
  273.  
  274. return Widgets::render(
  275. WidgetArea::ORIENTATION_EMAIL,
  276. $widget['type'],
  277. json_decode($widget['settings'], true),
  278. $pre_html,
  279. $post_html
  280. );
  281. }
  282.  
  283. /**
  284.   * Replace database fields with database values
  285.   *
  286.   * Replacement code is in the format ((REPLACE field_name))
  287.   *
  288.   * @param string $data An array of field data
  289.   **/
  290. public static function dbFields($text, $data)
  291. {
  292. '!\(\(REPLACE ([a-z_]+)\)\)!',
  293. function($matches) use ($data) {
  294. return $data[$matches[1]];
  295. },
  296. $text
  297. );
  298. }
  299.  
  300.  
  301. /**
  302.   * Remove tables that don't contain any content
  303.   **/
  304. public static function emptyTables($text)
  305. {
  306. return preg_replace_callback('!<table[^>]*?>(.*?)</table>!s', array('Sprout\Helpers\ContentReplace', '_empty_tables'), $text);
  307. }
  308.  
  309. private static function _emptyTables($matches)
  310. {
  311. if (strpos($matches[1], '<img') !== false and strpos($matches[1], 'class="deco"') === false) return $matches[0];
  312. if (trim(strip_tags($matches[1])) != '') return $matches[0];
  313. return '';
  314. }
  315.  
  316.  
  317. /**
  318.   * Replaces an expando with a link to the specified URL
  319.   *
  320.   * This method might not preserve whitespace in the input html.
  321.   * The link is only appended if the content actually contained expandos.
  322.   * The lunk url is optional. If not provided, no link gets appended.
  323.   *
  324.   * @param string $html The HTML which contains expandos.
  325.   * @param string $url A url for the link which gets appended to the end.
  326.   **/
  327. public static function expandolink($html, $url = null)
  328. {
  329. $html = trim($html);
  330. if ($html == '') return '';
  331.  
  332. // Create a DOMDocument obj
  333. $dom = new DOMDocument('1.0', 'UTF-8');
  334. $dom->preserveWhiteSpace = true;
  335. $dom->formatOutput = false;
  336. $dom->strictErrorChecking = false;
  337.  
  338. // Load html string into a DOMDocument
  339. @$dom->loadHTML('<meta http-equiv="Content-type" content="text/html; charset=UTF-8">' . $html);
  340.  
  341. // Find expandos
  342. $has_expando = false;
  343. $divs = $dom->getElementsByTagName('div');
  344. $rem = array();
  345. foreach ($divs as $elem) {
  346. if (strpos($elem->getAttribute('class'), 'expando') !== false) {
  347. $has_expando = true;
  348. $rem[] = $elem;
  349. }
  350. }
  351.  
  352. // No expandos? No worries
  353. if (! $has_expando) {
  354. return $html;
  355. }
  356.  
  357. // Remove expandos
  358. foreach ($rem as $elem) {
  359. $elem->parentNode->removeChild($elem);
  360. }
  361.  
  362. // Save HTML back to string
  363. $html = $dom->saveHTML();
  364. $html = preg_replace('!.+<body>(.+?)</body>.+!s', '$1', $html);
  365. $html = str_replace(array("\n", "\t"), '', $html);
  366.  
  367. // Append link if specified
  368. if ($url) {
  369. $html .= "<p><a href=\"{$url}\">More information</a></p>";
  370. }
  371.  
  372. return $html;
  373. }
  374.  
  375.  
  376. /**
  377.   * Replace anchors without a URL (e.g. href="#top") to include the page URL to fix issues with BASE HREF
  378.   **/
  379. public static function localAnchor($text)
  380. {
  381. return str_replace('href="#', 'href="' . Url::current() . '#', $text);
  382. }
  383.  
  384.  
  385. /**
  386.   * Use Sprout::specialFileLinks instead, it's got better unit tests etc.
  387.   **/
  388. public static function documentLinks($html)
  389. {
  390. return Sprout::specialFileLinks($html);
  391. }
  392.  
  393.  
  394. /**
  395.   * Replace file URLs that refer to IDs with URLs which refer to filenames
  396.   *
  397.   * @param string $html
  398.   * @return string
  399.   */
  400. public static function fileDownload($html)
  401. {
  402. $ids = [];
  403.  
  404. $pattern = '#<(?:a|img)\s[^>]*(?:href|src)="file/download/([0-9]+)#';
  405. $matches = [];
  406. if (!preg_match_all($pattern, $html, $matches)) {
  407. return $html;
  408. }
  409.  
  410. foreach ($matches[1] as $match) {
  411. $id = (int) $match;
  412. if ($id <= 0) continue;
  413.  
  414. if (!in_array($id, $ids)) $ids[] = $id;
  415. }
  416.  
  417. if (count($ids) == 0) return $html;
  418.  
  419. $params = [];
  420. $where = Pdb::buildClause([['id', 'IN', $ids]], $params);
  421. $q = "SELECT id, filename FROM ~files WHERE {$where}";
  422. $filenames = Pdb::q($q, $params, 'map');
  423.  
  424. // $matches:
  425. // [0] Everything from start of tag to end of file download URL
  426. // [1] Everything from the start of the tag up to the " at the start of the URL
  427. // E.g. '<a href="' or '<img class="some-class" src="'
  428. // [2] File ID
  429. // [3] File size variant, e.g. 'small', 'medium', ...
  430. $id_replace = function($matches) use ($filenames) {
  431. $id = $matches[2];
  432. $filename = $filenames[$id];
  433. if (!empty($matches[3])) {
  434. $filename = File::getResizeFilename($filename, $matches[3]);
  435. }
  436. return $matches[1] . File::url($filename);
  437. };
  438.  
  439. $pattern = '#(<(?:a|img)\s[^>]*(?:href|src)=")file/download/([0-9]+)(?:/([a-zA-Z0-9_]+))?#';
  440. return preg_replace_callback($pattern, $id_replace, $html);
  441. }
  442.  
  443. }
  444.