Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
33.59% |
44 / 131 |
|
18.75% |
3 / 16 |
CRAP | |
0.00% |
0 / 1 |
SpecialRedirect | |
33.85% |
44 / 130 |
|
18.75% |
3 / 16 |
744.12 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setParameter | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
dispatchUser | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
5.02 | |||
dispatchFile | |
47.06% |
8 / 17 |
|
0.00% |
0 / 1 |
24.84 | |||
dispatchRevision | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
dispatchPage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
dispatchLog | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
dispatch | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
156 | |||
getFormFields | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
2 | |||
onSubmit | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
onSuccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
alterForm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDisplayFormat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSubpagesForPrefixSearch | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
requiresPost | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Specials; |
22 | |
23 | use MediaWiki\HTMLForm\HTMLForm; |
24 | use MediaWiki\SpecialPage\FormSpecialPage; |
25 | use MediaWiki\Status\Status; |
26 | use MediaWiki\Title\MalformedTitleException; |
27 | use MediaWiki\Title\Title; |
28 | use MediaWiki\User\UserFactory; |
29 | use PermissionsError; |
30 | use RepoGroup; |
31 | |
32 | /** |
33 | * Redirect dispatcher for user IDs, thumbnails, and various permalinks. |
34 | * |
35 | * - user: the user page for a given numeric user ID. |
36 | * - file: the file thumbnail URL for a given filename. |
37 | * - revision: permalink for any revision. |
38 | * - page: permalink for page by numeric page ID. |
39 | * - logid: permalink for any log entry. |
40 | * |
41 | * @ingroup SpecialPage |
42 | * @since 1.22 |
43 | */ |
44 | class SpecialRedirect extends FormSpecialPage { |
45 | |
46 | /** |
47 | * The type of the redirect (user/file/revision) |
48 | * |
49 | * Example value: `'user'` |
50 | * |
51 | * @var string|null |
52 | */ |
53 | protected $mType; |
54 | |
55 | /** |
56 | * The identifier/value for the redirect (which id, which file) |
57 | * |
58 | * Example value: `'42'` |
59 | * |
60 | * @var string|null |
61 | */ |
62 | protected $mValue; |
63 | |
64 | private RepoGroup $repoGroup; |
65 | private UserFactory $userFactory; |
66 | |
67 | /** |
68 | * @param RepoGroup $repoGroup |
69 | * @param UserFactory $userFactory |
70 | */ |
71 | public function __construct( |
72 | RepoGroup $repoGroup, |
73 | UserFactory $userFactory |
74 | ) { |
75 | parent::__construct( 'Redirect' ); |
76 | $this->mType = null; |
77 | $this->mValue = null; |
78 | |
79 | $this->repoGroup = $repoGroup; |
80 | $this->userFactory = $userFactory; |
81 | } |
82 | |
83 | /** |
84 | * Set $mType and $mValue based on parsed value of $subpage. |
85 | * @param string|null $subpage |
86 | */ |
87 | public function setParameter( $subpage ) { |
88 | // parse $subpage to pull out the parts |
89 | $parts = $subpage !== null ? explode( '/', $subpage, 2 ) : []; |
90 | $this->mType = $parts[0] ?? null; |
91 | $this->mValue = $parts[1] ?? null; |
92 | } |
93 | |
94 | /** |
95 | * Handle Special:Redirect/user/xxxx (by redirecting to User:YYYY) |
96 | * |
97 | * @return Status A good status contains the url to redirect to |
98 | */ |
99 | public function dispatchUser() { |
100 | if ( !ctype_digit( $this->mValue ) ) { |
101 | return Status::newFatal( 'redirect-not-numeric' ); |
102 | } |
103 | $user = $this->userFactory->newFromId( (int)$this->mValue ); |
104 | $user->load(); // Make sure the id is validated by loading the user |
105 | if ( $user->isAnon() ) { |
106 | return Status::newFatal( 'redirect-not-exists' ); |
107 | } |
108 | if ( $user->isHidden() && !$this->getAuthority()->isAllowed( 'hideuser' ) ) { |
109 | throw new PermissionsError( null, [ 'badaccess-group0' ] ); |
110 | } |
111 | |
112 | return Status::newGood( [ |
113 | $user->getUserPage()->getFullURL( '', false, PROTO_CURRENT ), 302 |
114 | ] ); |
115 | } |
116 | |
117 | /** |
118 | * Handle Special:Redirect/file/xxxx |
119 | * |
120 | * @return Status A good status contains the url to redirect to |
121 | */ |
122 | public function dispatchFile() { |
123 | try { |
124 | $title = Title::newFromTextThrow( $this->mValue, NS_FILE ); |
125 | if ( $title && !$title->inNamespace( NS_FILE ) ) { |
126 | // If the given value contains a namespace enforce file namespace |
127 | $title = Title::newFromTextThrow( Title::makeName( NS_FILE, $this->mValue ) ); |
128 | } |
129 | } catch ( MalformedTitleException $e ) { |
130 | return Status::newFatal( $e->getMessageObject() ); |
131 | } |
132 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive |
133 | $file = $this->repoGroup->findFile( $title ); |
134 | |
135 | if ( !$file || !$file->exists() ) { |
136 | return Status::newFatal( 'redirect-not-exists' ); |
137 | } |
138 | // Default behavior: Use the direct link to the file. |
139 | $url = $file->getUrl(); |
140 | $request = $this->getRequest(); |
141 | $width = $request->getInt( 'width', -1 ); |
142 | $height = $request->getInt( 'height', -1 ); |
143 | |
144 | // If a width is requested... |
145 | if ( $width != -1 ) { |
146 | $mto = $file->transform( [ 'width' => $width, 'height' => $height ] ); |
147 | // ... and we can |
148 | if ( $mto && !$mto->isError() ) { |
149 | // ... change the URL to point to a thumbnail. |
150 | // Note: This url is more temporary as can change |
151 | // if file is reuploaded and has different aspect ratio. |
152 | $url = [ $mto->getUrl(), $height === -1 ? 301 : 302 ]; |
153 | } |
154 | } |
155 | |
156 | return Status::newGood( $url ); |
157 | } |
158 | |
159 | /** |
160 | * Handle Special:Redirect/revision/xxx |
161 | * (by redirecting to index.php?oldid=xxx) |
162 | * |
163 | * @return Status A good status contains the url to redirect to |
164 | */ |
165 | public function dispatchRevision() { |
166 | $oldid = $this->mValue; |
167 | if ( !ctype_digit( $oldid ) ) { |
168 | return Status::newFatal( 'redirect-not-numeric' ); |
169 | } |
170 | $oldid = (int)$oldid; |
171 | if ( $oldid === 0 ) { |
172 | return Status::newFatal( 'redirect-not-exists' ); |
173 | } |
174 | |
175 | return Status::newGood( wfAppendQuery( wfScript( 'index' ), [ |
176 | 'oldid' => $oldid |
177 | ] ) ); |
178 | } |
179 | |
180 | /** |
181 | * Handle Special:Redirect/page/xxx (by redirecting to index.php?curid=xxx) |
182 | * |
183 | * @return Status A good status contains the url to redirect to |
184 | */ |
185 | public function dispatchPage() { |
186 | $curid = $this->mValue; |
187 | if ( !ctype_digit( $curid ) ) { |
188 | return Status::newFatal( 'redirect-not-numeric' ); |
189 | } |
190 | $curid = (int)$curid; |
191 | if ( $curid === 0 ) { |
192 | return Status::newFatal( 'redirect-not-exists' ); |
193 | } |
194 | |
195 | return Status::newGood( wfAppendQuery( wfScript( 'index' ), [ |
196 | 'curid' => $curid |
197 | ] ) ); |
198 | } |
199 | |
200 | /** |
201 | * Handle Special:Redirect/logid/xxx |
202 | * (by redirecting to index.php?title=Special:Log&logid=xxx) |
203 | * |
204 | * @since 1.27 |
205 | * @return Status A good status contains the url to redirect to |
206 | */ |
207 | public function dispatchLog() { |
208 | $logid = $this->mValue; |
209 | if ( !ctype_digit( $logid ) ) { |
210 | return Status::newFatal( 'redirect-not-numeric' ); |
211 | } |
212 | $logid = (int)$logid; |
213 | if ( $logid === 0 ) { |
214 | return Status::newFatal( 'redirect-not-exists' ); |
215 | } |
216 | $query = [ 'title' => 'Special:Log', 'logid' => $logid ]; |
217 | return Status::newGood( wfAppendQuery( wfScript( 'index' ), $query ) ); |
218 | } |
219 | |
220 | /** |
221 | * Use appropriate dispatch* method to obtain a redirection URL, |
222 | * and either: redirect, set a 404 error code and error message, |
223 | * or do nothing (if $mValue wasn't set) allowing the form to be |
224 | * displayed. |
225 | * |
226 | * @return Status|bool True if a redirect was successfully handled. |
227 | */ |
228 | private function dispatch() { |
229 | // the various namespaces supported by Special:Redirect |
230 | switch ( $this->mType ) { |
231 | case 'user': |
232 | $status = $this->dispatchUser(); |
233 | break; |
234 | case 'file': |
235 | $status = $this->dispatchFile(); |
236 | break; |
237 | case 'revision': |
238 | $status = $this->dispatchRevision(); |
239 | break; |
240 | case 'page': |
241 | $status = $this->dispatchPage(); |
242 | break; |
243 | case 'logid': |
244 | $status = $this->dispatchLog(); |
245 | break; |
246 | default: |
247 | $status = null; |
248 | break; |
249 | } |
250 | if ( $status && $status->isGood() ) { |
251 | // These urls can sometimes be linked from prominent places, |
252 | // so varnish cache. |
253 | $value = $status->getValue(); |
254 | if ( is_array( $value ) ) { |
255 | [ $url, $code ] = $value; |
256 | } else { |
257 | $url = $value; |
258 | $code = 301; |
259 | } |
260 | if ( $code === 301 ) { |
261 | $this->getOutput()->setCdnMaxage( 60 * 60 ); |
262 | } else { |
263 | $this->getOutput()->setCdnMaxage( 10 ); |
264 | } |
265 | $this->getOutput()->redirect( $url, $code ); |
266 | |
267 | return true; |
268 | } |
269 | if ( $this->mValue !== null ) { |
270 | $this->getOutput()->setStatusCode( 404 ); |
271 | |
272 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable Null of $status seems unreachable |
273 | return $status; |
274 | } |
275 | |
276 | return false; |
277 | } |
278 | |
279 | protected function getFormFields() { |
280 | return [ |
281 | 'type' => [ |
282 | 'type' => 'select', |
283 | 'label-message' => 'redirect-lookup', |
284 | 'options-messages' => [ |
285 | 'redirect-user' => 'user', |
286 | 'redirect-page' => 'page', |
287 | 'redirect-revision' => 'revision', |
288 | 'redirect-file' => 'file', |
289 | 'redirect-logid' => 'logid', |
290 | ], |
291 | 'default' => $this->mType, |
292 | ], |
293 | 'value' => [ |
294 | 'type' => 'text', |
295 | 'label-message' => 'redirect-value', |
296 | 'default' => $this->mValue, |
297 | 'required' => true, |
298 | ], |
299 | ]; |
300 | } |
301 | |
302 | public function onSubmit( array $data ) { |
303 | if ( !empty( $data['type'] ) && !empty( $data['value'] ) ) { |
304 | $this->setParameter( $data['type'] . '/' . $data['value'] ); |
305 | } |
306 | |
307 | /* if this returns false, will show the form */ |
308 | return $this->dispatch(); |
309 | } |
310 | |
311 | public function onSuccess() { |
312 | /* do nothing, we redirect in $this->dispatch if successful. */ |
313 | } |
314 | |
315 | protected function alterForm( HTMLForm $form ) { |
316 | // tweak label on submit button |
317 | $form->setSubmitTextMsg( 'redirect-submit' ); |
318 | } |
319 | |
320 | protected function getDisplayFormat() { |
321 | return 'ooui'; |
322 | } |
323 | |
324 | /** |
325 | * Return an array of subpages that this special page will accept. |
326 | * |
327 | * @return string[] subpages |
328 | */ |
329 | protected function getSubpagesForPrefixSearch() { |
330 | return [ |
331 | 'file', |
332 | 'page', |
333 | 'revision', |
334 | 'user', |
335 | 'logid', |
336 | ]; |
337 | } |
338 | |
339 | /** |
340 | * @return bool |
341 | */ |
342 | public function requiresPost() { |
343 | return false; |
344 | } |
345 | |
346 | protected function getGroupName() { |
347 | return 'redirects'; |
348 | } |
349 | } |
350 | |
351 | /** |
352 | * Retain the old class name for backwards compatibility. |
353 | * @deprecated since 1.41 |
354 | */ |
355 | class_alias( SpecialRedirect::class, 'SpecialRedirect' ); |