SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Needs.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.  
  18. use Kohana;
  19. use Event;
  20.  
  21.  
  22. /**
  23. * Provides a system for injecting CSS and Javascript includes into the head of a document even after the head has been outputted.
  24. * Also does replacement for the string "SITE/", which gets changed into the Kohana root directory.
  25. **/
  26. class Needs
  27. {
  28. private static $needs = array();
  29.  
  30.  
  31. /**
  32.   * Adds a generic need.
  33.   * If a key is specified, the need is added with that key
  34.   * allowing for updating of the need later.
  35.   *
  36.   * @param string $need The HTML for the need.
  37.   **/
  38. public static function addNeed($need, $key = null)
  39. {
  40. if (in_array($need, self::$needs)) return;
  41. if ($key) {
  42. self::$needs[$key] = $need;
  43. } else {
  44. self::$needs[] = $need;
  45. }
  46. }
  47.  
  48. /**
  49.   * Removes a module
  50.   *
  51.   * e.g. Needs::fileGroup('facebox') can be removed with Needs::removeModule('facebox')
  52.   **/
  53. public static function removeModule($key)
  54. {
  55. unset (self::$needs[$key . '-js']);
  56. unset (self::$needs[$key . '-css']);
  57. }
  58.  
  59. /**
  60.   * Adds a CSS include need.
  61.   *
  62.   * @param string $url The URL of the CSS file to include
  63.   * @param array $extra_attrs Extra attributes to add to the LINK tag
  64.   **/
  65. public static function addCssInclude($url, $extra_attrs = null, $key = null)
  66. {
  67. if (! isset($extra_attrs['href'])) $extra_attrs['href'] = $url;
  68. if (! isset($extra_attrs['rel'])) $extra_attrs['rel'] = 'stylesheet';
  69.  
  70. $need = '<link' . Html::attributes($extra_attrs) . '>';
  71.  
  72. self::addNeed($need, $key);
  73. }
  74.  
  75. /**
  76.   * Adds a JS include need.
  77.   *
  78.   * @param string $url The URL of the CSS file to include
  79.   * @param array $extra_attrs Extra attributes to add to the JAVASCRIPT tag
  80.   **/
  81. public static function addJavascriptInclude($url, $extra_attrs = null, $key = null)
  82. {
  83. if (! isset($extra_attrs['src'])) $extra_attrs['src'] = $url;
  84. if (! isset($extra_attrs['type'])) $extra_attrs['type'] = 'text/javascript';
  85.  
  86. $need = '<script' . Html::attributes($extra_attrs) . '></script>';
  87.  
  88. self::addNeed($need, $key);
  89. }
  90.  
  91.  
  92. /**
  93.   * Loads a specific file group.
  94.   * The files in a group are JS and CSS files with matching names, e.g. file.js and file.css
  95.   * The JS files can be minified, so 'file' will match file.min.js; minified files take precedence.
  96.   *
  97.   * The group can be:
  98.   * 1. a straight file basename e.g. 'my_file', or
  99.   * 2. sprout plus a file basename, e.g. 'sprout/my_file', or
  100.   * 3. a module name plus a file basename, e.g. 'MyModule/my_file'
  101.   *
  102.   * The files must be in the 'media/(js/css)/' directory beneath the relevant path.
  103.   * As per the following exaples:
  104.   *
  105.   * If the group name 'fb' is requested, the following two files will be included, if they are found:
  106.   * - media/css/fb.css
  107.   * - media/js/fb.min.js OR media/js/fb.js
  108.   *
  109.   * If 'sprout/admin_layout' is requested, the following two files will be included, if they are found:
  110.   * - sprout/media/css/admin_layout.css
  111.   * - sprout/media/js/admin_layout.min.js OR sprout/media/js/admin_layout.js
  112.   *
  113.   * If 'Users/users' is requested, the following two files will be included, if found:
  114.   * - modules/Users/media/css/users.css
  115.   * - modules/Users/media/js/users.min.js OR modules/Users/media/js/users.js
  116.   *
  117.   * @param string $name The name of the file group, e.g. 'Forms/admin_fields'
  118.   * @return void
  119.   * @throws Exception if there are no matching JS or CSS files
  120.   */
  121. public static function fileGroup($name)
  122. {
  123. <<<<<<< HEAD
  124. if (Router::$controller != 'Sprout\\Controllers\\AdminController' and @in_array($name, is_array(Kohana::config('sprout.dont_need'))? Kohana::config('sprout.dont_need') : [])) return;
  125. =======
  126. if (Router::$controller != 'Sprout\\Controllers\\AdminController' and in_array($name, Kohana::config('sprout.dont_need') ?? [])) return;
  127. >>>>>>> 577661628531454a9681e3acaf8e91333e8b88cf
  128.  
  129. $rewrite = (php_sapi_name() != 'cli-server');
  130.  
  131. if (strpos($name, '/') === false) {
  132. $name = 'core/' . $name;
  133. }
  134.  
  135. $matches = null;
  136. if (!preg_match('!^([-_a-zA-Z0-9]+?)/(.+?)$!', $name, $matches)) {
  137. return;
  138. }
  139. $section = $matches[1];
  140. $name = $matches[2];
  141.  
  142. if ($section === 'core') {
  143. $srvbase = 'media';
  144. } elseif ($section === 'sprout') {
  145. $srvbase = 'sprout/media';
  146. } else {
  147. $srvbase = "modules/{$matches[1]}/media";
  148. }
  149.  
  150. if ($mtime = @filemtime(DOCROOT . "{$srvbase}/js/{$name}.min.js")) {
  151. $js_file = $rewrite ? "ROOT/media-{$mtime}/{$section}/js/{$name}.min.js" : "ROOT/{$srvbase}/js/{$name}.min.js?{$mtime}";
  152. } else if ($mtime = @filemtime(DOCROOT . "{$srvbase}/js/{$name}.js")) {
  153. $js_file = $rewrite ? "ROOT/media-{$mtime}/{$section}/js/{$name}.js" : "ROOT/{$srvbase}/js/{$name}.js?{$mtime}";
  154. }
  155.  
  156. if ($mtime = @filemtime(DOCROOT . "{$srvbase}/css/{$name}.css")) {
  157. $css_file = $rewrite ? "ROOT/media-{$mtime}/{$section}/css/{$name}.css" : "ROOT/{$srvbase}/css/{$name}.css?{$mtime}";
  158. }
  159.  
  160. if (!empty($js_file)) {
  161. self::addJavascriptInclude($js_file, null, $name . '-js');
  162. }
  163. if (!empty($css_file)) {
  164. self::addCssInclude($css_file, null, $name . '-css');
  165. }
  166. if (empty($js_file) and empty($css_file)) {
  167. throw new Exception('No matching JS or CSS files');
  168. }
  169. }
  170.  
  171.  
  172. /**
  173.   * Alias for {@see Needs::fileGroup}
  174.   * @deprecated Since the nomenclature makes no sense
  175.   * @param string $name
  176.   * @return void
  177.   */
  178. public static function module($name)
  179. {
  180. self::fileGroup($name);
  181. }
  182.  
  183.  
  184. /**
  185.   * Load the Google Maps JavaScript API, including an api key from the sprout config
  186.   */
  187. public static function googleMaps()
  188. {
  189. $key = Kohana::config('sprout.google_maps_key');
  190. if ($key === 'please_generate_me') {
  191. throw new Exception('Google Maps API key has not been specified');
  192. } else if (empty($key)) {
  193. self::addJavascriptInclude('https://maps.google.com/maps/api/js');
  194. } else {
  195. self::addJavascriptInclude('https://maps.google.com/maps/api/js?key=' . $key);
  196. }
  197. }
  198.  
  199.  
  200. /**
  201.   * Load the Google Maps JavaScript API using sprout config key and given callback JS function name
  202.   *
  203.   * @param string $callback Javascript function name
  204.   * @return void
  205.   */
  206. public static function googleMapsAsync($callback)
  207. {
  208. $key = Kohana::config('sprout.google_maps_key');
  209. if ($key === 'please_generate_me') {
  210. throw new Exception('Google Maps API key has not been specified');
  211. }
  212.  
  213. $params = [
  214. 'v' => 3,
  215. 'key' => $key,
  216. 'callback' => $callback,
  217. ];
  218. $url = '//maps.googleapis.com/maps/api/js?' . http_build_query($params);
  219.  
  220. $need = '<script src="' . Enc::html($url) . '" async defer></script>';
  221.  
  222. self::addNeed($need);
  223. }
  224.  
  225.  
  226. /**
  227.   * Load Google Autocomplete API, including api key from sprout config
  228.   */
  229. public static function googlePlaces()
  230. {
  231. $key = Kohana::config('sprout.google_places_key');
  232.  
  233. if ($key == 'please_generate_me') {
  234. throw new Exception('Google Places API key has not been specified');
  235. } elseif (empty($key)) {
  236. self::addJavascriptInclude('https://maps.googleapis.com/maps/api/js?libraries=places');
  237. } else {
  238. self::addJavascriptInclude('https://maps.googleapis.com/maps/api/js?key=' . $key . '&libraries=places');
  239. }
  240. }
  241.  
  242.  
  243. /**
  244.   * Adds a meta tag
  245.   *
  246.   * @param string $name The name of the meta element
  247.   * @param string $content The content of the meta element
  248.   * @param array $extra_attrs Extra attributes to add to the META tag
  249.   **/
  250. public static function addMeta($name, $content, $extra_attrs = null)
  251. {
  252. if (! isset($extra_attrs['name'])) $extra_attrs['name'] = $name;
  253. if (! isset($extra_attrs['content'])) $extra_attrs['content'] = $content;
  254.  
  255. $need = '<meta' . Html::attributes($extra_attrs) . '>';
  256.  
  257. self::addNeed($need);
  258. }
  259.  
  260.  
  261. /**
  262.   * Dynamic loader for <needs/> which have been specified in an AJAX call.
  263.   *
  264.   * Returns HTML of a snippet of JavaScript which does dynamic loading of the needs.
  265.   * Calls the function "dynamicNeedsLoader" located in media/js/common.js, which does
  266.   * the actual loading of the JS or CSS file. The dynamic loader will only load files
  267.   * not currently loaded.
  268.   *
  269.   * This function must be called after all Needs have been specified.
  270.   *
  271.   * @example
  272.   * Needs::fileGroup('fb');
  273.   * echo Needs::ajaxNeedsLoader(); // outputs <script>...</script>
  274.   *
  275.   * @return string HTML Snippet of JavaScript
  276.   */
  277. public static function dynamicNeedsLoader()
  278. {
  279. if (count(self::$needs) == 0) return '';
  280.  
  281. $out = '<script>$(document).ready(function(){' . PHP_EOL;
  282.  
  283. foreach (self::$needs as $tag) {
  284. $tag = trim(self::replacePathsString($tag));
  285.  
  286. // Browsers don't require (or even work) with HTML encoding of <script> tags in HTML5.
  287. // This is in contrast to XHTML which (as it's XML) does require encoding or CDATA.
  288. // The only logic they use is to check for </script> but this breaks the loader.
  289. // The only solution is to convert this to two separate strings.
  290. $tag = Enc::js($tag);
  291. $tag = str_replace('</script>', '</sc" + "ript>', $tag);
  292.  
  293. // The bulk of the work in a function located in media/js/common.js
  294. $out .= 'dynamicNeedsLoader("' . $tag . '");' . PHP_EOL;
  295. }
  296.  
  297. $out .= '});</script>' . PHP_EOL;
  298.  
  299. return $out;
  300. }
  301.  
  302.  
  303. /**
  304.   * Add data to GTM dataLayers
  305.   *
  306.   * @param array $data
  307.   * @return void
  308.   */
  309. public static function addGTMdataLayer($data)
  310. {
  311. Session::Instance();
  312. if (empty($_SESSION['gtm_datalayers'])) $_SESSION['gtm_datalayers'] = [];
  313. $_SESSION['gtm_datalayers'][] = $data;
  314. }
  315.  
  316.  
  317. /**
  318.   * Render GTM dataLayers
  319.   *
  320.   * @return string HTML
  321.   */
  322. public static function renderGTMDataLayers()
  323. {
  324. Session::Instance();
  325. if (empty($_SESSION['gtm_datalayers'])) return;
  326.  
  327. $out = '<script>';
  328. $out .= 'var dataLayer = window.dataLayer || [];';
  329.  
  330. foreach ($_SESSION['gtm_datalayers'] as $data) {
  331. $out .= 'dataLayer.push(' . json_encode($data) . ');';
  332. }
  333.  
  334. $out .= '</script>';
  335.  
  336. unset($_SESSION['gtm_datalayers']);
  337. return $out;
  338. }
  339.  
  340.  
  341. /**
  342.   * Does needs replacement on all of the html
  343.   **/
  344. public static function replacePlaceholders()
  345. {
  346. // Don't do anything if the output isn't HTML
  347. $headers = headers_list();
  348. $is_html = false;
  349. foreach ($headers as $header) {
  350. if (preg_match('#^Content-type:\s*text/html#i', $header)) {
  351. $is_html = true;
  352. break;
  353. }
  354. }
  355. if (!$is_html) return;
  356.  
  357. // GTM data layers
  358. self::addNeed(self::renderGTMDataLayers(), 'gtm_datalayer');
  359.  
  360. // Needs
  361. Event::$data = preg_replace ('!<needs\s?/?>!', implode ("\n\t", self::$needs), Event::$data);
  362.  
  363. // Path stuff
  364. Event::$data = str_replace ('ROOT/', Kohana::config('core.site_domain'), Event::$data);
  365. Event::$data = str_replace ('SITE/', Url::base(TRUE), Event::$data);
  366. Event::$data = str_replace ('SKIN/', Kohana::config('core.site_domain') . 'skin/' . SubsiteSelector::$subsite_code . '/', Event::$data);
  367.  
  368. // Page links to use slugs instead of IDs
  369. Event::$data = ContentReplace::intlinks(Event::$data);
  370. }
  371.  
  372.  
  373. /**
  374. * Do the path replacements for a provided string
  375. **/
  376. public static function replacePathsString($str)
  377. {
  378. $str = str_replace ('ROOT/', Kohana::config('core.site_domain'), $str);
  379. $str = str_replace ('SITE/', Url::base(TRUE), $str);
  380. $str = str_replace ('SKIN/', Kohana::config('core.site_domain') . 'skin/' . SubsiteSelector::$subsite_code . '/', $str);
  381. return $str;
  382. }
  383.  
  384. }
  385.