Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Url.php 20KB

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