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