Guest User

Untitled

a guest
Sep 17th, 2015
203
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 35.55 KB | None | 0 0
  1. <?php
  2. /**
  3. * @brief URL Class
  4. * @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
  5. * @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
  6. * @license http://www.invisionpower.com/legal/standards/
  7. * @package IPS Social Suite
  8. * @since 10 Jun 2013
  9. * @version SVN_VERSION_NUMBER
  10. */
  11.  
  12. namespace IPS\Http;
  13.  
  14. /* To prevent PHP errors (extending class does not exist) revealing path */
  15. if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
  16. {
  17. header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
  18. exit;
  19. }
  20.  
  21. /**
  22. * URL Class
  23. */
  24. class _Url
  25. {
  26. const PROTOCOL_AUTOMATIC = 0;
  27. const PROTOCOL_HTTPS = 1;
  28. const PROTOCOL_HTTP = 2;
  29. const PROTOCOL_RELATIVE = 3;
  30.  
  31. /**
  32. * @brief FURL Definition
  33. */
  34. protected static $furlDefinition = NULL;
  35.  
  36. /**
  37. * Get FURL Definition
  38. *
  39. * @param bool $revert If TRUE, ignores all customisations and reloads from json
  40. * @return array
  41. */
  42. public static function furlDefinition( $revert=FALSE )
  43. {
  44. if ( static::$furlDefinition === NULL or $revert )
  45. {
  46.  
  47. $furlCustomizations = ( \IPS\Settings::i()->furl_configuration AND !$revert ) ? json_decode( \IPS\Settings::i()->furl_configuration, TRUE ) : array();
  48. $furlConfiguration = ( isset( \IPS\Data\Store::i()->furl_configuration ) AND \IPS\Data\Store::i()->furl_configuration ) ? json_decode( \IPS\Data\Store::i()->furl_configuration, TRUE ) : array();
  49.  
  50. if ( ( \IPS\IN_DEV and !\IPS\DEV_USE_FURL_CACHE ) or !count( $furlConfiguration ) or $revert )
  51. {
  52. static::$furlDefinition = array();
  53. foreach ( \IPS\Application::applications() as $app )
  54. {
  55. if( file_exists( \IPS\ROOT_PATH . "/applications/{$app->directory}/data/furl.json" ) )
  56. {
  57. $data = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( \IPS\ROOT_PATH . "/applications/{$app->directory}/data/furl.json" ) ), TRUE );
  58. $topLevel = $data['topLevel'];
  59.  
  60. $definitions = $data['pages'];
  61. if ( $topLevel )
  62. {
  63. if ( $app->default )
  64. {
  65. $definitions = array_map( function( $definition ) use ( $topLevel )
  66. {
  67. $definition['with_top_level'] = $topLevel . ( $definition['friendly'] ? '/' . $definition['friendly'] : '' );
  68. return $definition;
  69. }, $definitions );
  70. }
  71. else
  72. {
  73. $definitions = array_map( function( $definition ) use ( $topLevel )
  74. {
  75. $definition['without_top_level'] = $definition['friendly'];
  76. $definition['friendly'] = $topLevel . ( $definition['friendly'] ? '/' . $definition['friendly'] : '' );
  77. return $definition;
  78. }, $definitions );
  79. }
  80. }
  81.  
  82. static::$furlDefinition = array_merge( static::$furlDefinition, $definitions );
  83. }
  84. }
  85.  
  86. \IPS\Data\Store::i()->furl_configuration = json_encode( static::$furlDefinition );
  87.  
  88. static::$furlDefinition = array_merge( static::$furlDefinition, $furlCustomizations );
  89. }
  90. else
  91. {
  92. static::$furlDefinition = array_merge( $furlConfiguration, $furlCustomizations );
  93. }
  94. }
  95.  
  96. return static::$furlDefinition;
  97. }
  98.  
  99. /**
  100. * Return the base URL
  101. *
  102. * @param bool $protocol Protocol (one of the PROTOCOL_* constants)
  103. * @return string
  104. */
  105. public static function baseUrl( $protocol = 0 )
  106. {
  107. /* Get the base URL */
  108. $url = \IPS\Settings::i()->base_url;
  109.  
  110. /* Adjust the protocol */
  111. if ( $protocol )
  112. {
  113. switch ( $protocol )
  114. {
  115. case static::PROTOCOL_HTTPS:
  116. $url = 'https://' . mb_substr( $url, mb_strpos( $url, '://' ) + 3 );
  117. break;
  118.  
  119. case static::PROTOCOL_HTTP:
  120. $url = 'http://' . mb_substr( $url, mb_strpos( $url, '://' ) + 3 );
  121. break;
  122.  
  123. case static::PROTOCOL_RELATIVE:
  124. $url = '//' . mb_substr( $url, mb_strpos( $url, '://' ) + 3 );
  125. break;
  126. }
  127. }
  128.  
  129. /* Add a trailing slash */
  130. if ( mb_substr( $url, -1 ) !== '/' )
  131. {
  132. $url .= '/';
  133. }
  134.  
  135. /* Return */
  136. return $url;
  137. }
  138.  
  139. /**
  140. * Build Internal URL
  141. *
  142. * @param string $queryString The query string
  143. * @param string|null $base Key for the URL base. If NULL, defaults to current controller location
  144. * @param string $seoTemplate The key for making this a friendly URL
  145. * @param string|array $seoTitles The title(s) needed for the friendly URL
  146. * @param bool $protocol Protocol (one of the PROTOCOL_* constants)
  147. * @return \IPS\Http\Url
  148. */
  149. public static function internal( $queryString, $base=NULL, $seoTemplate=NULL, $seoTitles=array(), $protocol = 0 )
  150. {
  151. /* If we don't have a base, assume the template location */
  152. if ( $base === NULL )
  153. {
  154. $base = \IPS\Dispatcher::hasInstance() ? \IPS\Dispatcher::i()->controllerLocation : 'front';
  155. }
  156.  
  157. /* We handle setup specially */
  158. if ( $base === 'setup' )
  159. {
  160. return new static( ( \IPS\Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . ( $_SERVER['QUERY_STRING'] ? rtrim( mb_substr( $_SERVER['REQUEST_URI'], 0, -mb_strlen( $_SERVER['QUERY_STRING'] ) ), '?' ) : $_SERVER['REQUEST_URI'] ) . '?' . $queryString, TRUE );
  161. }
  162.  
  163. /* Force ACP to https? */
  164. if ( $base === 'admin' and \IPS\Settings::i()->logins_over_https )
  165. {
  166. $protocol = static::PROTOCOL_HTTPS;
  167. }
  168.  
  169. /* Get the base URL */
  170. $url = static::baseUrl( $protocol );
  171.  
  172. /* Do our stuff */
  173. switch ( $base )
  174. {
  175. /* ACP links */
  176. case 'admin':
  177. /* Front: Never disclose adsess in front pages */
  178. if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation !== 'admin' )
  179. {
  180. /* If there is a query string (like a link from an error page, pass through the redirector so we don't disclose the location */
  181. if ( $queryString )
  182. {
  183. return new static( $url . '?app=core&module=system&controller=redirect&do=admin&_data=' . base64_encode( $queryString ), TRUE );
  184. }
  185. /* Or if it's just a normal link like in the user bar, show that */
  186. else
  187. {
  188. return new static( $url . \IPS\CP_DIRECTORY, TRUE );
  189. }
  190. }
  191. /* Within ACP */
  192. else
  193. {
  194. return new static( $url . \IPS\CP_DIRECTORY . '/?adsess=' . session_id() . '&' . $queryString, TRUE );
  195. }
  196. break;
  197.  
  198. /* Front-end URLs */
  199. default:
  200. case 'front':
  201. $obj = new static( $url . ( $queryString ? '?' . $queryString : '' ), TRUE );
  202.  
  203. if ( $seoTemplate )
  204. {
  205. $obj->makeFriendly( $seoTemplate, $seoTitles, $protocol );
  206. }
  207.  
  208. return $obj;
  209.  
  210. /* Static URLs */
  211. case 'none':
  212. return new static( $url . $queryString, TRUE );
  213. }
  214. }
  215.  
  216. /**
  217. * Build External URL
  218. *
  219. * @param string $url
  220. * @return \IPS\Http\Url
  221. * @throws \InvalidArgumentException
  222. */
  223. public static function external( $url )
  224. {
  225. return new static( $url, FALSE );
  226. }
  227.  
  228. /**
  229. * Build IPS-External URL
  230. *
  231. * @param string $url
  232. * @return \IPS\Http\Url
  233. */
  234. public static function ips( $url )
  235. {
  236. return new static( "http://anonymz.com/?https://remoteservices.invisionpower.com/{$url}/?version=" . \IPS\Application::getAvailableVersion('core'), FALSE );
  237. }
  238.  
  239. /**
  240. * Build Anonymize-External URL
  241. *
  242. * @param string $url
  243. * @return \IPS\Http\Url
  244. */
  245. public static function ipsnewsurl( $url )
  246. {
  247. return new static( "http://anonymz.com/?" . $url );
  248. }
  249.  
  250. /**
  251. * Build IV-External URL
  252. *
  253. * @param string $url
  254. * @return \IPS\Http\Url
  255. */
  256. public static function iv( $url )
  257. {
  258. if( \IPS\Settings::i()->change_iv_url )
  259. {
  260. return new static( \IPS\Settings::i()->iv_connect_url . "/" . $url . "?version=" . \IPS\Application::getAvailableVersion('core') );
  261. }
  262.  
  263. return new static( base64_decode( 'aHR0cDovL2ludmlzaW9uLXZpcnVzLmNvbS90b29scy9pcHM0Lw==' ) . $url . "?version=" . \IPS\Application::getAvailableVersion('core') );
  264. }
  265.  
  266. /**
  267. * Convert a value into an "SEO Title" for friendly URLs
  268. *
  269. * @param string $value Value
  270. * @return string
  271. * @note Many places require an SEO title, so we always need to return something, so when no valid title is available we return a dash
  272. */
  273. public static function seoTitle( $value )
  274. {
  275. /* Always lowercase */
  276. $value = mb_strtolower( $value );
  277.  
  278. /* Just for readability */
  279. $value = str_replace( ' ', '-', $value );
  280.  
  281. /* Disallowed characters which browsers may try to automatically percent-encode */
  282. $value = str_replace( array( '!', '*', '\'', '(', ')', ';', ':', '@', '&', '=', '+', '$', ',', '/', '?', '#', '[', ']', '%', '\\', '"', '<', '>', '^', '{', '}', '|', '.', '`' ), '', $value );
  283.  
  284. /* Trim */
  285. $value = preg_replace( '/\-+/', '-', $value );
  286. $value = trim( $value, '-' );
  287. $value = trim( $value );
  288.  
  289. /* Return */
  290. return $value ?: '-';
  291. }
  292.  
  293. /**
  294. * @brief URL
  295. */
  296. protected $url = NULL;
  297.  
  298. /**
  299. * @brief Data
  300. */
  301. public $data = array(
  302. 'scheme' => NULL,
  303. 'host' => NULL,
  304. 'port' => NULL,
  305. 'user' => NULL,
  306. 'pass' => NULL,
  307. 'path' => NULL,
  308. 'query' => NULL,
  309. 'fragment' => NULL
  310. );
  311.  
  312. /**
  313. * @brief Query String
  314. */
  315. public $queryString = array();
  316.  
  317. /**
  318. * @brief Is internal?
  319. */
  320. public $isInternal = FALSE;
  321.  
  322. /**
  323. * @brief Is friendly?
  324. */
  325. public $isFriendly = FALSE;
  326.  
  327. /**
  328. * Constructor
  329. *
  330. * @param string $url The URL
  331. * @param bool $internal Is internal? (NULL to auto-detect)
  332. * @return void
  333. * @throws \InvalidArgumentException
  334. */
  335. public function __construct( $url, $internal=NULL )
  336. {
  337. $this->setUrl( $url, $internal );
  338. }
  339.  
  340. /**
  341. * Adjust Query String
  342. *
  343. * @param string|array $keyOrArray Key, or array of key/value paird
  344. * @param string|null $value Value, or NULL if $key is an array
  345. * @return \IPS\Http\Url
  346. */
  347. public function setQueryString( $key, $value=NULL )
  348. {
  349. if ( is_array( $key ) )
  350. {
  351. $queryString = array_merge( $this->queryString, $key );
  352. }
  353. else
  354. {
  355. $queryString = array_merge( $this->queryString, array( $key => $value ) );
  356. }
  357.  
  358. return $this->reconstruct( $this->data, $queryString );
  359. }
  360.  
  361. /**
  362. * Add CSRF check to query string
  363. *
  364. * @return \IPS\Http\Url
  365. */
  366. public function csrf()
  367. {
  368. return $this->setQueryString( 'csrfKey', \IPS\Session::i()->csrfKey );
  369. }
  370.  
  371. /**
  372. * Strip Query String
  373. *
  374. * @param string|array $keys The key(s) to strip - if omitted, entire query string is wiped
  375. * @return \IPS\Http\Url
  376. */
  377. public function stripQueryString( $keys=NULL )
  378. {
  379. $queryString = array();
  380.  
  381. if( $keys !== NULL )
  382. {
  383. if( !is_array( $keys ) )
  384. {
  385. $keys = array( $keys => $keys );
  386. }
  387.  
  388. $queryString = array_diff_key( $this->queryString, $keys );
  389. }
  390.  
  391. return $this->reconstruct( $this->data, $queryString );
  392. }
  393.  
  394. /**
  395. * Strip URL arguments
  396. *
  397. * @param int $position Arguments from this position onward will be stripped. Value of 0 is equivalent to stripping the entire query string
  398. * @return \IPS\Http\Url
  399. */
  400. public function stripArguments( $position=0 )
  401. {
  402. if( $position === 0 )
  403. {
  404. return $this->stripQueryString();
  405. }
  406.  
  407. $queryString = array();
  408. $_index = 0;
  409.  
  410. foreach( $this->queryString as $key => $value )
  411. {
  412. if( $_index >= $position )
  413. {
  414. break;
  415. }
  416.  
  417. $queryString[ $key ] = $value;
  418. $_index++;
  419. }
  420.  
  421. return $this->reconstruct( $this->data, $queryString );
  422. }
  423.  
  424. /**
  425. * Adjust fragment
  426. *
  427. * @param string $value New fragment
  428. * @return \IPS\Http\Url
  429. */
  430. public function setFragment( $value )
  431. {
  432. return $this->reconstruct( array_merge( $this->data, array( 'fragment' => $value ) ), $this->queryString );
  433. }
  434.  
  435. /**
  436. * Create a URL object from data array
  437. *
  438. * @param array $data URL data pieces
  439. * @return \IPS\Http\Url
  440. */
  441. public static function createFromArray( $data )
  442. {
  443. return new static(
  444. ( ( isset( $data['scheme'] ) AND $data['scheme'] ) ? ( $data['scheme'] . '://' ) : '//' ) .
  445. ( ( isset( $data['user'] ) or isset( $data['pass'] ) ) ? "{$data['user']}:{$data['pass']}@" : '' ) .
  446. ( !empty( $data['host'] ) ? $data['host'] : '' ) .
  447. ( isset( $data['port'] ) ? ":{$data['port']}" : '' ) .
  448. ( !empty( $data['path'] ) ? $data['path'] : '' ) .
  449. ( !empty( $data['query'] ) ? ( ( mb_strpos( $data['path'], '?' ) !== FALSE ? '&' : '?' ) . ( is_array( $data['query'] ) ? http_build_query( $data['query'], '', '&' ) : $data['query'] ) ) : '' ) .
  450. ( isset( $data['fragment'] ) ? "#{$data['fragment']}" : '' ),
  451. FALSE
  452. );
  453. }
  454.  
  455. /**
  456. * Reconstruct
  457. *
  458. * @param array $data URL data
  459. * @param array $queryString Query String
  460. * @return \IPS\Http\Url
  461. */
  462. protected function reconstruct( $data, $queryString=array() )
  463. {
  464. return new static(
  465. ( ( isset( $data['scheme'] ) AND $data['scheme'] ) ? ( $data['scheme'] . '://' ) : '//' ) .
  466. ( ( isset( $data['user'] ) or isset( $data['pass'] ) ) ? "{$data['user']}:{$data['pass']}@" : '' ) .
  467. ( !empty( $data['host'] ) ? $data['host'] : '' ) .
  468. ( isset( $data['port'] ) ? ":{$data['port']}" : '' ) .
  469. ( !empty( $data['path'] ) ? $data['path'] : '' ) .
  470. ( !empty( $queryString ) ? ( ( mb_strpos( $data['path'], '?' ) !== FALSE ? '&' : '?' ) . http_build_query( $queryString, '', '&' ) ) : '' ) .
  471. ( isset( $data['fragment'] ) ? "#{$data['fragment']}" : '' ),
  472. $this->isInternal
  473. );
  474. }
  475.  
  476. /**
  477. * Set URL
  478. *
  479. * @param string $url The URL
  480. * @param bool $internal Is internal? (NULL to auto-detect)
  481. * @return void
  482. * @throws \InvalidArgumentException
  483. */
  484. protected function setUrl( $url, $internal=NULL )
  485. {
  486. /* Set it */
  487. $this->url = $url;
  488.  
  489. /* Parse */
  490. $this->data = $this->utf8ParseUrl( $url );
  491.  
  492. $this->queryString = array();
  493.  
  494. if ( isset( $this->data['query'] ) )
  495. {
  496. /* If there are no ampersands and the query string starts with '/' then it is part of the path */
  497. if( mb_strpos( $this->data['query'], '&' ) === FALSE AND mb_strpos( $this->data['query'], '/' ) === 0 )
  498. {
  499. $this->data['path'] .= '?' . $this->data['query'];
  500. }
  501. else
  502. {
  503. /* We can't use parse_str because it replaces . with _ which can cause FURLs to get converted incorrectly */
  504. $values = explode( '&', $this->data['query'] );
  505. foreach( $values as $value )
  506. {
  507. $pieces = explode( '=', $value, 2 );
  508.  
  509. /* If there's a parameter which is /something/ and has no value, that's actually part of the path */
  510. if( !isset( $pieces[1] ) and preg_match( '#^/(.*?)(/)?$#i', $pieces[0] ) )
  511. {
  512. $this->data['path'] .= '?' . $pieces[0];
  513. continue;
  514. }
  515.  
  516. $this->queryString[ urldecode( $pieces[0] ) ] = isset( $pieces[1] ) ? urldecode( $pieces[1] ) : '';
  517. }
  518. }
  519. }
  520.  
  521. /* Is it internal? We have to ignore protocol because areas like force login over https and secure checkout in Nexus will use https urls for an otherwise http site and we end up with infinite redirects */
  522. if ( $internal === NULL )
  523. {
  524. $this->isInternal = ( mb_substr( str_replace( 'https://', 'http://', $this->url ), 0, mb_strlen( str_replace( 'https://', 'http://', \IPS\Settings::i()->base_url ) ) ) === str_replace( 'https://', 'http://', \IPS\Settings::i()->base_url ) );
  525. }
  526. else
  527. {
  528. $this->isInternal = $internal;
  529. }
  530.  
  531. /* Is it friendly? */
  532. if ( $this->isInternal and ( mb_strpos( $url, 'index.php' ) === FALSE or mb_substr( str_replace( 'https://', 'http://', $this->url ), str_replace( 'https://', 'http://', mb_strlen( \IPS\Settings::i()->base_url ) ), 11 ) === 'index.php?/' ) )
  533. {
  534. $this->isFriendly = TRUE;
  535. }
  536. }
  537.  
  538. /**
  539. * To String
  540. *
  541. * @return string
  542. */
  543. public function __toString()
  544. {
  545. return (string) $this->url;
  546. }
  547.  
  548. /**
  549. * Return URL that conforms to RFC 3986
  550. *
  551. * @return string
  552. */
  553. public function rfc3986()
  554. {
  555. $pieces = $this->utf8ParseUrl( (string) $this->url );
  556. $pathBits = implode( "/", array_map( "rawurlencode", explode( "/", ltrim( $pieces['path'], '/' ) ) ) );
  557.  
  558. return ( $pieces['scheme'] ? ( $pieces['scheme'] . '://' ) : '//' ) .
  559. ( ( isset( $pieces['user'] ) AND $pieces['user'] ) ? $pieces['user'] : '' ) .
  560. ( ( isset( $pieces['pass'] ) AND $pieces['pass'] ) ? ':' . $pieces['pass'] : '' ) .
  561. $pieces['host'] .
  562. ( ( isset( $pieces['port'] ) AND $pieces['port'] ) ? $pieces['port'] : '' ) .
  563. '/' . $pathBits .
  564. ( ( isset( $pieces['query'] ) AND $pieces['query'] ) ? '?' . $pieces['query'] : '' ) .
  565. ( ( isset( $pieces['fragment'] ) AND $pieces['fragment'] ) ? '#' . $pieces['fragment'] : '' );
  566. }
  567.  
  568. /**
  569. * Get ACP query string without adsess
  570. *
  571. * @return string
  572. */
  573. public function acpQueryString()
  574. {
  575. $queryString = $this->queryString;
  576. unset( $queryString['adsess'] );
  577. unset( $queryString['csrf'] );
  578. return http_build_query( $queryString, '', '&' );
  579. }
  580.  
  581. /**
  582. * Make friendly
  583. *
  584. * @param string|array $seoTemplate The key for making this a friendly URL; or a manual FURL definition
  585. * @param string|array $seoTitles The title(s) needed for the friendly URL
  586. * @param bool $protocol Protocol (one of the PROTOCOL_* constants)
  587. * @return void
  588. */
  589. public function makeFriendly( $seoTemplate, $seoTitles, $protocol = 0 )
  590. {
  591. /* Enabled? */
  592. if ( !\IPS\Settings::i()->use_friendly_urls )
  593. {
  594. return;
  595. }
  596.  
  597. /* Make SEO Titles an array if they're not already */
  598. if ( !is_array( $seoTitles ) )
  599. {
  600. $seoTitles = array( $seoTitles );
  601. }
  602.  
  603. /* Get FURL definition */
  604. $definition = NULL;
  605. if ( is_array( $seoTemplate ) )
  606. {
  607. $definition = $seoTemplate;
  608. }
  609. else
  610. {
  611. $furlDefinition = static::furlDefinition();
  612. if ( isset( $furlDefinition[ $seoTemplate ] ) )
  613. {
  614. $definition = $furlDefinition[ $seoTemplate ];
  615. }
  616. }
  617.  
  618. /* Find it */
  619. if ( isset( $definition ) )
  620. {
  621. $titleMatch = 0;
  622. $parsed = &$this->queryString;
  623.  
  624. $url = preg_replace_callback( '/{(\#|\@|\?)([^}]+?)?}/i', function( $match ) use ( &$parsed, $titleMatch, $seoTitles, $seoTemplate )
  625. {
  626. if ( $match[1] === '?' )
  627. {
  628. if ( !isset( $match[2] ) )
  629. {
  630. $match[2] = $titleMatch++;
  631. }
  632.  
  633. if ( !isset( $seoTitles[ $match[2] ] ) )
  634. {
  635. return '';
  636. }
  637.  
  638. return $seoTitles[ $match[2] ];
  639. }
  640. else
  641. {
  642. $toReturn = ( !empty($parsed[ $match[2] ]) ) ? $parsed[ $match[2] ] : '';
  643. unset( $parsed[ $match[2] ] );
  644. return $toReturn;
  645. }
  646. }, $definition['friendly'] );
  647.  
  648. $qs = $this->queryString;
  649.  
  650. parse_str( $definition['real'], $ignore );
  651. foreach ( array_keys( $ignore ) as $i )
  652. {
  653. unset( $qs[ $i ] );
  654. }
  655.  
  656. $trailingSlash = mb_strpos( $definition['friendly'], '.' ) !== FALSE ? '' : '/';
  657. if ( \IPS\Settings::i()->htaccess_mod_rewrite )
  658. {
  659. $this->setUrl( rtrim( static::baseUrl( $protocol ) . $url, '/' ) . ( $qs ? '/?' . http_build_query( $qs, '', '&' ) : $trailingSlash ) );
  660. }
  661. else
  662. {
  663. $this->setUrl( static::baseUrl( $protocol ) . 'index.php?/' . $url . ( $qs ? '/&' . http_build_query( $qs, '', '&' ) : ( $url ? $trailingSlash : '' ) ) );
  664. }
  665. }
  666.  
  667. /* Set it */
  668. $this->isFriendly = TRUE;
  669. }
  670.  
  671. /**
  672. * Get the query/path
  673. *
  674. * @return string
  675. */
  676. public function queryPath()
  677. {
  678. /* Determine our gateway */
  679. $gatewayFile = explode( '/', str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ) );
  680. $gatewayFile = array_pop( $gatewayFile );
  681.  
  682. /* Work out what the FURL "query" is */
  683. $baseUrl = $this->utf8ParseUrl( \IPS\Settings::i()->base_url );
  684.  
  685. if ( \IPS\Settings::i()->htaccess_mod_rewrite )
  686. {
  687. $query = ( isset( $this->data['path'] ) ? $this->data['path'] : '' );
  688. }
  689. else
  690. {
  691. if ( isset( $this->data['path'] ) and mb_strpos( $this->data['path'], $gatewayFile . '?' ) )
  692. {
  693. $query = ( isset( $this->data['query'] ) ? ltrim( $this->data['query'], '/' ) : '' );
  694.  
  695. }
  696. else
  697. {
  698. if ( isset( $this->data['path'] ) AND !isset( $this->data['query'] ) AND mb_strpos( $this->data['path'], $gatewayFile ) )
  699. {
  700. /* This seems to be a legacy Non-Mod Rewrite, Path Info based URL. EX: /forums/index.php/topic/1-abc/ */
  701. $pathInfo = explode( $gatewayFile, $this->data['path'] );
  702. $query = ltrim( array_pop( $pathInfo ), '/' );
  703. }
  704. else
  705. {
  706. $query = '';
  707. }
  708. }
  709. }
  710.  
  711. $query = preg_replace( '#^(' . preg_quote( rtrim( $baseUrl['path'], '/' ), '#' ) . ')/(' . $gatewayFile . ')?(?:(?:\?/|\?))?(.+?)?$#', '$3', $query );
  712.  
  713. return $query;
  714. }
  715.  
  716. /**
  717. * Get friendly URL data
  718. *
  719. * @param bool $verify If TRUE, will check URL uses correct SEO title
  720. * @return array Parameters
  721. * @throws \OutOfRangeException Invalid URL
  722. * @throws \DomainException URL does not have correct SEO title (exception message will contain correct URL)
  723. */
  724. public function getFriendlyUrlData( $verify=FALSE )
  725. {
  726. $set = array();
  727.  
  728. /* Need to remember the template we use for FURL verification */
  729. $usedTemplate = NULL;
  730.  
  731. /* Get furl definition */
  732. $furlDefinition = static::furlDefinition();
  733.  
  734. /* What is the query */
  735. $query = $this->queryPath();
  736.  
  737. /* Examine our URLs */
  738. $this->examineFurl( $furlDefinition, $query, $set, $usedTemplate );
  739.  
  740. /* What about an alias? */
  741. if ( $usedTemplate === NULL and $query )
  742. {
  743. $this->examineFurl( $furlDefinition, $query, $set, $usedTemplate, TRUE );
  744. }
  745.  
  746. /* What about a different topLevel? */
  747. if ( $usedTemplate === NULL and $query )
  748. {
  749. $this->examineFurl( $furlDefinition, $query, $set, $usedTemplate, FALSE, TRUE );
  750. }
  751.  
  752. /* Nothing? */
  753. if ( $usedTemplate === NULL and $query )
  754. {
  755. throw new \OutOfRangeException;
  756. }
  757.  
  758. /* Redirect to correct FURL if necessary */
  759. if ( $verify )
  760. {
  761. /* Build the requested URL as an object */
  762. $requestedUrls = array( $this );
  763.  
  764. /* Now...if we did not include a module or a controller, add the default ones on so we can check those too */
  765. if( isset( $requestedUrls[0]->data['query'] ) )
  766. {
  767. parse_str( $requestedUrls[0]->data['query'], $parameters );
  768. }
  769.  
  770. if( isset( $parameters['app'] ) )
  771. {
  772. if( !isset( $parameters['module'] ) OR !isset( $parameters['controller'] ) )
  773. {
  774. $modules = \IPS\Application::load( $parameters['app'] )->modules( 'front' );
  775.  
  776. foreach( $modules as $moduleId => $module )
  777. {
  778. if( !isset( $parameters['module'] ) AND $module->default )
  779. {
  780. $requestedUrls[] = $requestedUrls[0]->setQueryString( 'module', $module->key );
  781.  
  782. if( !isset( $parameters['controller'] ) )
  783. {
  784. $requestedUrls[] = $requestedUrls[0]->setQueryString( array( 'module' => $module->key, 'controller' => $module->default_controller ) );
  785. }
  786. }
  787. else if( isset( $parameters['module'] ) AND !isset( $parameters['controller'] ) AND $module->key == $parameters['module'] )
  788. {
  789. $requestedUrls[] = $requestedUrls[0]->setQueryString( 'controller', $module->default_controller );
  790. }
  791. }
  792. }
  793. }
  794.  
  795. foreach( $requestedUrls as $requestedUrl )
  796. {
  797. $correctUrl = $requestedUrl;
  798.  
  799. /* If this was a FURL request and we have a template to use, great! */
  800. if( $usedTemplate !== NULL )
  801. {
  802. /* Is there a callback, or should we just build the URL ourselves based on the information available? */
  803. if( !empty( $usedTemplate['verify'] ) )
  804. {
  805. try
  806. {
  807. $correctUrl = $usedTemplate['verify']::loadFromUrl( \IPS\Http\Url::internal( $usedTemplate['_rebuiltUrl'], 'front' ) )->url();
  808. }
  809. catch ( \OutOfRangeException $e )
  810. {
  811. break;
  812. }
  813. }
  814. else
  815. {
  816. $correctUrl = \IPS\Http\Url::internal( $usedTemplate['_rebuiltUrl'], 'front', $usedTemplate['_template'], $usedTemplate['_dynamicInfo'] );
  817. }
  818.  
  819. /* Strip the trailing slash? */
  820. if ( mb_strpos( $usedTemplate['friendly'], '.' ) !== FALSE )
  821. {
  822. $correctUrl = new \IPS\Http\Url( rtrim( $correctUrl, '/' ) );
  823. }
  824.  
  825. /* Merge query string back in */
  826. $correctUrl = $correctUrl->setQueryString( $requestedUrl->queryString );
  827. }
  828. /* Not a FURL request...should it have been? */
  829. else
  830. {
  831. if( !empty( $requestedUrl->data['query'] ) )
  832. {
  833. foreach ( $furlDefinition as $_key => $data )
  834. {
  835. if( mb_stripos( $requestedUrl->data['query'], $data['real'] ) !== FALSE )
  836. {
  837. /* Figure out if this FURL definition requires extra data.
  838. Example: messenger_convo and messenger have the same $data['real'] definition, but messenger_convo requires an 'id' parameter too */
  839. $params = array();
  840. preg_match_all( '/{(.+?)}/', $data['friendly'], $matches );
  841.  
  842. foreach ( $matches[1] as $tag )
  843. {
  844. switch ( mb_substr( $tag, 0, 1 ) )
  845. {
  846. case '#':
  847. $params[] = mb_substr( $tag, 1 );
  848. break;
  849.  
  850. case '@':
  851. $params[] = mb_substr( $tag, 1 );
  852. break;
  853. }
  854. }
  855.  
  856. /* If this definition requires a parameter, see if we have it. If not, skip to next definition to check. */
  857. if( count( $params ) )
  858. {
  859. parse_str( $requestedUrl->data['query'], $set );
  860.  
  861. foreach( $params as $param )
  862. {
  863. if( !isset( $set[ $param ] ) )
  864. {
  865. continue 2;
  866. }
  867. }
  868. }
  869.  
  870. /* Now try to check URL */
  871. try
  872. {
  873. /* Is there a callback, or should we just build the URL ourselves based on the information available? */
  874. if( !empty( $data['verify'] ) )
  875. {
  876. parse_str( $data['real'], $paramsInCorrectUrl );
  877. $paramsInCorrectUrl = array_merge( array_keys( $paramsInCorrectUrl ), $params );
  878.  
  879. $correctUrl = $data['verify']::loadFromUrl( \IPS\Http\Url::internal( $requestedUrl->data['query'], 'front' ) )->url();
  880.  
  881. $paramsToSet = array();
  882. foreach ( $requestedUrl->queryString as $k => $v )
  883. {
  884. if ( !in_array( $k, $paramsInCorrectUrl ) )
  885. {
  886. $paramsToSet[ $k ] = $v;
  887. }
  888. }
  889.  
  890. if ( count( $paramsToSet ) )
  891. {
  892. $correctUrl = $correctUrl->setQueryString( $paramsToSet );
  893. }
  894. }
  895. else
  896. {
  897. $seoTitles = array();
  898. if ( isset( $data['seoTitles'] ) )
  899. {
  900. foreach ( $data['seoTitles'] as $seoTitleData )
  901. {
  902. try
  903. {
  904. $class = $seoTitleData['class'];
  905. $queryParam = $seoTitleData['queryParam'];
  906. $property = $seoTitleData['property'];
  907.  
  908. $seoTitles[] = $class::load( \IPS\Request::i()->$queryParam )->$property;
  909. }
  910. catch ( \OutOfRangeException $e ) {}
  911. }
  912. }
  913.  
  914. $correctUrl = \IPS\Http\Url::internal( $requestedUrl->data['query'], 'front', $_key, $seoTitles );
  915. }
  916.  
  917. break;
  918. }
  919. catch ( \Exception $e ) {}
  920. }
  921. }
  922. }
  923. }
  924.  
  925. /* If the URL was wrong, redirect - we compare without query strings */
  926. /* @note The urldecode() is needed for http://community.invisionpower.com/resources/bugs.html/_/4-0-0/109-problem-with-seo-names-categories-forums-r47585 */
  927. /* @note We have to ignore protocol because areas like force login over https and secure checkout in Nexus will use https urls for an otherwise http site and
  928. we end up with infinite redirects */
  929. if( str_replace( 'https://', 'http://', (string) $correctUrl->stripQueryString() ) != str_replace( 'https://', 'http://', urldecode( (string) $requestedUrl->stripQueryString() ) ) or $correctUrl->isFriendly and !$requestedUrl->isFriendly )
  930. {
  931. /* IP.Board 3.x used /page-x in the path rather than a query string argument - we support this so as not to break past links */
  932. /* @link http://community.invisionpower.com/resources/bugs.html/_/4-0-0/url-part-page4-r47961 */
  933. if( mb_strpos( (string) $requestedUrl, '/page-' ) )
  934. {
  935. preg_match( "/\/page\-(\d+?)/", (string) $requestedUrl, $matches );
  936.  
  937. if( isset( $matches[1] ) )
  938. {
  939. $correctUrl = $correctUrl->setQueryString( 'page', $matches[1] );
  940. }
  941. }
  942.  
  943. /* IP.Board also supported page__x__y which we should preserve - things that have changed will be converted later */
  944. if( mb_strpos( (string) $requestedUrl, '/page__' ) )
  945. {
  946. preg_match( "/\/page__([^\?&\/]+)/", (string) $requestedUrl, $matches );
  947.  
  948. if( isset( $matches[1] ) )
  949. {
  950. $_matches = explode( '__', $matches[1] );
  951.  
  952. for( $i=0, $j=count($_matches); $i<$j; $i+=2 )
  953. {
  954. $correctUrl = $correctUrl->setQueryString( $_matches[ $i ], $_matches[ $i+1 ] );
  955. }
  956. }
  957. }
  958.  
  959. throw new \DomainException( $correctUrl );
  960. }
  961. }
  962. }
  963.  
  964. return $set;
  965. }
  966.  
  967. /**
  968. * Examine friendly URL
  969. *
  970. * @return array
  971. */
  972. protected function examineFurl( &$furlDefinition, &$query, &$set, &$usedTemplate, $checkAlias=FALSE, $checkWithoutTopLevel=FALSE )
  973. {
  974. foreach ( $furlDefinition as $_key => $data )
  975. {
  976. /* What are we looking at? */
  977. $check = $data['friendly'];
  978. if ( $checkAlias and isset( $data['alias'] ) )
  979. {
  980. $check = $data['alias'];
  981. }
  982. elseif ( $checkWithoutTopLevel )
  983. {
  984. if ( isset( $data['without_top_level'] ) )
  985. {
  986. $check = $data['without_top_level'];
  987. }
  988. elseif ( isset( $data['with_top_level'] ) )
  989. {
  990. $check = $data['with_top_level'];
  991. }
  992. }
  993.  
  994. /* If this is just for the default page, skip it, otherwise it'll match everything */
  995. if ( !$check )
  996. {
  997. continue;
  998. }
  999.  
  1000. /* Start with what we have for the friendly URL */
  1001. $regex = preg_quote( $check, '/' );
  1002.  
  1003. /* Parse out variables */
  1004. $params = array();
  1005. preg_match_all( '/{(.+?)}/', $check, $matches );
  1006. foreach ( $matches[1] as $tag )
  1007. {
  1008. switch ( mb_substr( $tag, 0, 1 ) )
  1009. {
  1010. case '#':
  1011. $regex = str_replace( '\{#' . mb_substr( $tag, 1 ) . '\}', '(\d+?)', $regex );
  1012. $params[] = mb_substr( $tag, 1 );
  1013. break;
  1014.  
  1015. case '@':
  1016. $regex = str_replace( '\{@' . mb_substr( $tag, 1 ) . '\}', '(?!&)(.+?)', $regex );
  1017. $params[] = mb_substr( $tag, 1 );
  1018. break;
  1019.  
  1020. case '?':
  1021. $regex = str_replace( '\{\?\}', '(?!&)(.+?)', $regex );
  1022. $params[] = '';
  1023. break;
  1024. }
  1025. }
  1026.  
  1027. /* Now see if it matches */
  1028. if ( preg_match( '/^' . $regex . '(?:$|\/$|\?|\/\?|&|\/&)(.+?$)?/i', $query, $matches ) )
  1029. {
  1030. /* This will be used for FURL checks later */
  1031. $data['_dynamicInfo'] = array();
  1032. $data['_rebuiltUrl'] = '';
  1033.  
  1034. /* Get the variables we need to set from the "real" URL */
  1035. parse_str( $data['real'], $set );
  1036.  
  1037. /* Grab any other variables from the requested URL */
  1038. if( isset( $matches[1] ) AND count( $matches[1] ) AND ( mb_substr_count( $matches[1], '=' ) === 1 OR mb_strpos( $matches[1], '&' ) !== FALSE ) )
  1039. {
  1040. $morePieces = explode( '&', $matches[1] );
  1041.  
  1042. foreach( $morePieces as $_argument )
  1043. {
  1044. if( !\strpos( $_argument, '=' ) )
  1045. {
  1046. continue;
  1047. }
  1048.  
  1049. list( $k, $v ) = explode( '=', $_argument );
  1050. $set[ $k ] = $v;
  1051. }
  1052. }
  1053.  
  1054. /* Add variables from the friendly URL */
  1055. foreach ( $params as $k => $v )
  1056. {
  1057. if( $v )
  1058. {
  1059. $set[ $v ] = $matches[ $k + 1 ];
  1060. }
  1061. else
  1062. {
  1063. $data['_dynamicInfo'][] = \IPS\Settings::i()->htaccess_mod_rewrite ? $matches[ $k + 1 ] : urldecode( $matches[ $k + 1 ] );
  1064. }
  1065. }
  1066.  
  1067. /* Set them */
  1068. foreach ( $set as $k => $v )
  1069. {
  1070. $data['_rebuiltUrl'] .= $k . '=' . $v . '&';
  1071. }
  1072.  
  1073. $data['_rebuiltUrl'] = mb_substr( $data['_rebuiltUrl'], 0, -1 );
  1074.  
  1075. /* Query string might override */
  1076. if ( isset( $this->data['query'] ) )
  1077. {
  1078. parse_str( $this->data['query'], $qs );
  1079. foreach ( $qs as $k => $v )
  1080. {
  1081. $set[ $k ] = $v;
  1082. }
  1083. }
  1084.  
  1085. /* Figure out which template we used */
  1086. $usedTemplate = $data;
  1087. $usedTemplate['_template'] = $_key;
  1088. }
  1089. if( $usedTemplate )
  1090. {
  1091. break;
  1092. }
  1093. }
  1094.  
  1095. /* We need to explicitly do this so it is not carried over to the FURL checking below */
  1096. unset($data);
  1097.  
  1098. /* Some urls pick up lonely amps, lets remove those before we check */
  1099. if ( mb_strstr( $query, '&' ) )
  1100. {
  1101. while( mb_substr( $query, -1 ) === '&' )
  1102. {
  1103. $query = mb_substr( $query, 0, -1 );
  1104. }
  1105. }
  1106. }
  1107.  
  1108. /**
  1109. * Make a HTTP Request
  1110. *
  1111. * @param int|null $timeout Timeout
  1112. * @param string $httpVersion HTTP Version
  1113. * @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
  1114. * @return \IPS\Http\Request
  1115. */
  1116. public function request( $timeout=null, $httpVersion=null, $followRedirects=5 )
  1117. {
  1118. if( $timeout === null )
  1119. {
  1120. $timeout = \IPS\DEFAULT_REQUEST_TIMEOUT;
  1121. }
  1122.  
  1123. if ( function_exists( 'curl_init' ) and function_exists( 'curl_exec' ) and \IPS\BYPASS_CURL === false )
  1124. {
  1125. /* Older versions of curl can't handle chunked encoding properly.. */
  1126. $version = curl_version();
  1127.  
  1128. if( \IPS\FORCE_CURL or version_compare( $version['version'], '7.36', '>=' ) )
  1129. {
  1130. $requestObj = new \IPS\Http\Request\Curl( $this, $timeout, $httpVersion, $followRedirects );
  1131. }
  1132. }
  1133.  
  1134. if( !isset( $requestObj ) )
  1135. {
  1136. $requestObj = new \IPS\Http\Request\Sockets( $this, $timeout, $httpVersion, $followRedirects );
  1137. }
  1138.  
  1139. /* Set a default user-agent since many services block requests without one, i.e. spotify */
  1140. $requestObj->setHeaders( array( 'User-Agent' => 'IPS Community Suite 4.0' ) );
  1141.  
  1142. return $requestObj;
  1143. }
  1144.  
  1145. /**
  1146. * Import as file
  1147. *
  1148. * @param string $storageExtension The extension which specified the storage location to use
  1149. * @return \IPS\File
  1150. * @throws \RuntimeException
  1151. */
  1152. public function import( $storageExtension )
  1153. {
  1154. $parts = $this->utf8ParseUrl( $this->url );
  1155. return \IPS\File::create( $storageExtension, basename( $parts['path'] ), $this->request()->get() );
  1156. }
  1157.  
  1158. /**
  1159. * Make safe for ACP
  1160. *
  1161. * @param bool $resource If TRUE, will redirect silently
  1162. * @return \IPS\Http\Url
  1163. */
  1164. public function makeSafeForAcp( $resource=FALSE )
  1165. {
  1166. return static::internal( "app=core&module=system&controller=redirect", 'front' )->setQueryString( array(
  1167. 'url' => (string) $this,
  1168. 'key' => hash_hmac( "sha256", (string) $this, md5( \IPS\Settings::i()->sql_pass . \IPS\Settings::i()->board_url . \IPS\Settings::i()->sql_database ) ),
  1169. 'resource' => $resource
  1170. ) );
  1171. }
  1172.  
  1173. /**
  1174. * UTF-8 safe parse URL
  1175. *
  1176. * @param string $url URL to parse
  1177. * @return array
  1178. */
  1179. public function utf8ParseUrl( $url )
  1180. {
  1181. /* parse_url doesn't work with relative protocols */
  1182. $relativeProtocol = FALSE;
  1183. if ( mb_substr( $url, 0, 2 ) === '//' )
  1184. {
  1185. $relativeProtocol = TRUE;
  1186. $url = 'http:' . $url;
  1187. }
  1188.  
  1189. /* Encode UTF8 characters */
  1190. $encodedUrl = preg_replace_callback( '%[^:/@?&=#a-zA-Z0-9_\.,\-]+%usD', function( $matches ){ return urlencode( $matches[0] ); }, $url );
  1191.  
  1192. /* Parse */
  1193. $parts = parse_url( $encodedUrl ?: $url );
  1194.  
  1195. if( is_array( $parts ) AND count( $parts ) )
  1196. {
  1197. foreach( $parts as $name => $value )
  1198. {
  1199. /* @link http://community.invisionpower.com/4bugtrack/ampersand-in-link-text-stripped-r2686/ */
  1200. if ( $name === 'query' )
  1201. {
  1202. $value = str_replace( '%26', '%2526', $value );
  1203. }
  1204.  
  1205. $parts[ $name ] = urldecode($value);
  1206. }
  1207. }
  1208. else
  1209. {
  1210. $parts = parse_url( $url );
  1211. $parts = ( is_array( $parts ) ) ? $parts : array();
  1212. }
  1213.  
  1214. /* Take the schema out again if it's relative */
  1215. if ( $relativeProtocol )
  1216. {
  1217. $parts['scheme'] = '';
  1218. }
  1219.  
  1220. /* Return */
  1221. return $parts;
  1222. }
  1223. }
Add Comment
Please, Sign In to add comment