Commit | Line | Data |
---|---|---|
11ade432 | 1 | <?php |
a9229942 | 2 | |
11ade432 | 3 | namespace wcf\system\request; |
a9229942 | 4 | |
3cd3cfc3 | 5 | use wcf\data\DatabaseObjectDecorator; |
86043833 | 6 | use wcf\data\IIDObject; |
a9229942 | 7 | use wcf\data\page\PageCache; |
11ade432 | 8 | use wcf\system\application\ApplicationHandler; |
3295fb92 | 9 | use wcf\system\language\LanguageFactory; |
c8a4a1f5 | 10 | use wcf\system\Regex; |
11ade432 | 11 | use wcf\system\SingletonFactory; |
bc62b9c1 | 12 | use wcf\system\WCF; |
1db37793 | 13 | use wcf\util\StringUtil; |
11ade432 AE |
14 | |
15 | /** | |
16 | * Handles relative links within the wcf. | |
a9229942 TD |
17 | * |
18 | * @author Marcel Werk | |
19 | * @copyright 2001-2019 WoltLab GmbH | |
20 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
11ade432 | 21 | */ |
7e2f15f0 | 22 | final class LinkHandler extends SingletonFactory |
a9229942 TD |
23 | { |
24 | /** | |
25 | * regex object to extract controller data from controller class name | |
26 | * @var Regex | |
27 | * @since 5.2 | |
28 | */ | |
29 | protected $controllerRegex; | |
30 | ||
a9229942 TD |
31 | /** |
32 | * title search strings | |
33 | * @var string[] | |
34 | */ | |
35 | protected $titleSearch = []; | |
36 | ||
37 | /** | |
38 | * title replacement strings | |
39 | * @var string[] | |
40 | */ | |
41 | protected $titleReplace = []; | |
42 | ||
43 | /** | |
44 | * @inheritDoc | |
45 | */ | |
46 | protected function init() | |
47 | { | |
a9229942 TD |
48 | $this->controllerRegex = new Regex( |
49 | '^(?P<application>[a-z][a-z0-9]*)\\\\(?P<isAcp>acp\\\\)?.+\\\\(?P<controller>[^\\\\]+)(?:Action|Form|Page)$' | |
50 | ); | |
51 | ||
52 | if (\defined('URL_TITLE_COMPONENT_REPLACEMENT') && URL_TITLE_COMPONENT_REPLACEMENT) { | |
53 | $replacements = \explode( | |
54 | "\n", | |
55 | StringUtil::unifyNewlines(StringUtil::trim(URL_TITLE_COMPONENT_REPLACEMENT)) | |
56 | ); | |
57 | foreach ($replacements as $replacement) { | |
58 | if (\strpos($replacement, '=') === false) { | |
59 | continue; | |
60 | } | |
61 | $components = \explode('=', $replacement); | |
62 | $this->titleSearch[] = $components[0]; | |
63 | $this->titleReplace[] = $components[1]; | |
64 | } | |
65 | } | |
66 | } | |
67 | ||
68 | /** | |
69 | * Returns in internal link based on the given fully qualified controller | |
70 | * class name. | |
71 | * | |
72 | * Important: The controller class is not checked if it actually exists. | |
73 | * That check happens during the runtime. | |
74 | * | |
a9229942 TD |
75 | * @throws \InvalidArgumentException if the passed string is no controller class name |
76 | * @since 5.2 | |
77 | */ | |
8dcf63b1 | 78 | public function getControllerLink(string $controllerClass, array $parameters = [], string $url = ''): string |
a9229942 TD |
79 | { |
80 | if (!$this->controllerRegex->match($controllerClass)) { | |
81 | throw new \InvalidArgumentException("Invalid controller '{$controllerClass}' passed."); | |
82 | } | |
83 | ||
84 | $matches = $this->controllerRegex->getMatches(); | |
85 | ||
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']; | |
90 | ||
91 | return $this->getLink($matches['controller'], $parameters, $url); | |
92 | } | |
93 | ||
94 | /** | |
95 | * Returns a relative link. | |
a9229942 | 96 | */ |
8dcf63b1 | 97 | public function getLink(?string $controller = null, array $parameters = [], string $url = ''): string |
a9229942 TD |
98 | { |
99 | $abbreviation = 'wcf'; | |
100 | $anchor = ''; | |
13b11e4c | 101 | $isACP = RequestHandler::getInstance()->isACPRequest(); |
a9229942 TD |
102 | $isRaw = false; |
103 | $encodeTitle = true; | |
104 | ||
a9229942 TD |
105 | // enforce a certain level of sanitation and protection for links embedded in emails |
106 | if (isset($parameters['isEmail'])) { | |
107 | if ((bool)$parameters['isEmail']) { | |
d95065c1 MW |
108 | if (!isset($parameters['isACP']) || !(bool)$parameters['isACP']) { |
109 | $parameters['forceFrontend'] = true; | |
110 | } | |
a9229942 TD |
111 | } |
112 | ||
113 | unset($parameters['isEmail']); | |
114 | } | |
115 | ||
116 | if (isset($parameters['application'])) { | |
117 | $abbreviation = $parameters['application']; | |
118 | } | |
119 | if (isset($parameters['isRaw'])) { | |
120 | $isRaw = $parameters['isRaw']; | |
121 | unset($parameters['isRaw']); | |
122 | } | |
a9229942 TD |
123 | if (isset($parameters['isACP'])) { |
124 | $isACP = (bool)$parameters['isACP']; | |
125 | unset($parameters['isACP']); | |
126 | } | |
127 | if (isset($parameters['forceFrontend'])) { | |
128 | if ($parameters['forceFrontend'] && $isACP) { | |
129 | $isACP = false; | |
130 | } | |
131 | unset($parameters['forceFrontend']); | |
132 | } | |
a9229942 TD |
133 | if (isset($parameters['encodeTitle'])) { |
134 | $encodeTitle = $parameters['encodeTitle']; | |
135 | unset($parameters['encodeTitle']); | |
136 | } | |
137 | ||
b1a7b2bd TD |
138 | /** @deprecated 3.0 */ |
139 | unset($parameters['appendSession']); | |
140 | /** @deprecated 3.0 */ | |
141 | unset($parameters['forceWCF']); | |
142 | ||
a9229942 TD |
143 | // remove anchor before parsing |
144 | if (($pos = \strpos($url, '#')) !== false) { | |
145 | $anchor = \substr($url, $pos); | |
146 | $url = \substr($url, 0, $pos); | |
147 | } | |
148 | ||
149 | // build route | |
150 | if ($controller === null) { | |
151 | if ($isACP) { | |
152 | $controller = 'Index'; | |
390f92b1 TD |
153 | if ($abbreviation !== 'wcf') { |
154 | throw new \InvalidArgumentException("A 'controller' must be specified for non-'wcf' links in ACP."); | |
155 | } | |
a9229942 | 156 | } else { |
85787b74 | 157 | if ($abbreviation !== 'wcf') { |
a9229942 TD |
158 | $application = ApplicationHandler::getInstance()->getApplication($abbreviation); |
159 | if ($application === null) { | |
160 | throw new \RuntimeException("Unknown abbreviation '" . $abbreviation . "'."); | |
161 | } | |
162 | ||
163 | $landingPage = PageCache::getInstance()->getPage($application->landingPageID); | |
164 | if ($landingPage === null) { | |
165 | $landingPage = PageCache::getInstance() | |
166 | ->getPageByController(WCF::getApplicationObject($application)->getPrimaryController()); | |
167 | } | |
168 | ||
169 | if ($landingPage !== null) { | |
170 | return $landingPage->getLink(); | |
171 | } | |
172 | } | |
173 | ||
174 | return PageCache::getInstance()->getLandingPage()->getLink(); | |
175 | } | |
176 | } | |
177 | ||
178 | // handle object | |
179 | if (isset($parameters['object'])) { | |
180 | if ( | |
181 | !($parameters['object'] instanceof IRouteController) | |
182 | && $parameters['object'] instanceof DatabaseObjectDecorator | |
183 | && $parameters['object']->getDecoratedObject() instanceof IRouteController | |
184 | ) { | |
185 | $parameters['object'] = $parameters['object']->getDecoratedObject(); | |
186 | } | |
187 | ||
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(); | |
193 | } | |
194 | } | |
195 | unset($parameters['object']); | |
196 | ||
197 | if (isset($parameters['title'])) { | |
198 | // component replacement | |
49c7aa51 | 199 | if ($this->titleSearch !== []) { |
a9229942 TD |
200 | $parameters['title'] = \str_replace($this->titleSearch, $this->titleReplace, $parameters['title']); |
201 | } | |
202 | ||
203 | // remove illegal characters | |
8d026a18 TD |
204 | $parameters['title'] = \trim( |
205 | \preg_replace('/[^\p{L}\p{N}]+/u', '-', $parameters['title']), | |
206 | '-' | |
207 | ); | |
a9229942 TD |
208 | |
209 | // trim to 80 characters | |
210 | $parameters['title'] = \rtrim(\mb_substr($parameters['title'], 0, 80), '-'); | |
211 | $parameters['title'] = \mb_strtolower($parameters['title']); | |
212 | ||
213 | // encode title | |
214 | if ($encodeTitle) { | |
215 | $parameters['title'] = \rawurlencode($parameters['title']); | |
216 | } | |
217 | } | |
218 | ||
219 | $parameters['controller'] = $controller; | |
220 | if (!$isACP) { | |
221 | $abbreviation = ControllerMap::getInstance()->getApplicationOverride($abbreviation, $controller); | |
222 | } | |
223 | $routeURL = RouteHandler::getInstance()->buildRoute($abbreviation, $parameters, $isACP); | |
0257c7bb | 224 | if (!$isRaw && $url !== '') { |
387df230 | 225 | $routeURL .= \str_contains($routeURL, '?') ? '&' : '?'; |
a9229942 TD |
226 | } |
227 | ||
228 | // encode certain characters | |
0257c7bb | 229 | if ($url !== '') { |
a9229942 TD |
230 | $url = \str_replace(['[', ']'], ['%5B', '%5D'], $url); |
231 | } | |
232 | ||
233 | $url = $routeURL . $url; | |
234 | ||
235 | // handle applications | |
236 | if (!PACKAGE_ID) { | |
237 | $url = RouteHandler::getHost() . RouteHandler::getPath(['acp']) . ($isACP ? 'acp/' : '') . $url; | |
238 | } else { | |
392a1446 TD |
239 | $application = ApplicationHandler::getInstance()->getApplication($abbreviation); |
240 | if ($application === null) { | |
241 | throw new \InvalidArgumentException("Unknown application identifier '{$abbreviation}'."); | |
a9229942 TD |
242 | } |
243 | ||
392a1446 TD |
244 | $pageURL = $application->getPageURL(); |
245 | ||
a9229942 TD |
246 | $url = $pageURL . ($isACP ? 'acp/' : '') . $url; |
247 | } | |
248 | ||
249 | // append previously removed anchor | |
250 | $url .= $anchor; | |
251 | ||
252 | return $url; | |
253 | } | |
254 | ||
255 | /** | |
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. | |
259 | * | |
260 | * Passing in an illegal page id will cause this method to fail silently, returning an | |
261 | * empty string. | |
262 | * | |
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 | |
266 | * @since 3.0 | |
267 | */ | |
8dcf63b1 | 268 | public function getCmsLink($pageID, $languageID = -1): string |
a9229942 | 269 | { |
94e6fee5 TD |
270 | $data = null; |
271 | ||
a9229942 TD |
272 | // use current language |
273 | if ($languageID === -1) { | |
274 | $data = ControllerMap::getInstance()->lookupCmsPage($pageID, WCF::getLanguage()->languageID); | |
275 | ||
94e6fee5 TD |
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); | |
280 | } | |
a9229942 | 281 | |
94e6fee5 TD |
282 | // no result, possibly this is a non-multilingual page |
283 | if ($data === null) { | |
284 | $data = ControllerMap::getInstance()->lookupCmsPage($pageID, null); | |
a9229942 TD |
285 | } |
286 | } else { | |
287 | $data = ControllerMap::getInstance()->lookupCmsPage($pageID, $languageID); | |
94e6fee5 | 288 | } |
a9229942 | 289 | |
94e6fee5 TD |
290 | // no result, page does not exist or at least not in the given language |
291 | if ($data === null) { | |
292 | return ''; | |
a9229942 TD |
293 | } |
294 | ||
295 | return $this->getLink($data['controller'], [ | |
296 | 'application' => $data['application'], | |
297 | 'forceFrontend' => true, | |
298 | ]); | |
299 | } | |
11ade432 | 300 | } |