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