PageRenderTime 55ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Url.php

https://github.com/CodeYellowBV/piwik
PHP | 573 lines | 310 code | 48 blank | 215 comment | 56 complexity | bf5178ab078bd6e6c19ba5a0463f4ce2 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. /**
  12. * Provides URL related helper methods.
  13. *
  14. * This class provides simple methods that can be used to parse and modify
  15. * the current URL. It is most useful when plugins need to redirect the current
  16. * request to a URL and when they need to link to other parts of Piwik in
  17. * HTML.
  18. *
  19. * ### Examples
  20. *
  21. * **Redirect to a different controller action**
  22. *
  23. * public function myControllerAction()
  24. * {
  25. * $url = Url::getCurrentQueryStringWithParametersModified(array(
  26. * 'module' => 'UserSettings',
  27. * 'action' => 'index'
  28. * ));
  29. * Url::redirectToUrl($url);
  30. * }
  31. *
  32. * **Link to a different controller action in a template**
  33. *
  34. * public function myControllerAction()
  35. * {
  36. * $url = Url::getCurrentQueryStringWithParametersModified(array(
  37. * 'module' => 'UserCountryMap',
  38. * 'action' => 'realtimeMap',
  39. * 'changeVisitAlpha' => 0,
  40. * 'removeOldVisits' => 0
  41. * ));
  42. * $view = new View("@MyPlugin/myPopup");
  43. * $view->realtimeMapUrl = $url;
  44. * return $view->render();
  45. * }
  46. *
  47. */
  48. class Url
  49. {
  50. /**
  51. * List of hosts that are never checked for validity.
  52. */
  53. private static $alwaysTrustedHosts = array('localhost', '127.0.0.1', '::1', '[::1]');
  54. /**
  55. * Returns the current URL.
  56. *
  57. * @return string eg, `"http://example.org/dir1/dir2/index.php?param1=value1&param2=value2"`
  58. * @api
  59. */
  60. static public function getCurrentUrl()
  61. {
  62. return self::getCurrentScheme() . '://'
  63. . self::getCurrentHost()
  64. . self::getCurrentScriptName()
  65. . self::getCurrentQueryString();
  66. }
  67. /**
  68. * Returns the current URL without the query string.
  69. *
  70. * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true,
  71. * except in {@link Piwik\Plugin\Controller}.
  72. * @return string eg, `"http://example.org/dir1/dir2/index.php"` if the current URL is
  73. * `"http://example.org/dir1/dir2/index.php?param1=value1&param2=value2"`.
  74. * @api
  75. */
  76. static public function getCurrentUrlWithoutQueryString($checkTrustedHost = true)
  77. {
  78. return self::getCurrentScheme() . '://'
  79. . self::getCurrentHost($default = 'unknown', $checkTrustedHost)
  80. . self::getCurrentScriptName();
  81. }
  82. /**
  83. * Returns the current URL without the query string and without the name of the file
  84. * being executed.
  85. *
  86. * @return string eg, `"http://example.org/dir1/dir2/"` if the current URL is
  87. * `"http://example.org/dir1/dir2/index.php?param1=value1&param2=value2"`.
  88. * @api
  89. */
  90. static public function getCurrentUrlWithoutFileName()
  91. {
  92. return self::getCurrentScheme() . '://'
  93. . self::getCurrentHost()
  94. . self::getCurrentScriptPath();
  95. }
  96. /**
  97. * Returns the path to the script being executed. The script file name is not included.
  98. *
  99. * @return string eg, `"/dir1/dir2/"` if the current URL is
  100. * `"http://example.org/dir1/dir2/index.php?param1=value1&param2=value2"`
  101. * @api
  102. */
  103. static public function getCurrentScriptPath()
  104. {
  105. $queryString = self::getCurrentScriptName();
  106. //add a fake letter case /test/test2/ returns /test which is not expected
  107. $urlDir = dirname($queryString . 'x');
  108. $urlDir = str_replace('\\', '/', $urlDir);
  109. // if we are in a subpath we add a trailing slash
  110. if (strlen($urlDir) > 1) {
  111. $urlDir .= '/';
  112. }
  113. return $urlDir;
  114. }
  115. /**
  116. * Returns the path to the script being executed. Includes the script file name.
  117. *
  118. * @return string eg, `"/dir1/dir2/index.php"` if the current URL is
  119. * `"http://example.org/dir1/dir2/index.php?param1=value1&param2=value2"`
  120. * @api
  121. */
  122. static public function getCurrentScriptName()
  123. {
  124. $url = '';
  125. if (!empty($_SERVER['REQUEST_URI'])) {
  126. $url = $_SERVER['REQUEST_URI'];
  127. // strip http://host (Apache+Rails anomaly)
  128. if (preg_match('~^https?://[^/]+($|/.*)~D', $url, $matches)) {
  129. $url = $matches[1];
  130. }
  131. // strip parameters
  132. if (($pos = strpos($url, "?")) !== false) {
  133. $url = substr($url, 0, $pos);
  134. }
  135. // strip path_info
  136. if (isset($_SERVER['PATH_INFO'])) {
  137. $url = substr($url, 0, -strlen($_SERVER['PATH_INFO']));
  138. }
  139. }
  140. /**
  141. * SCRIPT_NAME is our fallback, though it may not be set correctly
  142. *
  143. * @see http://php.net/manual/en/reserved.variables.php
  144. */
  145. if (empty($url)) {
  146. if (isset($_SERVER['SCRIPT_NAME'])) {
  147. $url = $_SERVER['SCRIPT_NAME'];
  148. } elseif (isset($_SERVER['SCRIPT_FILENAME'])) {
  149. $url = $_SERVER['SCRIPT_FILENAME'];
  150. } elseif (isset($_SERVER['argv'])) {
  151. $url = $_SERVER['argv'][0];
  152. }
  153. }
  154. if (!isset($url[0]) || $url[0] !== '/') {
  155. $url = '/' . $url;
  156. }
  157. return $url;
  158. }
  159. /**
  160. * Returns the current URL's protocol.
  161. *
  162. * @return string `'https'` or `'http'`
  163. * @api
  164. */
  165. static public function getCurrentScheme()
  166. {
  167. try {
  168. $assume_secure_protocol = @Config::getInstance()->General['assume_secure_protocol'];
  169. } catch (Exception $e) {
  170. $assume_secure_protocol = false;
  171. }
  172. if ($assume_secure_protocol
  173. || (isset($_SERVER['HTTPS'])
  174. && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true))
  175. || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
  176. ) {
  177. return 'https';
  178. }
  179. return 'http';
  180. }
  181. /**
  182. * Validates the **Host** HTTP header (untrusted user input). Used to prevent Host header
  183. * attacks.
  184. *
  185. * @param string|bool $host Contents of Host: header from the HTTP request. If `false`, gets the
  186. * value from the request.
  187. * @return bool `true` if valid; `false` otherwise.
  188. */
  189. static public function isValidHost($host = false)
  190. {
  191. // only do trusted host check if it's enabled
  192. if (isset(Config::getInstance()->General['enable_trusted_host_check'])
  193. && Config::getInstance()->General['enable_trusted_host_check'] == 0
  194. ) {
  195. return true;
  196. }
  197. if ($host === false) {
  198. $host = @$_SERVER['HTTP_HOST'];
  199. if (empty($host)) // if no current host, assume valid
  200. {
  201. return true;
  202. }
  203. }
  204. // if host is in hardcoded whitelist, assume it's valid
  205. if (in_array($host, self::$alwaysTrustedHosts)) {
  206. return true;
  207. }
  208. $trustedHosts = self::getTrustedHosts();
  209. // if no trusted hosts, just assume it's valid
  210. if (empty($trustedHosts)) {
  211. self::saveTrustedHostnameInConfig($host);
  212. return true;
  213. }
  214. // Only punctuation we allow is '[', ']', ':', '.' and '-'
  215. $hostLength = strlen($host);
  216. if ($hostLength !== strcspn($host, '`~!@#$%^&*()_+={}\\|;"\'<>,?/ ')) {
  217. return false;
  218. }
  219. foreach ($trustedHosts as &$trustedHost) {
  220. $trustedHost = preg_quote($trustedHost);
  221. }
  222. $untrustedHost = Common::mb_strtolower($host);
  223. $untrustedHost = rtrim($untrustedHost, '.');
  224. $hostRegex = Common::mb_strtolower('/(^|.)' . implode('|', $trustedHosts) . '$/');
  225. $result = preg_match($hostRegex, $untrustedHost);
  226. return 0 !== $result;
  227. }
  228. /**
  229. * Records one host, or an array of hosts in the config file,
  230. * if user is Super User
  231. *
  232. * @static
  233. * @param $host string|array
  234. * @return bool
  235. */
  236. public static function saveTrustedHostnameInConfig($host)
  237. {
  238. if (Piwik::hasUserSuperUserAccess()
  239. && file_exists(Config::getLocalConfigPath())
  240. ) {
  241. $general = Config::getInstance()->General;
  242. if (!is_array($host)) {
  243. $host = array($host);
  244. }
  245. $host = array_filter($host);
  246. if (empty($host)) {
  247. return false;
  248. }
  249. $general['trusted_hosts'] = $host;
  250. Config::getInstance()->General = $general;
  251. Config::getInstance()->forceSave();
  252. return true;
  253. }
  254. return false;
  255. }
  256. /**
  257. * Returns the current host.
  258. *
  259. * @param bool $checkIfTrusted Whether to do trusted host check. Should ALWAYS be true,
  260. * except in Controller.
  261. * @return string|bool eg, `"demo.piwik.org"` or false if no host found.
  262. */
  263. static public function getHost($checkIfTrusted = true)
  264. {
  265. // HTTP/1.1 request
  266. if (isset($_SERVER['HTTP_HOST'])
  267. && strlen($host = $_SERVER['HTTP_HOST'])
  268. && (!$checkIfTrusted
  269. || self::isValidHost($host))
  270. ) {
  271. return $host;
  272. }
  273. // HTTP/1.0 request doesn't include Host: header
  274. if (isset($_SERVER['SERVER_ADDR'])) {
  275. return $_SERVER['SERVER_ADDR'];
  276. }
  277. return false;
  278. }
  279. /**
  280. * Sets the host. Useful for CLI scripts, eg. core:archive command
  281. *
  282. * @param $host string
  283. */
  284. static public function setHost($host)
  285. {
  286. $_SERVER['HTTP_HOST'] = $host;
  287. }
  288. /**
  289. * Returns the current host.
  290. *
  291. * @param string $default Default value to return if host unknown
  292. * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true,
  293. * except in Controller.
  294. * @return string eg, `"example.org"` if the current URL is
  295. * `"http://example.org/dir1/dir2/index.php?param1=value1&param2=value2"`
  296. * @api
  297. */
  298. static public function getCurrentHost($default = 'unknown', $checkTrustedHost = true)
  299. {
  300. $hostHeaders = array();
  301. $config = Config::getInstance()->General;
  302. if(isset($config['proxy_host_headers'])) {
  303. $hostHeaders = $config['proxy_host_headers'];
  304. }
  305. if (!is_array($hostHeaders)) {
  306. $hostHeaders = array();
  307. }
  308. $host = self::getHost($checkTrustedHost);
  309. $default = Common::sanitizeInputValue($host ? $host : $default);
  310. return IP::getNonProxyIpFromHeader($default, $hostHeaders);
  311. }
  312. /**
  313. * Returns the query string of the current URL.
  314. *
  315. * @return string eg, `"?param1=value1&param2=value2"` if the current URL is
  316. * `"http://example.org/dir1/dir2/index.php?param1=value1&param2=value2"`
  317. * @api
  318. */
  319. static public function getCurrentQueryString()
  320. {
  321. $url = '';
  322. if (isset($_SERVER['QUERY_STRING'])
  323. && !empty($_SERVER['QUERY_STRING'])
  324. ) {
  325. $url .= "?" . $_SERVER['QUERY_STRING'];
  326. }
  327. return $url;
  328. }
  329. /**
  330. * Returns an array mapping query paramater names with query parameter values for
  331. * the current URL.
  332. *
  333. * @return array If current URL is `"http://example.org/dir1/dir2/index.php?param1=value1&param2=value2"`
  334. * this will return:
  335. *
  336. * array(
  337. * 'param1' => string 'value1',
  338. * 'param2' => string 'value2'
  339. * )
  340. * @api
  341. */
  342. static public function getArrayFromCurrentQueryString()
  343. {
  344. $queryString = self::getCurrentQueryString();
  345. $urlValues = UrlHelper::getArrayFromQueryString($queryString);
  346. return $urlValues;
  347. }
  348. /**
  349. * Modifies the current query string with the supplied parameters and returns
  350. * the result. Parameters in the current URL will be overwritten with values
  351. * in `$params` and parameters absent from the current URL but present in `$params`
  352. * will be added to the result.
  353. *
  354. * @param array $params set of parameters to modify/add in the current URL
  355. * eg, `array('param3' => 'value3')`
  356. * @return string eg, `"?param2=value2&param3=value3"`
  357. * @api
  358. */
  359. static function getCurrentQueryStringWithParametersModified($params)
  360. {
  361. $urlValues = self::getArrayFromCurrentQueryString();
  362. foreach ($params as $key => $value) {
  363. $urlValues[$key] = $value;
  364. }
  365. $query = self::getQueryStringFromParameters($urlValues);
  366. if (strlen($query) > 0) {
  367. return '?' . $query;
  368. }
  369. return '';
  370. }
  371. /**
  372. * Converts an array of parameters name => value mappings to a query
  373. * string.
  374. *
  375. * @param array $parameters eg. `array('param1' => 10, 'param2' => array(1,2))`
  376. * @return string eg. `"param1=10&param2[]=1&param2[]=2"`
  377. * @api
  378. */
  379. static public function getQueryStringFromParameters($parameters)
  380. {
  381. $query = '';
  382. foreach ($parameters as $name => $value) {
  383. if (is_null($value) || $value === false) {
  384. continue;
  385. }
  386. if (is_array($value)) {
  387. foreach ($value as $theValue) {
  388. $query .= $name . "[]=" . $theValue . "&";
  389. }
  390. } else {
  391. $query .= $name . "=" . $value . "&";
  392. }
  393. }
  394. $query = substr($query, 0, -1);
  395. return $query;
  396. }
  397. static public function getQueryStringFromUrl($url)
  398. {
  399. return parse_url($url, PHP_URL_QUERY);
  400. }
  401. /**
  402. * Redirects the user to the referrer. If no referrer exists, the user is redirected
  403. * to the current URL without query string.
  404. *
  405. * @api
  406. */
  407. static public function redirectToReferrer()
  408. {
  409. $referrer = self::getReferrer();
  410. if ($referrer !== false) {
  411. self::redirectToUrl($referrer);
  412. }
  413. self::redirectToUrl(self::getCurrentUrlWithoutQueryString());
  414. }
  415. /**
  416. * Redirects the user to the specified URL.
  417. *
  418. * @param string $url
  419. * @api
  420. */
  421. static public function redirectToUrl($url)
  422. {
  423. // Close the session manually.
  424. // We should not have to call this because it was registered via register_shutdown_function,
  425. // but it is not always called fast enough
  426. Session::close();
  427. if (UrlHelper::isLookLikeUrl($url)
  428. || strpos($url, 'index.php') === 0
  429. ) {
  430. @header("Location: $url");
  431. } else {
  432. echo "Invalid URL to redirect to.";
  433. }
  434. if(Common::isPhpCliMode()) {
  435. throw new Exception("If you were using a browser, Piwik would redirect you to this URL: $url \n\n");
  436. }
  437. exit;
  438. }
  439. /**
  440. * If the page is using HTTP, redirect to the same page over HTTPS
  441. */
  442. static public function redirectToHttps()
  443. {
  444. if(ProxyHttp::isHttps()) {
  445. return;
  446. }
  447. $url = self::getCurrentUrl();
  448. $url = str_replace("http://", "https://", $url);
  449. self::redirectToUrl($url);
  450. }
  451. /**
  452. * Returns the **HTTP_REFERER** `$_SERVER` variable, or `false` if not found.
  453. *
  454. * @return string|false
  455. * @api
  456. */
  457. static public function getReferrer()
  458. {
  459. if (!empty($_SERVER['HTTP_REFERER'])) {
  460. return $_SERVER['HTTP_REFERER'];
  461. }
  462. return false;
  463. }
  464. /**
  465. * Returns `true` if the URL points to something on the same host, `false` if otherwise.
  466. *
  467. * @param string $url
  468. * @return bool True if local; false otherwise.
  469. * @api
  470. */
  471. static public function isLocalUrl($url)
  472. {
  473. if (empty($url)) {
  474. return true;
  475. }
  476. // handle host name mangling
  477. $requestUri = isset($_SERVER['SCRIPT_URI']) ? $_SERVER['SCRIPT_URI'] : '';
  478. $parseRequest = @parse_url($requestUri);
  479. $hosts = array(self::getHost(), self::getCurrentHost());
  480. if (!empty($parseRequest['host'])) {
  481. $hosts[] = $parseRequest['host'];
  482. }
  483. // drop port numbers from hostnames and IP addresses
  484. $hosts = array_map(array('self', 'getHostSanitized'), $hosts);
  485. $disableHostCheck = Config::getInstance()->General['enable_trusted_host_check'] == 0;
  486. // compare scheme and host
  487. $parsedUrl = @parse_url($url);
  488. $host = IP::sanitizeIp(@$parsedUrl['host']);
  489. return !empty($host)
  490. && ($disableHostCheck || in_array($host, $hosts))
  491. && !empty($parsedUrl['scheme'])
  492. && in_array($parsedUrl['scheme'], array('http', 'https'));
  493. }
  494. public static function getTrustedHostsFromConfig()
  495. {
  496. $trustedHosts = @Config::getInstance()->General['trusted_hosts'];
  497. if (!is_array($trustedHosts)) {
  498. return array();
  499. }
  500. foreach ($trustedHosts as &$trustedHost) {
  501. // Case user wrote in the config, http://example.com/test instead of example.com
  502. if (UrlHelper::isLookLikeUrl($trustedHost)) {
  503. $trustedHost = parse_url($trustedHost, PHP_URL_HOST);
  504. }
  505. }
  506. return $trustedHosts;
  507. }
  508. public static function getTrustedHosts()
  509. {
  510. return self::getTrustedHostsFromConfig();
  511. }
  512. /**
  513. * Returns hostname, without port numbers
  514. *
  515. * @param $host
  516. * @return array
  517. */
  518. public static function getHostSanitized($host)
  519. {
  520. return IP::sanitizeIp($host);
  521. }
  522. }