Uri.php 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386
  1. <?php
  2. /**
  3. * @package Grav.Common
  4. *
  5. * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use Grav\Common\Config\Config;
  10. use Grav\Common\Language\Language;
  11. use Grav\Common\Page\Page;
  12. use Grav\Common\Page\Pages;
  13. use Grav\Framework\Route\RouteFactory;
  14. use Grav\Framework\Uri\UriFactory;
  15. use Grav\Framework\Uri\UriPartsFilter;
  16. use RocketTheme\Toolbox\Event\Event;
  17. class Uri
  18. {
  19. const HOSTNAME_REGEX = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/';
  20. /** @var \Grav\Framework\Uri\Uri */
  21. protected static $currentUri;
  22. /** @var \Grav\Framework\Route\Route */
  23. protected static $currentRoute;
  24. public $url;
  25. // Uri parts.
  26. protected $scheme;
  27. protected $user;
  28. protected $password;
  29. protected $host;
  30. protected $port;
  31. protected $path;
  32. protected $query;
  33. protected $fragment;
  34. // Internal stuff.
  35. protected $base;
  36. protected $basename;
  37. protected $content_path;
  38. protected $extension;
  39. protected $env;
  40. protected $paths;
  41. protected $queries;
  42. protected $params;
  43. protected $root;
  44. protected $root_path;
  45. protected $uri;
  46. protected $content_type;
  47. protected $post;
  48. /**
  49. * Uri constructor.
  50. * @param string|array $env
  51. */
  52. public function __construct($env = null)
  53. {
  54. if (is_string($env)) {
  55. $this->createFromString($env);
  56. } else {
  57. $this->createFromEnvironment(is_array($env) ? $env : $_SERVER);
  58. }
  59. }
  60. /**
  61. * Initialize the URI class with a url passed via parameter.
  62. * Used for testing purposes.
  63. *
  64. * @param string $url the URL to use in the class
  65. *
  66. * @return $this
  67. */
  68. public function initializeWithUrl($url = '')
  69. {
  70. if ($url) {
  71. $this->createFromString($url);
  72. }
  73. return $this;
  74. }
  75. /**
  76. * Initialize the URI class by providing url and root_path arguments
  77. *
  78. * @param string $url
  79. * @param string $root_path
  80. *
  81. * @return $this
  82. */
  83. public function initializeWithUrlAndRootPath($url, $root_path)
  84. {
  85. $this->initializeWithUrl($url);
  86. $this->root_path = $root_path;
  87. return $this;
  88. }
  89. /**
  90. * Validate a hostname
  91. *
  92. * @param string $hostname The hostname
  93. *
  94. * @return boolean
  95. */
  96. public function validateHostname($hostname)
  97. {
  98. return (bool)preg_match(static::HOSTNAME_REGEX, $hostname);
  99. }
  100. /**
  101. * Initializes the URI object based on the url set on the object
  102. */
  103. public function init()
  104. {
  105. $grav = Grav::instance();
  106. /** @var Config $config */
  107. $config = $grav['config'];
  108. /** @var Language $language */
  109. $language = $grav['language'];
  110. // add the port to the base for non-standard ports
  111. if ($this->port !== null && $config->get('system.reverse_proxy_setup') === false) {
  112. $this->base .= ':' . (string)$this->port;
  113. }
  114. // Handle custom base
  115. $custom_base = rtrim($grav['config']->get('system.custom_base_url'), '/');
  116. if ($custom_base) {
  117. $custom_parts = parse_url($custom_base);
  118. $orig_root_path = $this->root_path;
  119. $this->root_path = isset($custom_parts['path']) ? rtrim($custom_parts['path'], '/') : '';
  120. if (isset($custom_parts['scheme'])) {
  121. $this->base = $custom_parts['scheme'] . '://' . $custom_parts['host'];
  122. $this->root = $custom_base;
  123. } else {
  124. $this->root = $this->base . $this->root_path;
  125. }
  126. $this->uri = Utils::replaceFirstOccurrence($orig_root_path, $this->root_path, $this->uri);
  127. } else {
  128. $this->root = $this->base . $this->root_path;
  129. }
  130. $this->url = $this->base . $this->uri;
  131. $uri = str_replace(static::filterPath($this->root), '', $this->url);
  132. // remove the setup.php based base if set:
  133. $setup_base = $grav['pages']->base();
  134. if ($setup_base) {
  135. $uri = preg_replace('|^' . preg_quote($setup_base, '|') . '|', '', $uri);
  136. }
  137. // process params
  138. $uri = $this->processParams($uri, $config->get('system.param_sep'));
  139. // set active language
  140. $uri = $language->setActiveFromUri($uri);
  141. // split the URL and params
  142. $bits = parse_url($uri);
  143. //process fragment
  144. if (isset($bits['fragment'])) {
  145. $this->fragment = $bits['fragment'];
  146. }
  147. // Get the path. If there's no path, make sure pathinfo() still returns dirname variable
  148. $path = isset($bits['path']) ? $bits['path'] : '/';
  149. // remove the extension if there is one set
  150. $parts = pathinfo($path);
  151. // set the original basename
  152. $this->basename = $parts['basename'];
  153. // set the extension
  154. if (isset($parts['extension'])) {
  155. $this->extension = $parts['extension'];
  156. }
  157. $valid_page_types = implode('|', $config->get('system.pages.types'));
  158. // Strip the file extension for valid page types
  159. if (preg_match('/\.(' . $valid_page_types . ')$/', $parts['basename'])) {
  160. $path = rtrim(str_replace(DIRECTORY_SEPARATOR, DS, $parts['dirname']), DS) . '/' . $parts['filename'];
  161. }
  162. // set the new url
  163. $this->url = $this->root . $path;
  164. $this->path = static::cleanPath($path);
  165. $this->content_path = trim(str_replace($this->base, '', $this->path), '/');
  166. if ($this->content_path !== '') {
  167. $this->paths = explode('/', $this->content_path);
  168. }
  169. // Set some Grav stuff
  170. $grav['base_url_absolute'] = $config->get('system.custom_base_url') ?: $this->rootUrl(true);
  171. $grav['base_url_relative'] = $this->rootUrl(false);
  172. $grav['base_url'] = $config->get('system.absolute_urls') ? $grav['base_url_absolute'] : $grav['base_url_relative'];
  173. RouteFactory::setRoot($this->root_path);
  174. RouteFactory::setLanguage($language->getLanguageURLPrefix());
  175. }
  176. /**
  177. * Return URI path.
  178. *
  179. * @param string $id
  180. *
  181. * @return string|string[]
  182. */
  183. public function paths($id = null)
  184. {
  185. if ($id !== null) {
  186. return $this->paths[$id];
  187. }
  188. return $this->paths;
  189. }
  190. /**
  191. * Return route to the current URI. By default route doesn't include base path.
  192. *
  193. * @param bool $absolute True to include full path.
  194. * @param bool $domain True to include domain. Works only if first parameter is also true.
  195. *
  196. * @return string
  197. */
  198. public function route($absolute = false, $domain = false)
  199. {
  200. return ($absolute ? $this->rootUrl($domain) : '') . '/' . implode('/', $this->paths);
  201. }
  202. /**
  203. * Return full query string or a single query attribute.
  204. *
  205. * @param string $id Optional attribute. Get a single query attribute if set
  206. * @param bool $raw If true and $id is not set, return the full query array. Otherwise return the query string
  207. *
  208. * @return string|array Returns an array if $id = null and $raw = true
  209. */
  210. public function query($id = null, $raw = false)
  211. {
  212. if ($id !== null) {
  213. return isset($this->queries[$id]) ? $this->queries[$id] : null;
  214. }
  215. if ($raw) {
  216. return $this->queries;
  217. }
  218. if (!$this->queries) {
  219. return '';
  220. }
  221. return http_build_query($this->queries);
  222. }
  223. /**
  224. * Return all or a single query parameter as a URI compatible string.
  225. *
  226. * @param string $id Optional parameter name.
  227. * @param boolean $array return the array format or not
  228. *
  229. * @return null|string|array
  230. */
  231. public function params($id = null, $array = false)
  232. {
  233. $config = Grav::instance()['config'];
  234. $sep = $config->get('system.param_sep');
  235. $params = null;
  236. if ($id === null) {
  237. if ($array) {
  238. return $this->params;
  239. }
  240. $output = [];
  241. foreach ($this->params as $key => $value) {
  242. $output[] = "{$key}{$sep}{$value}";
  243. $params = '/' . implode('/', $output);
  244. }
  245. } elseif (isset($this->params[$id])) {
  246. if ($array) {
  247. return $this->params[$id];
  248. }
  249. $params = "/{$id}{$sep}{$this->params[$id]}";
  250. }
  251. return $params;
  252. }
  253. /**
  254. * Get URI parameter.
  255. *
  256. * @param string $id
  257. *
  258. * @return bool|string
  259. */
  260. public function param($id)
  261. {
  262. if (isset($this->params[$id])) {
  263. return html_entity_decode(rawurldecode($this->params[$id]));
  264. }
  265. return false;
  266. }
  267. /**
  268. * Gets the Fragment portion of a URI (eg #target)
  269. *
  270. * @param string $fragment
  271. *
  272. * @return string|null
  273. */
  274. public function fragment($fragment = null)
  275. {
  276. if ($fragment !== null) {
  277. $this->fragment = $fragment;
  278. }
  279. return $this->fragment;
  280. }
  281. /**
  282. * Return URL.
  283. *
  284. * @param bool $include_host Include hostname.
  285. *
  286. * @return string
  287. */
  288. public function url($include_host = false)
  289. {
  290. if ($include_host) {
  291. return $this->url;
  292. }
  293. $url = str_replace($this->base, '', rtrim($this->url, '/'));
  294. return $url ?: '/';
  295. }
  296. /**
  297. * Return the Path
  298. *
  299. * @return String The path of the URI
  300. */
  301. public function path()
  302. {
  303. return $this->path;
  304. }
  305. /**
  306. * Return the Extension of the URI
  307. *
  308. * @param string|null $default
  309. *
  310. * @return string The extension of the URI
  311. */
  312. public function extension($default = null)
  313. {
  314. if (!$this->extension) {
  315. $this->extension = $default;
  316. }
  317. return $this->extension;
  318. }
  319. public function method()
  320. {
  321. $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
  322. if ($method === 'POST' && isset($_SERVER['X-HTTP-METHOD-OVERRIDE'])) {
  323. $method = strtoupper($_SERVER['X-HTTP-METHOD-OVERRIDE']);
  324. }
  325. return $method;
  326. }
  327. /**
  328. * Return the scheme of the URI
  329. *
  330. * @param bool $raw
  331. * @return string The scheme of the URI
  332. */
  333. public function scheme($raw = false)
  334. {
  335. if (!$raw) {
  336. $scheme = '';
  337. if ($this->scheme) {
  338. $scheme = $this->scheme . '://';
  339. } elseif ($this->host) {
  340. $scheme = '//';
  341. }
  342. return $scheme;
  343. }
  344. return $this->scheme;
  345. }
  346. /**
  347. * Return the host of the URI
  348. *
  349. * @return string|null The host of the URI
  350. */
  351. public function host()
  352. {
  353. return $this->host;
  354. }
  355. /**
  356. * Return the port number if it can be figured out
  357. *
  358. * @param bool $raw
  359. * @return int|null
  360. */
  361. public function port($raw = false)
  362. {
  363. $port = $this->port;
  364. // If not in raw mode and port is not set, figure it out from scheme.
  365. if (!$raw && $port === null) {
  366. if ($this->scheme === 'http') {
  367. $this->port = 80;
  368. } elseif ($this->scheme === 'https') {
  369. $this->port = 443;
  370. }
  371. }
  372. return $this->port;
  373. }
  374. /**
  375. * Return user
  376. *
  377. * @return string|null
  378. */
  379. public function user()
  380. {
  381. return $this->user;
  382. }
  383. /**
  384. * Return password
  385. *
  386. * @return string|null
  387. */
  388. public function password()
  389. {
  390. return $this->password;
  391. }
  392. /**
  393. * Gets the environment name
  394. *
  395. * @return String
  396. */
  397. public function environment()
  398. {
  399. return $this->env;
  400. }
  401. /**
  402. * Return the basename of the URI
  403. *
  404. * @return String The basename of the URI
  405. */
  406. public function basename()
  407. {
  408. return $this->basename;
  409. }
  410. /**
  411. * Return the full uri
  412. *
  413. * @param bool $include_root
  414. * @return mixed
  415. */
  416. public function uri($include_root = true)
  417. {
  418. if ($include_root) {
  419. return $this->uri;
  420. }
  421. return str_replace($this->root_path, '', $this->uri);
  422. }
  423. /**
  424. * Return the base of the URI
  425. *
  426. * @return String The base of the URI
  427. */
  428. public function base()
  429. {
  430. return $this->base;
  431. }
  432. /**
  433. * Return the base relative URL including the language prefix
  434. * or the base relative url if multi-language is not enabled
  435. *
  436. * @return String The base of the URI
  437. */
  438. public function baseIncludingLanguage()
  439. {
  440. $grav = Grav::instance();
  441. /** @var Pages $pages */
  442. $pages = $grav['pages'];
  443. return $pages->baseUrl(null, false);
  444. }
  445. /**
  446. * Return root URL to the site.
  447. *
  448. * @param bool $include_host Include hostname.
  449. *
  450. * @return mixed
  451. */
  452. public function rootUrl($include_host = false)
  453. {
  454. if ($include_host) {
  455. return $this->root;
  456. }
  457. return str_replace($this->base, '', $this->root);
  458. }
  459. /**
  460. * Return current page number.
  461. *
  462. * @return int
  463. */
  464. public function currentPage()
  465. {
  466. return isset($this->params['page']) ? $this->params['page'] : 1;
  467. }
  468. /**
  469. * Return relative path to the referrer defaulting to current or given page.
  470. *
  471. * @param string $default
  472. * @param string $attributes
  473. *
  474. * @return string
  475. */
  476. public function referrer($default = null, $attributes = null)
  477. {
  478. $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
  479. // Check that referrer came from our site.
  480. $root = $this->rootUrl(true);
  481. if ($referrer) {
  482. // Referrer should always have host set and it should come from the same base address.
  483. if (stripos($referrer, $root) !== 0) {
  484. $referrer = null;
  485. }
  486. }
  487. if (!$referrer) {
  488. $referrer = $default ?: $this->route(true, true);
  489. }
  490. if ($attributes) {
  491. $referrer .= $attributes;
  492. }
  493. // Return relative path.
  494. return substr($referrer, strlen($root));
  495. }
  496. public function __toString()
  497. {
  498. return static::buildUrl($this->toArray());
  499. }
  500. public function toArray()
  501. {
  502. return [
  503. 'scheme' => $this->scheme,
  504. 'host' => $this->host,
  505. 'port' => $this->port,
  506. 'user' => $this->user,
  507. 'pass' => $this->password,
  508. 'path' => $this->path,
  509. 'params' => $this->params,
  510. 'query' => $this->query,
  511. 'fragment' => $this->fragment
  512. ];
  513. }
  514. /**
  515. * Calculate the parameter regex based on the param_sep setting
  516. *
  517. * @return string
  518. */
  519. public static function paramsRegex()
  520. {
  521. return '/\/([^\:\#\/\?]*' . Grav::instance()['config']->get('system.param_sep') . '[^\:\#\/\?]*)/';
  522. }
  523. /**
  524. * Return the IP address of the current user
  525. *
  526. * @return string ip address
  527. */
  528. public static function ip()
  529. {
  530. if (getenv('HTTP_CLIENT_IP')) {
  531. $ip = getenv('HTTP_CLIENT_IP');
  532. } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
  533. $ip = getenv('HTTP_X_FORWARDED_FOR');
  534. } elseif (getenv('HTTP_X_FORWARDED')) {
  535. $ip = getenv('HTTP_X_FORWARDED');
  536. } elseif (getenv('HTTP_FORWARDED_FOR')) {
  537. $ip = getenv('HTTP_FORWARDED_FOR');
  538. } elseif (getenv('HTTP_FORWARDED')) {
  539. $ip = getenv('HTTP_FORWARDED');
  540. } elseif (getenv('REMOTE_ADDR')){
  541. $ip = getenv('REMOTE_ADDR');
  542. } else {
  543. $ip = 'UNKNOWN';
  544. }
  545. return $ip;
  546. }
  547. /**
  548. * Returns current Uri.
  549. *
  550. * @return \Grav\Framework\Uri\Uri
  551. */
  552. public static function getCurrentUri()
  553. {
  554. if (!static::$currentUri) {
  555. static::$currentUri = UriFactory::createFromEnvironment($_SERVER);
  556. }
  557. return static::$currentUri;
  558. }
  559. /**
  560. * Returns current route.
  561. *
  562. * @return \Grav\Framework\Route\Route
  563. */
  564. public static function getCurrentRoute()
  565. {
  566. if (!static::$currentRoute) {
  567. $uri = Grav::instance()['uri'];
  568. static::$currentRoute = RouteFactory::createFromParts($uri->toArray());
  569. }
  570. return static::$currentRoute;
  571. }
  572. /**
  573. * Is this an external URL? if it starts with `http` then yes, else false
  574. *
  575. * @param string $url the URL in question
  576. *
  577. * @return boolean is eternal state
  578. */
  579. public static function isExternal($url)
  580. {
  581. return Utils::startsWith($url, 'http');
  582. }
  583. /**
  584. * The opposite of built-in PHP method parse_url()
  585. *
  586. * @param array $parsed_url
  587. *
  588. * @return string
  589. */
  590. public static function buildUrl($parsed_url)
  591. {
  592. $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . ':' : '';
  593. $authority = isset($parsed_url['host']) ? '//' : '';
  594. $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
  595. $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
  596. $user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
  597. $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
  598. $pass = ($user || $pass) ? "{$pass}@" : '';
  599. $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
  600. $path = !empty($parsed_url['params']) ? rtrim($path, '/') . static::buildParams($parsed_url['params']) : $path;
  601. $query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
  602. $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
  603. return "{$scheme}{$authority}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}";
  604. }
  605. /**
  606. * @param array $params
  607. * @return string
  608. */
  609. public static function buildParams(array $params)
  610. {
  611. if (!$params) {
  612. return '';
  613. }
  614. $grav = Grav::instance();
  615. $sep = $grav['config']->get('system.param_sep');
  616. $output = [];
  617. foreach ($params as $key => $value) {
  618. $output[] = "{$key}{$sep}{$value}";
  619. }
  620. return '/' . implode('/', $output);
  621. }
  622. /**
  623. * Converts links from absolute '/' or relative (../..) to a Grav friendly format
  624. *
  625. * @param Page $page the current page to use as reference
  626. * @param string|array $url the URL as it was written in the markdown
  627. * @param string $type the type of URL, image | link
  628. * @param bool $absolute if null, will use system default, if true will use absolute links internally
  629. * @param bool $route_only only return the route, not full URL path
  630. * @return string the more friendly formatted url
  631. */
  632. public static function convertUrl(Page $page, $url, $type = 'link', $absolute = false, $route_only = false)
  633. {
  634. $grav = Grav::instance();
  635. $uri = $grav['uri'];
  636. // Link processing should prepend language
  637. $language = $grav['language'];
  638. $language_append = '';
  639. if ($type === 'link' && $language->enabled()) {
  640. $language_append = $language->getLanguageURLPrefix();
  641. }
  642. // Handle Excerpt style $url array
  643. $url_path = is_array($url) ? $url['path'] : $url;
  644. $external = false;
  645. $base = $grav['base_url_relative'];
  646. $base_url = rtrim($base . $grav['pages']->base(), '/') . $language_append;
  647. $pages_dir = $grav['locator']->findResource('page://');
  648. // if absolute and starts with a base_url move on
  649. if (isset($url['scheme']) && Utils::startsWith($url['scheme'], 'http')) {
  650. $external = true;
  651. } elseif ($url_path === '' && isset($url['fragment'])) {
  652. $external = true;
  653. } elseif ($url_path === '/' || ($base_url !== '' && Utils::startsWith($url_path, $base_url))) {
  654. $url_path = $base_url . $url_path;
  655. } else {
  656. // see if page is relative to this or absolute
  657. if (Utils::startsWith($url_path, '/')) {
  658. $normalized_url = Utils::normalizePath($base_url . $url_path);
  659. $normalized_path = Utils::normalizePath($pages_dir . $url_path);
  660. } else {
  661. $page_route = ($page->home() && !empty($url_path)) ? $page->rawRoute() : $page->route();
  662. $normalized_url = $base_url . Utils::normalizePath($page_route . '/' . $url_path);
  663. $normalized_path = Utils::normalizePath($page->path() . '/' . $url_path);
  664. }
  665. // special check to see if path checking is required.
  666. $just_path = str_replace($normalized_url, '', $normalized_path);
  667. if ($normalized_url === '/' || $just_path === $page->path()) {
  668. $url_path = $normalized_url;
  669. } else {
  670. $url_bits = static::parseUrl($normalized_path);
  671. $full_path = $url_bits['path'];
  672. $raw_full_path = rawurldecode($full_path);
  673. if (file_exists($raw_full_path)) {
  674. $full_path = $raw_full_path;
  675. } elseif (!file_exists($full_path)) {
  676. $full_path = false;
  677. }
  678. if ($full_path) {
  679. $path_info = pathinfo($full_path);
  680. $page_path = $path_info['dirname'];
  681. $filename = '';
  682. if ($url_path === '..') {
  683. $page_path = $full_path;
  684. } else {
  685. // save the filename if a file is part of the path
  686. if (is_file($full_path)) {
  687. if ($path_info['extension'] !== 'md') {
  688. $filename = '/' . $path_info['basename'];
  689. }
  690. } else {
  691. $page_path = $full_path;
  692. }
  693. }
  694. // get page instances and try to find one that fits
  695. $instances = $grav['pages']->instances();
  696. if (isset($instances[$page_path])) {
  697. /** @var Page $target */
  698. $target = $instances[$page_path];
  699. $url_bits['path'] = $base_url . rtrim($target->route(), '/') . $filename;
  700. $url_path = Uri::buildUrl($url_bits);
  701. } else {
  702. $url_path = $normalized_url;
  703. }
  704. } else {
  705. $url_path = $normalized_url;
  706. }
  707. }
  708. }
  709. // handle absolute URLs
  710. if (is_array($url) && !$external && ($absolute === true || $grav['config']->get('system.absolute_urls', false))) {
  711. $url['scheme'] = $uri->scheme(true);
  712. $url['host'] = $uri->host();
  713. $url['port'] = $uri->port(true);
  714. // check if page exists for this route, and if so, check if it has SSL enabled
  715. $pages = $grav['pages'];
  716. $routes = $pages->routes();
  717. // if this is an image, get the proper path
  718. $url_bits = pathinfo($url_path);
  719. if (isset($url_bits['extension'])) {
  720. $target_path = $url_bits['dirname'];
  721. } else {
  722. $target_path = $url_path;
  723. }
  724. // strip base from this path
  725. $target_path = str_replace($uri->rootUrl(), '', $target_path);
  726. // set to / if root
  727. if (empty($target_path)) {
  728. $target_path = '/';
  729. }
  730. // look to see if this page exists and has ssl enabled
  731. if (isset($routes[$target_path])) {
  732. $target_page = $pages->get($routes[$target_path]);
  733. if ($target_page) {
  734. $ssl_enabled = $target_page->ssl();
  735. if ($ssl_enabled !== null) {
  736. if ($ssl_enabled) {
  737. $url['scheme'] = 'https';
  738. } else {
  739. $url['scheme'] = 'http';
  740. }
  741. }
  742. }
  743. }
  744. }
  745. // Handle route only
  746. if ($route_only) {
  747. $url_path = str_replace(static::filterPath($base_url), '', $url_path);
  748. }
  749. // transform back to string/array as needed
  750. if (is_array($url)) {
  751. $url['path'] = $url_path;
  752. } else {
  753. $url = $url_path;
  754. }
  755. return $url;
  756. }
  757. public static function parseUrl($url)
  758. {
  759. $grav = Grav::instance();
  760. $encodedUrl = preg_replace_callback(
  761. '%[^:/@?&=#]+%usD',
  762. function ($matches) { return rawurlencode($matches[0]); },
  763. $url
  764. );
  765. $parts = parse_url($encodedUrl);
  766. if (false === $parts) {
  767. return false;
  768. }
  769. foreach($parts as $name => $value) {
  770. $parts[$name] = rawurldecode($value);
  771. }
  772. if (!isset($parts['path'])) {
  773. $parts['path'] = '';
  774. }
  775. list($stripped_path, $params) = static::extractParams($parts['path'], $grav['config']->get('system.param_sep'));
  776. if (!empty($params)) {
  777. $parts['path'] = $stripped_path;
  778. $parts['params'] = $params;
  779. }
  780. return $parts;
  781. }
  782. public static function extractParams($uri, $delimiter)
  783. {
  784. $params = [];
  785. if (strpos($uri, $delimiter) !== false) {
  786. preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER);
  787. foreach ($matches as $match) {
  788. $param = explode($delimiter, $match[1]);
  789. if (count($param) === 2) {
  790. $plain_var = filter_var(rawurldecode($param[1]), FILTER_SANITIZE_STRING);
  791. $params[$param[0]] = $plain_var;
  792. $uri = str_replace($match[0], '', $uri);
  793. }
  794. }
  795. }
  796. return [$uri, $params];
  797. }
  798. /**
  799. * Converts links from absolute '/' or relative (../..) to a Grav friendly format
  800. *
  801. * @param Page $page the current page to use as reference
  802. * @param string $markdown_url the URL as it was written in the markdown
  803. * @param string $type the type of URL, image | link
  804. * @param null $relative if null, will use system default, if true will use relative links internally
  805. *
  806. * @return string the more friendly formatted url
  807. */
  808. public static function convertUrlOld(Page $page, $markdown_url, $type = 'link', $relative = null)
  809. {
  810. $grav = Grav::instance();
  811. $language = $grav['language'];
  812. // Link processing should prepend language
  813. $language_append = '';
  814. if ($type === 'link' && $language->enabled()) {
  815. $language_append = $language->getLanguageURLPrefix();
  816. }
  817. $pages_dir = $grav['locator']->findResource('page://');
  818. if ($relative === null) {
  819. $base = $grav['base_url'];
  820. } else {
  821. $base = $relative ? $grav['base_url_relative'] : $grav['base_url_absolute'];
  822. }
  823. $base_url = rtrim($base . $grav['pages']->base(), '/') . $language_append;
  824. // if absolute and starts with a base_url move on
  825. if (pathinfo($markdown_url, PATHINFO_DIRNAME) === '.' && $page->url() === '/') {
  826. return '/' . $markdown_url;
  827. }
  828. // no path to convert
  829. if ($base_url !== '' && Utils::startsWith($markdown_url, $base_url)) {
  830. return $markdown_url;
  831. }
  832. // if contains only a fragment
  833. if (Utils::startsWith($markdown_url, '#')) {
  834. return $markdown_url;
  835. }
  836. $target = null;
  837. // see if page is relative to this or absolute
  838. if (Utils::startsWith($markdown_url, '/')) {
  839. $normalized_url = Utils::normalizePath($base_url . $markdown_url);
  840. $normalized_path = Utils::normalizePath($pages_dir . $markdown_url);
  841. } else {
  842. $normalized_url = $base_url . Utils::normalizePath($page->route() . '/' . $markdown_url);
  843. $normalized_path = Utils::normalizePath($page->path() . '/' . $markdown_url);
  844. }
  845. // special check to see if path checking is required.
  846. $just_path = str_replace($normalized_url, '', $normalized_path);
  847. if ($just_path === $page->path()) {
  848. return $normalized_url;
  849. }
  850. $url_bits = parse_url($normalized_path);
  851. $full_path = $url_bits['path'];
  852. if (file_exists($full_path)) {
  853. // do nothing
  854. } elseif (file_exists(rawurldecode($full_path))) {
  855. $full_path = rawurldecode($full_path);
  856. } else {
  857. return $normalized_url;
  858. }
  859. $path_info = pathinfo($full_path);
  860. $page_path = $path_info['dirname'];
  861. $filename = '';
  862. if ($markdown_url === '..') {
  863. $page_path = $full_path;
  864. } else {
  865. // save the filename if a file is part of the path
  866. if (is_file($full_path)) {
  867. if ($path_info['extension'] !== 'md') {
  868. $filename = '/' . $path_info['basename'];
  869. }
  870. } else {
  871. $page_path = $full_path;
  872. }
  873. }
  874. // get page instances and try to find one that fits
  875. $instances = $grav['pages']->instances();
  876. if (isset($instances[$page_path])) {
  877. /** @var Page $target */
  878. $target = $instances[$page_path];
  879. $url_bits['path'] = $base_url . rtrim($target->route(), '/') . $filename;
  880. return static::buildUrl($url_bits);
  881. }
  882. return $normalized_url;
  883. }
  884. /**
  885. * Adds the nonce to a URL for a specific action
  886. *
  887. * @param string $url the url
  888. * @param string $action the action
  889. * @param string $nonceParamName the param name to use
  890. *
  891. * @return string the url with the nonce
  892. */
  893. public static function addNonce($url, $action, $nonceParamName = 'nonce')
  894. {
  895. $fake = $url && $url[0] === '/';
  896. if ($fake) {
  897. $url = 'http://domain.com' . $url;
  898. }
  899. $uri = new static($url);
  900. $parts = $uri->toArray();
  901. $nonce = Utils::getNonce($action);
  902. $parts['params'] = (isset($parts['params']) ? $parts['params'] : []) + [$nonceParamName => $nonce];
  903. if ($fake) {
  904. unset($parts['scheme'], $parts['host']);
  905. }
  906. return static::buildUrl($parts);
  907. }
  908. /**
  909. * Is the passed in URL a valid URL?
  910. *
  911. * @param $url
  912. * @return bool
  913. */
  914. public static function isValidUrl($url)
  915. {
  916. $regex = '/^(?:(https?|ftp|telnet):)?\/\/((?:[a-z0-9@:.-]|%[0-9A-F]{2}){3,})(?::(\d+))?((?:\/(?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\@]|%[0-9A-F]{2})*)*)(?:\?((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?/';
  917. if (preg_match($regex, $url)) {
  918. return true;
  919. }
  920. return false;
  921. }
  922. /**
  923. * Removes extra double slashes and fixes back-slashes
  924. *
  925. * @param $path
  926. * @return mixed|string
  927. */
  928. public static function cleanPath($path)
  929. {
  930. $regex = '/(\/)\/+/';
  931. $path = str_replace(['\\', '/ /'], '/', $path);
  932. $path = preg_replace($regex,'$1',$path);
  933. return $path;
  934. }
  935. /**
  936. * Filters the user info string.
  937. *
  938. * @param string $info The raw user or password.
  939. * @return string The percent-encoded user or password string.
  940. */
  941. public static function filterUserInfo($info)
  942. {
  943. return $info !== null ? UriPartsFilter::filterUserInfo($info) : '';
  944. }
  945. /**
  946. * Filter Uri path.
  947. *
  948. * This method percent-encodes all reserved
  949. * characters in the provided path string. This method
  950. * will NOT double-encode characters that are already
  951. * percent-encoded.
  952. *
  953. * @param string $path The raw uri path.
  954. * @return string The RFC 3986 percent-encoded uri path.
  955. * @link http://www.faqs.org/rfcs/rfc3986.html
  956. */
  957. public static function filterPath($path)
  958. {
  959. return $path !== null ? UriPartsFilter::filterPath($path) : '';
  960. }
  961. /**
  962. * Filters the query string or fragment of a URI.
  963. *
  964. * @param string $query The raw uri query string.
  965. * @return string The percent-encoded query string.
  966. */
  967. public static function filterQuery($query)
  968. {
  969. return $query !== null ? UriPartsFilter::filterQueryOrFragment($query) : '';
  970. }
  971. /**
  972. * @param array $env
  973. */
  974. protected function createFromEnvironment(array $env)
  975. {
  976. // Build scheme.
  977. if (isset($env['HTTP_X_FORWARDED_PROTO'])) {
  978. $this->scheme = $env['HTTP_X_FORWARDED_PROTO'];
  979. } elseif (isset($env['X-FORWARDED-PROTO'])) {
  980. $this->scheme = $env['X-FORWARDED-PROTO'];
  981. } elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) {
  982. $this->scheme = $env['HTTP_CLOUDFRONT_FORWARDED_PROTO'];
  983. } elseif (isset($env['REQUEST_SCHEME'])) {
  984. $this->scheme = $env['REQUEST_SCHEME'];
  985. } else {
  986. $https = isset($env['HTTPS']) ? $env['HTTPS'] : '';
  987. $this->scheme = (empty($https) || strtolower($https) === 'off') ? 'http' : 'https';
  988. }
  989. // Build user and password.
  990. $this->user = isset($env['PHP_AUTH_USER']) ? $env['PHP_AUTH_USER'] : null;
  991. $this->password = isset($env['PHP_AUTH_PW']) ? $env['PHP_AUTH_PW'] : null;
  992. // Build host.
  993. $hostname = 'localhost';
  994. if (isset($env['HTTP_HOST'])) {
  995. $hostname = $env['HTTP_HOST'];
  996. } elseif (isset($env['SERVER_NAME'])) {
  997. $hostname = $env['SERVER_NAME'];
  998. }
  999. // Remove port from HTTP_HOST generated $hostname
  1000. $hostname = Utils::substrToString($hostname, ':');
  1001. // Validate the hostname
  1002. $this->host = $this->validateHostname($hostname) ? $hostname : 'unknown';
  1003. // Build port.
  1004. if (isset($env['HTTP_X_FORWARDED_PORT'])) {
  1005. $this->port = (int)$env['HTTP_X_FORWARDED_PORT'];
  1006. } elseif (isset($env['X-FORWARDED-PORT'])) {
  1007. $this->port = (int)$env['X-FORWARDED-PORT'];
  1008. } elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) {
  1009. // Since AWS Cloudfront does not provide a forwarded port header,
  1010. // we have to build the port using the scheme.
  1011. $this->port = $this->port();
  1012. } elseif (isset($env['SERVER_PORT'])) {
  1013. $this->port = (int)$env['SERVER_PORT'];
  1014. } else {
  1015. $this->port = null;
  1016. }
  1017. if ($this->hasStandardPort()) {
  1018. $this->port = null;
  1019. }
  1020. // Build path.
  1021. $request_uri = isset($env['REQUEST_URI']) ? $env['REQUEST_URI'] : '';
  1022. $this->path = rawurldecode(parse_url('http://example.com' . $request_uri, PHP_URL_PATH));
  1023. // Build query string.
  1024. $this->query = isset($env['QUERY_STRING']) ? $env['QUERY_STRING'] : '';
  1025. if ($this->query === '') {
  1026. $this->query = parse_url('http://example.com' . $request_uri, PHP_URL_QUERY);
  1027. }
  1028. // Support ngnix routes.
  1029. if (strpos($this->query, '_url=') === 0) {
  1030. parse_str($this->query, $query);
  1031. unset($query['_url']);
  1032. $this->query = http_build_query($query);
  1033. }
  1034. // Build fragment.
  1035. $this->fragment = null;
  1036. // Filter userinfo, path and query string.
  1037. $this->user = $this->user !== null ? static::filterUserInfo($this->user) : null;
  1038. $this->password = $this->password !== null ? static::filterUserInfo($this->password) : null;
  1039. $this->path = empty($this->path) ? '/' : static::filterPath($this->path);
  1040. $this->query = static::filterQuery($this->query);
  1041. $this->reset();
  1042. }
  1043. /**
  1044. * Does this Uri use a standard port?
  1045. *
  1046. * @return bool
  1047. */
  1048. protected function hasStandardPort()
  1049. {
  1050. return ($this->scheme === 'http' && $this->port === 80) || ($this->scheme === 'https' && $this->port === 443);
  1051. }
  1052. /**
  1053. * @param string $url
  1054. */
  1055. protected function createFromString($url)
  1056. {
  1057. // Set Uri parts.
  1058. $parts = parse_url($url);
  1059. if ($parts === false) {
  1060. throw new \RuntimeException('Malformed URL: ' . $url);
  1061. }
  1062. $this->scheme = isset($parts['scheme']) ? $parts['scheme'] : null;
  1063. $this->user = isset($parts['user']) ? $parts['user'] : null;
  1064. $this->password = isset($parts['pass']) ? $parts['pass'] : null;
  1065. $this->host = isset($parts['host']) ? $parts['host'] : null;
  1066. $this->port = isset($parts['port']) ? (int)$parts['port'] : null;
  1067. $this->path = isset($parts['path']) ? $parts['path'] : '';
  1068. $this->query = isset($parts['query']) ? $parts['query'] : '';
  1069. $this->fragment = isset($parts['fragment']) ? $parts['fragment'] : null;
  1070. // Validate the hostname
  1071. if ($this->host) {
  1072. $this->host = $this->validateHostname($this->host) ? $this->host : 'unknown';
  1073. }
  1074. // Filter userinfo, path, query string and fragment.
  1075. $this->user = $this->user !== null ? static::filterUserInfo($this->user) : null;
  1076. $this->password = $this->password !== null ? static::filterUserInfo($this->password) : null;
  1077. $this->path = empty($this->path) ? '/' : static::filterPath($this->path);
  1078. $this->query = static::filterQuery($this->query);
  1079. $this->fragment = $this->fragment !== null ? static::filterQuery($this->fragment) : null;
  1080. $this->reset();
  1081. }
  1082. protected function reset()
  1083. {
  1084. // resets
  1085. parse_str($this->query, $this->queries);
  1086. $this->extension = null;
  1087. $this->basename = null;
  1088. $this->paths = [];
  1089. $this->params = [];
  1090. $this->env = $this->buildEnvironment();
  1091. $this->uri = $this->path . (!empty($this->query) ? '?' . $this->query : '');
  1092. $this->base = $this->buildBaseUrl();
  1093. $this->root_path = $this->buildRootPath();
  1094. $this->root = $this->base . $this->root_path;
  1095. $this->url = $this->base . $this->uri;
  1096. }
  1097. /**
  1098. * Get's post from either $_POST or JSON response object
  1099. * By default returns all data, or can return a single item
  1100. *
  1101. * @param string $element
  1102. * @param string $filter_type
  1103. * @return array|mixed|null
  1104. */
  1105. public function post($element = null, $filter_type = null)
  1106. {
  1107. if (!$this->post) {
  1108. $content_type = $this->getContentType();
  1109. if ($content_type === 'application/json') {
  1110. $json = file_get_contents('php://input');
  1111. $this->post = json_decode($json, true);
  1112. } elseif (!empty($_POST)) {
  1113. $this->post = (array)$_POST;
  1114. }
  1115. $event = new Event(['post' => &$this->post]);
  1116. Grav::instance()->fireEvent('onHttpPostFilter', $event);
  1117. }
  1118. if ($this->post && null !== $element) {
  1119. $item = Utils::getDotNotation($this->post, $element);
  1120. if ($filter_type) {
  1121. $item = filter_var($item, $filter_type);
  1122. }
  1123. return $item;
  1124. }
  1125. return $this->post;
  1126. }
  1127. /**
  1128. * Get content type from request
  1129. *
  1130. * @param bool $short
  1131. * @return null|string
  1132. */
  1133. private function getContentType($short = true)
  1134. {
  1135. if (isset($_SERVER['CONTENT_TYPE'])) {
  1136. $content_type = $_SERVER['CONTENT_TYPE'];
  1137. if ($short) {
  1138. return Utils::substrToString($content_type,';');
  1139. }
  1140. return $content_type;
  1141. }
  1142. return null;
  1143. }
  1144. /**
  1145. * Get the base URI with port if needed
  1146. *
  1147. * @return string
  1148. */
  1149. private function buildBaseUrl()
  1150. {
  1151. return $this->scheme() . $this->host;
  1152. }
  1153. /**
  1154. * Get the Grav Root Path
  1155. *
  1156. * @return string
  1157. */
  1158. private function buildRootPath()
  1159. {
  1160. // In Windows script path uses backslash, convert it:
  1161. $scriptPath = str_replace('\\', '/', $_SERVER['PHP_SELF']);
  1162. $rootPath = str_replace(' ', '%20', rtrim(substr($scriptPath, 0, strpos($scriptPath, 'index.php')), '/'));
  1163. return $rootPath;
  1164. }
  1165. private function buildEnvironment()
  1166. {
  1167. // check for localhost variations
  1168. if ($this->host === '127.0.0.1' || $this->host === '::1') {
  1169. return 'localhost';
  1170. }
  1171. return $this->host ?: 'unknown';
  1172. }
  1173. /**
  1174. * Process any params based in this URL, supports any valid delimiter
  1175. *
  1176. * @param $uri
  1177. * @param string $delimiter
  1178. *
  1179. * @return string
  1180. */
  1181. private function processParams($uri, $delimiter = ':')
  1182. {
  1183. if (strpos($uri, $delimiter) !== false) {
  1184. preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER);
  1185. foreach ($matches as $match) {
  1186. $param = explode($delimiter, $match[1]);
  1187. if (count($param) === 2) {
  1188. $plain_var = filter_var($param[1], FILTER_SANITIZE_STRING);
  1189. $this->params[$param[0]] = $plain_var;
  1190. $uri = str_replace($match[0], '', $uri);
  1191. }
  1192. }
  1193. }
  1194. return $uri;
  1195. }
  1196. }