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 * 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
21namespace MediaWiki\Specials;
22
23use MediaWiki\HTMLForm\HTMLForm;
24use MediaWiki\SpecialPage\FormSpecialPage;
25use MediaWiki\Status\Status;
26use MediaWiki\Title\MalformedTitleException;
27use MediaWiki\Title\Title;
28use MediaWiki\User\UserFactory;
29use PermissionsError;
30use 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 */
44class 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 */
355class_alias( SpecialRedirect::class, 'SpecialRedirect' );