SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Url.php

Copyright (C) 2017 Karmabunny Pty Ltd.

This file is a part of SproutCMS.

SproutCMS is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

For more information, visit <http://getsproutcms.com>.

This class was originally from Kohana 2.3.4
Copyright 2007-2008 Kohana Team
  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.  * This class was originally from Kohana 2.3.4
  14.  * Copyright 2007-2008 Kohana Team
  15.  */
  16. namespace Sprout\Helpers;
  17.  
  18.  
  19. use Kohana;
  20. use Event;
  21.  
  22. use LogicException;
  23.  
  24. /**
  25.  * Helper functions for working with URLs.
  26.  */
  27. class Url
  28. {
  29.  
  30. /**
  31.   * Fetches the current URI.
  32.   *
  33.   * @param boolean include the query string
  34.   * @return string
  35.   */
  36. public static function current($qs = FALSE)
  37. {
  38. return ($qs === TRUE) ? Router::$complete_uri : Router::$current_uri;
  39. }
  40.  
  41. /**
  42.   * Base URL, with or without the index page.
  43.   *
  44.   * If protocol is specified, a full URL including the domain will be used
  45.   * otherwise only the root path will be used
  46.   *
  47.   * If a subsite-section has a defined URL prefix, it will be added to the URL automatically
  48.   *
  49.   * @param boolean include the index page
  50.   * @param boolean non-default protocol
  51.   * @return string
  52.   */
  53. public static function base($index = FALSE, $protocol = FALSE)
  54. {
  55. // Load the site domain
  56. $site_domain = (string) Kohana::config('core.site_domain', TRUE);
  57.  
  58. if ($protocol == FALSE)
  59. {
  60. // Use the configured site domain
  61. $base_url = $site_domain;
  62. }
  63. else
  64. {
  65. // Guess the server name if the domain starts with slash
  66. $base_url = $protocol . '://' . $_SERVER['HTTP_HOST'] . $site_domain;
  67. }
  68.  
  69. $base_url .= SubsiteSelector::$url_prefix;
  70.  
  71. if ($index === TRUE AND $index = Kohana::config('core.index_page'))
  72. {
  73. // Append the index page
  74. $base_url = $base_url . $index;
  75. }
  76.  
  77. // Force a slash on the end of the URL
  78. $base_url = rtrim($base_url, '/').'/';
  79.  
  80. return $base_url;
  81. }
  82.  
  83. /**
  84.   * Fetches an absolute site URL based on a URI segment.
  85.   *
  86.   * @param string site URI to convert
  87.   * @param string non-default protocol
  88.   * @return string
  89.   */
  90. public static function site($uri = '', $protocol = FALSE)
  91. {
  92. if ($path = trim(parse_url($uri, PHP_URL_PATH), '/'))
  93. {
  94. // Add path suffix
  95. $path .= Kohana::config('core.url_suffix');
  96. }
  97.  
  98. if ($query = parse_url($uri, PHP_URL_QUERY))
  99. {
  100. // ?query=string
  101. $query = '?'.$query;
  102. }
  103.  
  104. if ($fragment = parse_url($uri, PHP_URL_FRAGMENT))
  105. {
  106. // #fragment
  107. $fragment = '#'.$fragment;
  108. }
  109.  
  110. // Strip the base if it is already in the path
  111. $base = trim(Url::base(TRUE), '/');
  112. if ($base != '' and strpos($path, $base) === 0)
  113. {
  114. $path = substr($path, strlen($base) + 1);
  115. }
  116.  
  117. $url = Url::base(TRUE, $protocol).$path.$query.$fragment;
  118. return $url;
  119. }
  120.  
  121. /**
  122.   * Return the URL to a file. Absolute filenames and relative filenames
  123.   * are allowed.
  124.   *
  125.   * @param string filename
  126.   * @param boolean include the index page
  127.   * @return string
  128.   */
  129. public static function file($file, $index = FALSE)
  130. {
  131. if (strpos($file, '://') === FALSE)
  132. {
  133. // Add the base URL to the filename
  134. $file = Url::base($index).$file;
  135. }
  136.  
  137. return $file;
  138. }
  139.  
  140. /**
  141.   * Merges an array of arguments with the current URI and query string to
  142.   * overload, instead of replace, the current query string.
  143.   *
  144.   * @param array associative array of arguments
  145.   * @return string
  146.   */
  147. public static function merge(array $arguments)
  148. {
  149. if ($_GET === $arguments)
  150. {
  151. $query = Router::$query_string;
  152. }
  153. elseif ($query = http_build_query(array_merge($_GET, $arguments)))
  154. {
  155. $query = '?'.$query;
  156. }
  157.  
  158. // Return the current URI with the arguments merged into the query string
  159. return Router::$current_uri.$query;
  160. }
  161.  
  162. /**
  163.   * Sends a page redirect header and runs the system.redirect Event.
  164.   *
  165.   * @param mixed string site URI or URL to redirect to, or array of strings if method is 300
  166.   * @param string HTTP method of redirect
  167.   * @return void
  168.   */
  169. public static function redirect($uri = '', $method = '302')
  170. {
  171. if (Event::hasRun('system.send_headers'))
  172. {
  173. if (!IN_PRODUCTION) {
  174. throw new LogicException("Attempting to redirect after headers have been sent.");
  175. }
  176.  
  177. }
  178.  
  179. $codes = array
  180. (
  181. 'refresh' => 'Refresh',
  182. '300' => 'Multiple Choices',
  183. '301' => 'Moved Permanently',
  184. '302' => 'Found',
  185. '303' => 'See Other',
  186. '304' => 'Not Modified',
  187. '305' => 'Use Proxy',
  188. '307' => 'Temporary Redirect'
  189. );
  190.  
  191. // Validate the method and default to 302
  192. $method = isset($codes[$method]) ? (string) $method : '302';
  193.  
  194. if (strpos($uri, '://') === FALSE)
  195. {
  196. // HTTP headers expect absolute URLs
  197. $uri = Url::site($uri, Request::protocol());
  198. }
  199.  
  200. if ($method === '300')
  201. {
  202. $uri = (array) $uri;
  203.  
  204. $output = '<ul>';
  205. foreach ($uri as $link)
  206. {
  207. $output .= '<li>'.Html::anchor($link).'</li>';
  208. }
  209. $output .= '</ul>';
  210.  
  211. // The first URI will be used for the Location header
  212. $uri = $uri[0];
  213. }
  214. else
  215. {
  216. $output = '<p>'.Html::anchor($uri).'</p>';
  217. }
  218.  
  219. // Run the redirect event
  220. Event::run('system.redirect', $uri);
  221.  
  222. if ($method === 'refresh')
  223. {
  224. header('Refresh: 0; url='.$uri);
  225. }
  226. else
  227. {
  228. header('HTTP/1.1 '.$method.' '.$codes[$method]);
  229. header('Location: '.$uri);
  230. }
  231.  
  232. $trace = debug_backtrace();
  233. $call = $trace[0];
  234. unset($call['args']);
  235. if (!empty($call['file'])) {
  236. unset($call['class'], $call['type'], $call['function']);
  237. }
  238.  
  239. // We are about to exit, so run the send_headers event
  240. Event::run('system.send_headers');
  241.  
  242. // If using a session driver, the session needs to be explicitly saved
  243. Event::run('system.shutdown');
  244.  
  245. exit('<h1>'.$method.' - '.$codes[$method].'</h1>'.$output);
  246. }
  247.  
  248. /**
  249.   * Removes one or more argumens from the current URL, returning a URL which can have arguments appended to it
  250.   * Multiple arguments can be specified
  251.   *
  252.   * If the current URL is:
  253.   * /search?q=test&category=general
  254.   * and the function call is:
  255.   * Url::withoutArgs('q')
  256.   * the resulting URL will be:
  257.   * /search?category=general&
  258.   *
  259.   * Use rtrim($url, '&?') if you do not desire the trailing ? or &
  260.   */
  261. public static function withoutArgs()
  262. {
  263. $url_base = Url::base() . Url::current() . '?';
  264. $get = $_GET;
  265.  
  266. $args = func_get_args();
  267. foreach ($args as $a)
  268. {
  269. unset ($get[$a]);
  270. }
  271.  
  272. if (count($get)) {
  273. $url_base .= http_build_query($get) . '&';
  274. }
  275.  
  276. return $url_base;
  277. }
  278.  
  279.  
  280. /**
  281.   * Checks the provided argument is a valid redirect URL to the local site
  282.   *
  283.   * This is designed to prevent redirects to other domains, bad pages, etc
  284.   *
  285.   * @param string $url
  286.   * @param bool $allow_querysting If true, allow querystrings too
  287.   **/
  288. public static function checkRedirect($url, $allow_querysting = false)
  289. {
  290. if ($url === '') return true;
  291.  
  292. $url = trim(Enc::cleanfunky($url));
  293. if ($url === '') return false;
  294.  
  295. $regex = '/^';
  296. $regex .= '[-_a-zA-Z0-9.\/]+';
  297. if ($allow_querysting) $regex .= '(\?[-_a-zA-Z0-9=&% ]+)?';
  298. $regex .= '$/i';
  299.  
  300. if (! preg_match($regex, $url)) {
  301. return false;
  302. }
  303.  
  304. return true;
  305. }
  306.  
  307.  
  308. /**
  309.   * Add a scheme (e.g. 'http://') to a URL which doesn't have one
  310.   *
  311.   * @param string $url May or may not contain a scheme
  312.   * @return string URL Always contains a scheme
  313.   */
  314. public static function addUrlScheme($url)
  315. {
  316. if (preg_match('!^https?://!i', $url)) {
  317. return $url;
  318. } else {
  319. return 'http://' . $url;
  320. }
  321. }
  322.  
  323.  
  324. /**
  325.   * Add a domain to provided social link, if it doesn't have one
  326.   *
  327.   * @param string $social_link Social link, e.g. 'https://instagram.com/kbtestbot3000' or 'kbtestbot3000'
  328.   * @param string $domain, e.g. 'instagram.com'
  329.   * @return string Social link with domain, e.g. 'https://instagram.com/kbtestbot3000'
  330.   */
  331. public static function addSocialDomain($social_link, $domain)
  332. {
  333. $url = parse_url($social_link);
  334.  
  335. // A URL such as "instagram.com/kbtestbot3000" will be treated as if there isn't a host specified
  336. // As we know the exepcted hostname, it's possible to look for this and make an assumption
  337. if (empty($url['host']) and !empty($url['path'])) {
  338. $domain_loc = strpos($url['path'], $domain . '/');
  339. if ($domain_loc !== false) {
  340. $url['host'] = $domain;
  341. $url['path'] = substr($url['path'], $domain_loc + strlen($domain));
  342. }
  343. }
  344.  
  345. if (empty($url['scheme'])) {
  346. $url['scheme'] = 'https';
  347. } else {
  348. $url['scheme'] = strtolower($url['scheme']);
  349. }
  350.  
  351. if (empty($url['host'])) {
  352. $url['host'] = $domain;
  353. } else {
  354. $url['host'] = strtolower($url['host']);
  355. }
  356.  
  357. if ($url['path'][0] != '/') {
  358. $url['path'] = '/' . $url['path'];
  359. }
  360.  
  361. $out = $url['scheme'] . '://' . $url['host'] . $url['path'];
  362.  
  363. if (!empty($url['query'])) {
  364. $out .= '?' . $url['query'];
  365. }
  366.  
  367. if (!empty($url['fragment'])) {
  368. $out .= '#' . $url['fragment'];
  369. }
  370.  
  371. return $out;
  372. }
  373.  
  374.  
  375. /**
  376.   * Return HTML for canonical URLs
  377.   *
  378.   * @param string $canonical_url
  379.   * @return string HMTL
  380.   */
  381. public static function canonical($canonical_url)
  382. {
  383. $parts = parse_url($canonical_url);
  384.  
  385. // Attempt to determine if 3rd-party or local URL and fill in the blanks
  386. if (empty($parts['scheme'])) $parts['scheme'] = Request::protocol();
  387. if (empty($parts['host'])) $parts['host'] = str_replace($parts['scheme'], '', Sprout::absRoot($parts['scheme']));
  388. if (empty($parts['path'])) $parts['path'] = '';
  389. if (strpos($parts['host'],'://') === false) $parts['host'] = '://' . $parts['host'];
  390.  
  391. $canonical_url = $parts['scheme'] . $parts['host'] . $parts['path'];
  392. return sprintf('<link rel="canonical" href="%s">', Enc::html($canonical_url));
  393. }
  394. }
  395.