3 namespace wcf\system\request
;
5 use wcf\data\DatabaseObjectDecorator
;
6 use wcf\data\IIDObject
;
7 use wcf\data\page\PageCache
;
8 use wcf\system\application\ApplicationHandler
;
9 use wcf\system\language\LanguageFactory
;
11 use wcf\system\SingletonFactory
;
13 use wcf\util\StringUtil
;
16 * Handles relative links within the wcf.
19 * @copyright 2001-2019 WoltLab GmbH
20 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
22 final class LinkHandler
extends SingletonFactory
25 * regex object to extract controller data from controller class name
29 protected $controllerRegex;
32 * title search strings
35 protected $titleSearch = [];
38 * title replacement strings
41 protected $titleReplace = [];
46 protected function init()
48 $this->controllerRegex
= new Regex(
49 '^(?P<application>[a-z][a-z0-9]*)\\\\(?P<isAcp>acp\\\\)?.+\\\\(?P<controller>[^\\\\]+)(?:Action|Form|Page)$'
52 if (\
defined('URL_TITLE_COMPONENT_REPLACEMENT') && URL_TITLE_COMPONENT_REPLACEMENT
) {
53 $replacements = \
explode(
55 StringUtil
::unifyNewlines(StringUtil
::trim(URL_TITLE_COMPONENT_REPLACEMENT
))
57 foreach ($replacements as $replacement) {
58 if (\
strpos($replacement, '=') === false) {
61 $components = \
explode('=', $replacement);
62 $this->titleSearch
[] = $components[0];
63 $this->titleReplace
[] = $components[1];
69 * Returns in internal link based on the given fully qualified controller
72 * Important: The controller class is not checked if it actually exists.
73 * That check happens during the runtime.
75 * @throws \InvalidArgumentException if the passed string is no controller class name
78 public function getControllerLink(string $controllerClass, array $parameters = [], string $url = ''): string
80 if (!$this->controllerRegex
->match($controllerClass)) {
81 throw new \
InvalidArgumentException("Invalid controller '{$controllerClass}' passed.");
84 $matches = $this->controllerRegex
->getMatches();
86 // important: matches cannot overwrite explicitly set parameters
87 $parameters['application'] = $parameters['application'] ??
$matches['application'];
88 $parameters['isACP'] = $parameters['isACP'] ??
$matches['isAcp'];
89 $parameters['forceFrontend'] = $parameters['forceFrontend'] ??
!$matches['isAcp'];
91 return $this->getLink($matches['controller'], $parameters, $url);
95 * Returns a relative link.
97 public function getLink(?
string $controller = null, array $parameters = [], string $url = ''): string
99 $abbreviation = 'wcf';
101 $isACP = RequestHandler
::getInstance()->isACPRequest();
105 // enforce a certain level of sanitation and protection for links embedded in emails
106 if (isset($parameters['isEmail'])) {
107 if ((bool)$parameters['isEmail']) {
108 if (!isset($parameters['isACP']) ||
!(bool)$parameters['isACP']) {
109 $parameters['forceFrontend'] = true;
113 unset($parameters['isEmail']);
116 if (isset($parameters['application'])) {
117 $abbreviation = $parameters['application'];
119 if (isset($parameters['isRaw'])) {
120 $isRaw = $parameters['isRaw'];
121 unset($parameters['isRaw']);
123 if (isset($parameters['isACP'])) {
124 $isACP = (bool)$parameters['isACP'];
125 unset($parameters['isACP']);
127 if (isset($parameters['forceFrontend'])) {
128 if ($parameters['forceFrontend'] && $isACP) {
131 unset($parameters['forceFrontend']);
133 if (isset($parameters['encodeTitle'])) {
134 $encodeTitle = $parameters['encodeTitle'];
135 unset($parameters['encodeTitle']);
138 /** @deprecated 3.0 */
139 unset($parameters['appendSession']);
140 /** @deprecated 3.0 */
141 unset($parameters['forceWCF']);
143 // remove anchor before parsing
144 if (($pos = \
strpos($url, '#')) !== false) {
145 $anchor = \
substr($url, $pos);
146 $url = \
substr($url, 0, $pos);
150 if ($controller === null) {
152 $controller = 'Index';
153 if ($abbreviation !== 'wcf') {
154 throw new \
InvalidArgumentException("A 'controller' must be specified for non-'wcf' links in ACP.");
157 if ($abbreviation !== 'wcf') {
158 $application = ApplicationHandler
::getInstance()->getApplication($abbreviation);
159 if ($application === null) {
160 throw new \
RuntimeException("Unknown abbreviation '" . $abbreviation . "'.");
163 $landingPage = PageCache
::getInstance()->getPage($application->landingPageID
);
164 if ($landingPage === null) {
165 $landingPage = PageCache
::getInstance()
166 ->getPageByController(WCF
::getApplicationObject($application)->getPrimaryController());
169 if ($landingPage !== null) {
170 return $landingPage->getLink();
174 return PageCache
::getInstance()->getLandingPage()->getLink();
179 if (isset($parameters['object'])) {
181 !($parameters['object'] instanceof IRouteController
)
182 && $parameters['object'] instanceof DatabaseObjectDecorator
183 && $parameters['object']->getDecoratedObject() instanceof IRouteController
185 $parameters['object'] = $parameters['object']->getDecoratedObject();
188 if ($parameters['object'] instanceof IRouteController
) {
189 $parameters['id'] = $parameters['object']->getObjectID();
190 $parameters['title'] = $parameters['object']->getTitle();
191 } elseif ($parameters['object'] instanceof IIDObject
) {
192 $parameters['id'] = $parameters['object']->getObjectID();
195 unset($parameters['object']);
197 if (isset($parameters['title'])) {
198 // component replacement
199 if ($this->titleSearch
!== []) {
200 $parameters['title'] = \
str_replace($this->titleSearch
, $this->titleReplace
, $parameters['title']);
203 // remove illegal characters
204 $parameters['title'] = \trim
(
205 \
preg_replace('/[^\p{L}\p{N}]+/u', '-', $parameters['title']),
209 // trim to 80 characters
210 $parameters['title'] = \rtrim
(\
mb_substr($parameters['title'], 0, 80), '-');
211 $parameters['title'] = \
mb_strtolower($parameters['title']);
215 $parameters['title'] = \rawurlencode
($parameters['title']);
219 $parameters['controller'] = $controller;
221 $abbreviation = ControllerMap
::getInstance()->getApplicationOverride($abbreviation, $controller);
223 $routeURL = RouteHandler
::getInstance()->buildRoute($abbreviation, $parameters, $isACP);
224 if (!$isRaw && $url !== '') {
225 $routeURL .= \
str_contains($routeURL, '?') ?
'&' : '?';
228 // encode certain characters
230 $url = \
str_replace(['[', ']'], ['%5B', '%5D'], $url);
233 $url = $routeURL . $url;
235 // handle applications
237 $url = RouteHandler
::getHost() . RouteHandler
::getPath(['acp']) . ($isACP ?
'acp/' : '') . $url;
239 $application = ApplicationHandler
::getInstance()->getApplication($abbreviation);
240 if ($application === null) {
241 throw new \
InvalidArgumentException("Unknown application identifier '{$abbreviation}'.");
244 $pageURL = $application->getPageURL();
246 $url = $pageURL . ($isACP ?
'acp/' : '') . $url;
249 // append previously removed anchor
256 * Returns the full URL to a CMS page. The `$languageID` parameter is optional and if not
257 * present (or the integer value `-1` is given) will cause the handler to pick the correct
258 * language version based upon the user's language.
260 * Passing in an illegal page id will cause this method to fail silently, returning an
263 * @param int $pageID page id
264 * @param int $languageID language id, optional
265 * @return string full URL of empty string if `$pageID` is invalid
268 public function getCmsLink($pageID, $languageID = -1): string
272 // use current language
273 if ($languageID === -1) {
274 $data = ControllerMap
::getInstance()->lookupCmsPage($pageID, WCF
::getLanguage()->languageID
);
276 // no result, attempt to use the default language instead
277 $defaultLanguageID = LanguageFactory
::getInstance()->getDefaultLanguageID();
278 if ($data === null && $defaultLanguageID != WCF
::getLanguage()->languageID
) {
279 $data = ControllerMap
::getInstance()->lookupCmsPage($pageID, $defaultLanguageID);
282 // no result, possibly this is a non-multilingual page
283 if ($data === null) {
284 $data = ControllerMap
::getInstance()->lookupCmsPage($pageID, null);
287 $data = ControllerMap
::getInstance()->lookupCmsPage($pageID, $languageID);
290 // no result, page does not exist or at least not in the given language
291 if ($data === null) {
295 return $this->getLink($data['controller'], [
296 'application' => $data['application'],
297 'forceFrontend' => true,