PageRenderTime 49ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/library/XenForo/Link.php

https://github.com/hanguyenhuu/DTUI_201105
PHP | 773 lines | 470 code | 77 blank | 226 comment | 99 complexity | aabc6320083ce25dbd95d23ce73ec52e MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * Helper methods to generate links to content. Links generated
  4. * by this are not necessarily HTML escaped. The calling code
  5. * should escape them for the output context they apply to.
  6. *
  7. * @package XenForo_Core
  8. */
  9. class XenForo_Link
  10. {
  11. /**
  12. * Stores a cache of handlers for prefixes. Many types of links will
  13. * be generated multiple times on a page, so this cache reduces the
  14. * amount of object creation/validation necessary.
  15. *
  16. * @var array
  17. */
  18. protected static $_handlerCache = array();
  19. /**
  20. * URL prefix to use when generating a canonical link.
  21. *
  22. * @var string|null
  23. */
  24. protected static $_canonicalLinkPrefix = null;
  25. /**
  26. * If true, uses friendly URLs that don't include index.php or a query string (unless required).
  27. *
  28. * @var boolean
  29. */
  30. protected static $_useFriendlyUrls = false;
  31. protected $_linkString = '';
  32. protected $_canPrependFull = true;
  33. /**
  34. * Constructor. Use the static methods in general. However, you can create
  35. * an object of this type from a link builder to generate an arbitrary URL.
  36. *
  37. * @param string $linkString
  38. * @param boolean $canPrependFull True if the default full link prefix can be prepended to make a full URL
  39. */
  40. public function __construct($linkString, $canPrependFull = true)
  41. {
  42. $this->_linkString = $linkString;
  43. $this->_canPrependFull = $canPrependFull;
  44. }
  45. /**
  46. * @return string Link
  47. */
  48. public function __toString()
  49. {
  50. return $this->_linkString;
  51. }
  52. /**
  53. * @return boolean
  54. */
  55. public function canPrependFull()
  56. {
  57. return $this->_canPrependFull;
  58. }
  59. /**
  60. * Builds a link to a public resource. The type should contain a prefix
  61. * optionally split by a "/" with the specific action (eg "templates/edit").
  62. *
  63. * @param string $type Prefix and action
  64. * @param mixed $data Data that the prefix/action should be applied to, if applicable
  65. * @param array $extraParams Additional params
  66. *
  67. * @return string The link
  68. */
  69. public static function buildPublicLink($type, $data = null, array $extraParams = array(), $skipPrepend = false)
  70. {
  71. $type = self::_checkForFullLink($type, $fullLink, $fullLinkPrefix);
  72. $link = self::_buildLink('public', $type, $data, $extraParams);
  73. $queryString = self::buildQueryString($extraParams);
  74. if ($link instanceof XenForo_Link)
  75. {
  76. $isRaw = true;
  77. $canPrependFull = $link->canPrependFull();
  78. }
  79. else
  80. {
  81. $isRaw = false;
  82. $canPrependFull = true;
  83. }
  84. if (self::$_useFriendlyUrls || $isRaw)
  85. {
  86. $outputLink = ($queryString !== '' ? "$link?$queryString" : $link);
  87. }
  88. else
  89. {
  90. if ($queryString !== '' && $link !== '')
  91. {
  92. $append = "?$link&$queryString";
  93. }
  94. else
  95. {
  96. // 1 or neither of these has content
  97. $append = $link . $queryString;
  98. if ($append !== '')
  99. {
  100. $append = "?$append";
  101. }
  102. }
  103. if ($skipPrepend)
  104. {
  105. $outputLink = $append;
  106. }
  107. else
  108. {
  109. $outputLink = 'index.php' . $append;
  110. }
  111. }
  112. if ($fullLink && $canPrependFull)
  113. {
  114. $outputLink = $fullLinkPrefix . $outputLink;
  115. }
  116. if (($hashPos = strpos($type, '#')) !== false)
  117. {
  118. $outputLink .= substr($type, $hashPos);
  119. }
  120. if ($outputLink === '')
  121. {
  122. $outputLink = '.';
  123. }
  124. return $outputLink;
  125. }
  126. /**
  127. * Builds a link to an admin resource. The type should contain a prefix
  128. * optionally split by a "/" with the specific action (eg "templates/edit").
  129. *
  130. * @param string $type Prefix and action
  131. * @param mixed $data Data that the prefix/action should be applied to, if applicable
  132. * @param array $extraParams Additional params
  133. *
  134. * @return string The link
  135. */
  136. public static function buildAdminLink($type, $data = null, array $extraParams = array())
  137. {
  138. $type = self::_checkForFullLink($type, $fullLink, $fullLinkPrefix);
  139. $link = self::_buildLink('admin', $type, $data, $extraParams);
  140. $queryString = self::buildQueryString($extraParams);
  141. if ($queryString !== '' && $link !== '')
  142. {
  143. $append = $link . '&' . $queryString;
  144. }
  145. else
  146. {
  147. // 1 or neither of these has content
  148. $append = $link . $queryString;
  149. }
  150. if (($hashPos = strpos($type, '#')) !== false)
  151. {
  152. $append .= substr($type, $hashPos);
  153. }
  154. $outputLink = 'admin.php' . ($append !== '' ? '?' : '') . $append;
  155. if ($fullLink)
  156. {
  157. $outputLink = $fullLinkPrefix . $outputLink;
  158. }
  159. return $outputLink;
  160. }
  161. /**
  162. * Check to see if a full link is requested.
  163. *
  164. * @param string $type Link type
  165. * @param boolean $fullLink Modified by ref. Returns whether a full link is requested.
  166. * @param string $fullLinkPrefix If a full link is requested, the prefix to use
  167. *
  168. * @return string Link type, with full link param stripped off if necessary
  169. */
  170. protected static function _checkForFullLink($type, &$fullLink, &$fullLinkPrefix)
  171. {
  172. if (!$type)
  173. {
  174. $fullLink = false;
  175. $fullLinkPrefix = '';
  176. return $type;
  177. }
  178. if ($type[0] == 'c' && substr($type, 0, 10) == 'canonical:')
  179. {
  180. $type = substr($type, 10);
  181. $fullLink = true;
  182. $fullLinkPrefix = self::getCanonicalLinkPrefix() . '/';
  183. }
  184. else if ($type[0] == 'f' && substr($type, 0, 5) == 'full:')
  185. {
  186. $type = substr($type, 5);
  187. $fullLink = true;
  188. $paths = XenForo_Application::get('requestPaths');
  189. $fullLinkPrefix = $paths['fullBasePath'];
  190. }
  191. else
  192. {
  193. $fullLink = false;
  194. $fullLinkPrefix = '';
  195. }
  196. return $type;
  197. }
  198. /**
  199. * Internal link builder.
  200. *
  201. * @param string $group Type of link being built (admin or public)
  202. * @param string $type Type of data the link is for (prefix and action)
  203. * @param mixed $data
  204. * @param array $extraParams
  205. *
  206. * @return string
  207. */
  208. protected static function _buildLink($group, $type, $data, array &$extraParams)
  209. {
  210. if (isset($extraParams['_params']) && is_array($extraParams['_params']))
  211. {
  212. $params = $extraParams['_params'];
  213. unset($extraParams['_params']);
  214. $extraParams = array_merge($params, $extraParams);
  215. }
  216. $extension = '';
  217. if (($dotPos = strrpos($type, '.')) !== false)
  218. {
  219. $extension = substr($type, $dotPos + 1);
  220. $type = substr($type, 0, $dotPos);
  221. }
  222. if (($hashPos = strpos($type, '#')) !== false)
  223. {
  224. $type = substr($type, 0, $hashPos);
  225. }
  226. if (($slashPos = strpos($type, '/')) !== false)
  227. {
  228. list($prefix, $action) = explode('/', $type, 2);
  229. if ($action == 'index')
  230. {
  231. $action = '';
  232. }
  233. }
  234. else
  235. {
  236. $prefix = $type;
  237. $action = '';
  238. }
  239. unset($type);
  240. $handler = self::_getPrefixHandler($group, $prefix, (boolean)$data);
  241. if ($handler === false)
  242. {
  243. $link = false;
  244. }
  245. else
  246. {
  247. $link = $handler->buildLink($prefix, $prefix, $action, $extension, $data, $extraParams);
  248. }
  249. if ($link === false || $link === null)
  250. {
  251. return self::buildBasicLink($prefix, $action, $extension);
  252. }
  253. else
  254. {
  255. return $link;
  256. }
  257. }
  258. /**
  259. * Gets the object that should handle building the link for this prefix.
  260. * May also return false if only the standard behavior is desired.
  261. *
  262. * @param string $group Type of link (public or admin)
  263. * @param string $originalPrefix Prefix to build the link for (should be the "original prefix" in the DB)
  264. * @param boolean $haveData Whether we have a data element
  265. *
  266. * @return object|false Object with "buildLink" method or false
  267. */
  268. protected static function _getPrefixHandler($group, $originalPrefix, $haveData)
  269. {
  270. if (!isset(self::$_handlerCache[$group]))
  271. {
  272. self::$_handlerCache[$group] = self::_loadHandlerInfoForGroup($group);
  273. }
  274. if (!isset(self::$_handlerCache[$group][$originalPrefix]))
  275. {
  276. return false;
  277. }
  278. $info =& self::$_handlerCache[$group][$originalPrefix];
  279. if ($haveData)
  280. {
  281. if (!isset($info['handlerWithData']))
  282. {
  283. $info['handlerWithData'] = self::_loadPrefixHandlerClass($info, true);
  284. }
  285. return $info['handlerWithData'];
  286. }
  287. else
  288. {
  289. if (!isset($info['handlerNoData']))
  290. {
  291. $info['handlerNoData'] = self::_loadPrefixHandlerClass($info, false);
  292. }
  293. return $info['handlerNoData'];
  294. }
  295. }
  296. /**
  297. * Load the prefix link build handler class based on current settings.
  298. *
  299. * @param array $info Info about how to build this link (includes build_link, route_class keys)
  300. * @param boolean $haveData True if we have a data param for this link
  301. *
  302. * @return object|false Object with "buildLink" method or false
  303. */
  304. protected static function _loadPrefixHandlerClass(array $info, $haveData)
  305. {
  306. if ($info['build_link'] == 'none' || ($info['build_link'] == 'data_only' && !$haveData))
  307. {
  308. // never build or only build when we have data (and we don't now)
  309. return false;
  310. }
  311. if ($info['build_link'] == 'all')
  312. {
  313. // always build - check for a previous call
  314. if (isset($info['handlerWithData']))
  315. {
  316. return $info['handlerWithData'];
  317. }
  318. else if (isset($info['handlerNoData']))
  319. {
  320. return $info['handlerNoData'];
  321. }
  322. }
  323. // ...otherwise load the class we need
  324. $class = XenForo_Application::resolveDynamicClass($info['route_class'], 'route_prefix');
  325. if (!$class)
  326. {
  327. return false;
  328. }
  329. $handler = new $class();
  330. if (!method_exists($handler, 'buildLink'))
  331. {
  332. return false;
  333. }
  334. return $handler;
  335. }
  336. /**
  337. * Loads all the link build handler data for an entire group of prefixes.
  338. *
  339. * @param string $group Type of prefix (public or admin)
  340. *
  341. * @return array Keys are "original prefixes" and values are info about output prefix/class/build settings
  342. */
  343. protected static function _loadHandlerInfoForGroup($group)
  344. {
  345. return XenForo_Model::create('XenForo_Model_RoutePrefix')->getPrefixesForRouteCache($group);
  346. }
  347. /**
  348. * Gets the name of the specified prefix handler class.
  349. *
  350. * @param string $group
  351. * @param string $prefix
  352. *
  353. * @return string|false
  354. */
  355. public static function getPrefixHandlerClassName($group, $prefix)
  356. {
  357. if (!isset(self::$_handlerCache[$group]))
  358. {
  359. self::$_handlerCache[$group] = self::_loadHandlerInfoForGroup($group);
  360. }
  361. if (!isset(self::$_handlerCache[$group][$prefix]))
  362. {
  363. return false;
  364. }
  365. return self::$_handlerCache[$group][$prefix]['route_class'];
  366. }
  367. /**
  368. * Examines action and extra parameters from a link build call and formulates
  369. * a page number link parameter if applicable.
  370. *
  371. * @param string $action
  372. * @param array $params
  373. *
  374. * @return string $action
  375. */
  376. public static function getPageNumberAsAction($action, array &$params)
  377. {
  378. if (isset($params['page']))
  379. {
  380. if (strval($params['page']) !== XenForo_Application::$integerSentinel && $params['page'] <= 1)
  381. {
  382. unset($params['page']);
  383. }
  384. else if (!$action)
  385. {
  386. if ($params['page'] != XenForo_Application::$integerSentinel)
  387. {
  388. $params['page'] = intval($params['page']);
  389. }
  390. $action = "page-$params[page]";
  391. unset($params['page']);
  392. }
  393. }
  394. return $action;
  395. }
  396. /**
  397. * Helper to manually set handler info for a group. Keys should be "original prefixes"
  398. * and values should be arrays with keys matching the xf_route_prefix table.
  399. *
  400. * @param string $group Type of prefix to handle (public or admin)
  401. * @param array $info Info to set
  402. */
  403. public static function setHandlerInfoForGroup($group, array $info)
  404. {
  405. self::$_handlerCache[$group] = $info;
  406. }
  407. /**
  408. * Resets the handlers for all groups or for a particular group. Mainly used for testing.
  409. *
  410. * @param string|false $group If false, resets all handlers; otherwise, resets the specified handler group
  411. */
  412. public static function resetHandlerInfo($group = false)
  413. {
  414. if ($group === false)
  415. {
  416. self::$_handlerCache = array();
  417. }
  418. else
  419. {
  420. unset(self::$_handlerCache[strval($group)]);
  421. }
  422. }
  423. /**
  424. * Builds a basic link: a prefix and action only.
  425. *
  426. * @param string $prefix
  427. * @param string $action
  428. * @param string $extension
  429. *
  430. * @return string
  431. */
  432. public static function buildBasicLink($prefix, $action, $extension = '')
  433. {
  434. if ($extension)
  435. {
  436. self::prepareExtensionAndAction($extension, $action);
  437. }
  438. if ($prefix === 'index' && $action === '')
  439. {
  440. return '';
  441. }
  442. else
  443. {
  444. return "$prefix/$action$extension";
  445. }
  446. }
  447. /**
  448. * Prepares the link extension and action, if necessary. If an extension is specified,
  449. * the provided value will be prefixed with a ".". If there is an extension and there's
  450. * no action, an explicit "index" action will be specified.
  451. *
  452. * @param string $extension Initially, the extension to the link specified; prefixed with "." if necessary
  453. * @param string $action The link action; modified if necessary
  454. */
  455. public static function prepareExtensionAndAction(&$extension, &$action, $prepareAction = true)
  456. {
  457. if ($extension)
  458. {
  459. $extension = '.' . $extension;
  460. if ($action === '')
  461. {
  462. $action = 'index';
  463. }
  464. }
  465. }
  466. /**
  467. * Builds a basic link for a request that may have an integer param.
  468. * Output will be in the format [prefix]/[int]-[title]/[action] or similar,
  469. * based on whether the correct values in data are set.
  470. *
  471. * @param string $prefix Link prefix
  472. * @param string $action Link action
  473. * @param string $extension Link extension (for content type)
  474. * @param mixed $data Specific data to link to. If available, an array or an object that implements ArrayAccess
  475. * @param string $intField The name of the field that holds the integer identifier
  476. * @param string $titleField If there is a title field, the name of the field that holds the title
  477. *
  478. * @return false|string False if no data is provided, the link otherwise
  479. */
  480. public static function buildBasicLinkWithIntegerParam($prefix, $action, $extension, $data, $intField, $titleField = '')
  481. {
  482. if ((is_array($data) || $data instanceof ArrayAccess) && isset($data[$intField]))
  483. {
  484. self::prepareExtensionAndAction($extension, $action);
  485. $title = (($titleField && !empty($data[$titleField])) ? $data[$titleField] : '');
  486. return "$prefix/" . self::buildIntegerAndTitleUrlComponent($data[$intField], $title) . "/$action$extension";
  487. }
  488. else
  489. {
  490. return false;
  491. }
  492. }
  493. /**
  494. * Builds a basic link for a request that may have a string param.
  495. * Output will be in the format [prefix]/[param]/[action].
  496. *
  497. * Note that it is expected that the string param is already clean enough
  498. * to be inserted into the link.
  499. *
  500. * @param string $prefix Link prefix
  501. * @param string $action Link action
  502. * @param string $extension Link extension (for content type)
  503. * @param mixed $data Specific data to link to. If available, an array or an object that implements ArrayAccess, or a simple string to be used directly
  504. * @param string $strField The name of the field that holds the string identifier
  505. *
  506. * @return false|string False if no data is provided, the link otherwise
  507. */
  508. public static function buildBasicLinkWithStringParam($prefix, $action, $extension, $data, $strField)
  509. {
  510. if ($data)
  511. {
  512. self::prepareExtensionAndAction($extension, $action);
  513. if ((is_array($data) || $data instanceof ArrayAccess)
  514. && isset($data[$strField])
  515. && $data[$strField] !== '')
  516. {
  517. return "$prefix/" . $data[$strField] . "/$action$extension";
  518. }
  519. else if (is_string($data))
  520. {
  521. return "$prefix/$data/$action$extension";
  522. }
  523. }
  524. return false;
  525. }
  526. /**
  527. * Builds the URL component for an integer and title. Outputs <int> or <int>-<title>.
  528. *
  529. * @param integer $integer
  530. * @param string $title
  531. * @param boolean $romanize If true, non-latin strings are romanized
  532. *
  533. * @return string
  534. */
  535. public static function buildIntegerAndTitleUrlComponent($integer, $title = '', $romanize = false)
  536. {
  537. if ($title && XenForo_Application::get('options')->includeTitleInUrls)
  538. {
  539. # /item-title.id/ (where delimiter is '.')
  540. return urlencode(self::getTitleForUrl($title, $romanize)) . XenForo_Application::URL_ID_DELIMITER . intval($integer);
  541. }
  542. else
  543. {
  544. return intval($integer);
  545. }
  546. }
  547. /**
  548. * Gets version of a title that is valid in a URL. Invalid elements are stripped
  549. * or replaced with '-'. It may not be possible to reverse a URL'd title to the
  550. * original title.
  551. *
  552. * @param string $title
  553. * @param boolean $romanize If true, non-latin strings are romanized
  554. *
  555. * @return string
  556. */
  557. public static function getTitleForUrl($title, $romanize = false)
  558. {
  559. if ($romanize)
  560. {
  561. $title = utf8_romanize(utf8_deaccent($title));
  562. }
  563. $title = strtr(
  564. $title,
  565. '`!"$%^&*()-+={}[]<>;:@#~,./?|' . "\r\n\t\\",
  566. ' ' . ' '
  567. );
  568. $title = strtr($title, array('"' => '', "'" => ''));
  569. $title = preg_replace('/[ ]+/', '-', trim($title));
  570. return strtr($title, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
  571. }
  572. /**
  573. * Builds a query string from an array of items. Keys of the array will become
  574. * names of items in the query string. Nested arrays are supported.
  575. *
  576. * @param array $elements Elements to build the query string from
  577. * @param string $prefix For nested arrays, specifies the base context we're in.
  578. * Leave default unless wanting all elements inside an array.
  579. *
  580. * @return string
  581. */
  582. public static function buildQueryString(array $elements, $prefix = '')
  583. {
  584. $output = array();
  585. foreach ($elements AS $name => $value)
  586. {
  587. if (is_array($value))
  588. {
  589. if (!$value)
  590. {
  591. continue;
  592. }
  593. $encodedName = ($prefix ? $prefix . '[' . urlencode($name) . ']' : urlencode($name));
  594. $childOutput = self::buildQueryString($value, $encodedName);
  595. if ($childOutput !== '')
  596. {
  597. $output[] = $childOutput;
  598. }
  599. }
  600. else
  601. {
  602. if ($value === null || $value === false || $value === '')
  603. {
  604. continue;
  605. }
  606. $value = strval($value);
  607. if ($prefix)
  608. {
  609. // part of an array
  610. $output[] = $prefix . '[' . urlencode($name) . ']=' . urlencode($value);
  611. }
  612. else
  613. {
  614. $output[] = urlencode($name) . '=' . urlencode($value);
  615. }
  616. }
  617. }
  618. return implode('&', $output);
  619. }
  620. /**
  621. * Set the prefix for links that are generated as canonical links.
  622. *
  623. * @param string $linkPrefix
  624. */
  625. public static function setCanonicalLinkPrefix($linkPrefix)
  626. {
  627. self::$_canonicalLinkPrefix = $linkPrefix;
  628. }
  629. /**
  630. * Gets the canonical link prefix to use for generating canonical links.
  631. *
  632. * @return string
  633. */
  634. public static function getCanonicalLinkPrefix()
  635. {
  636. if (self::$_canonicalLinkPrefix === null)
  637. {
  638. self::$_canonicalLinkPrefix = XenForo_Application::get('options')->boardUrl;
  639. }
  640. return self::$_canonicalLinkPrefix;
  641. }
  642. /**
  643. * Sets whether friendly URLs should be used for generating links.
  644. *
  645. * @param boolean $value
  646. */
  647. public static function useFriendlyUrls($value)
  648. {
  649. self::$_useFriendlyUrls = $value;
  650. }
  651. /**
  652. * Converts what may be a relative link into an absolute URI.
  653. *
  654. * @param string $uri URI to convert
  655. * @param boolean $includeHost If true, includes host, port, and protocol
  656. * @param array|null $paths Paths to override (uses application level if not provided)
  657. *
  658. * @return string
  659. */
  660. public static function convertUriToAbsoluteUri($uri, $includeHost = false, array $paths = null)
  661. {
  662. if (!$paths)
  663. {
  664. $paths = XenForo_Application::get('requestPaths');
  665. }
  666. if ($uri == '.')
  667. {
  668. $uri = ''; // current directory
  669. }
  670. if (substr($uri, 0, 1) == '/')
  671. {
  672. if ($includeHost)
  673. {
  674. return $paths['protocol'] . '://' . $paths['host'] . $uri;
  675. }
  676. else
  677. {
  678. return $uri;
  679. }
  680. }
  681. else if (preg_match('#^[a-z0-9-]+://#i', $uri))
  682. {
  683. return $uri;
  684. }
  685. else if ($includeHost)
  686. {
  687. return $paths['fullBasePath'] . $uri;
  688. }
  689. else
  690. {
  691. return $paths['basePath'] . $uri;
  692. }
  693. }
  694. }