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
24use MediaWiki\User\UserFactory;
25
26/**
27 * A special page that redirects to: the user for a numeric user id,
28 * the file for a given filename, or the page for a given revision id.
29 *
30 * @ingroup SpecialPage
31 * @since 1.22
32 */
33class SpecialRedirect extends FormSpecialPage {
34
35	/**
36	 * The type of the redirect (user/file/revision)
37	 *
38	 * Example value: `'user'`
39	 *
40	 * @var string
41	 */
42	protected $mType;
43
44	/**
45	 * The identifier/value for the redirect (which id, which file)
46	 *
47	 * Example value: `'42'`
48	 *
49	 * @var string
50	 */
51	protected $mValue;
52
53	/** @var RepoGroup */
54	private $repoGroup;
55
56	/** @var UserFactory */
57	private $userFactory;
58
59	/**
60	 * @param RepoGroup $repoGroup
61	 * @param UserFactory $userFactory
62	 */
63	public function __construct(
64		RepoGroup $repoGroup,
65		UserFactory $userFactory
66	) {
67		parent::__construct( 'Redirect' );
68		$this->mType = null;
69		$this->mValue = null;
70
71		$this->repoGroup = $repoGroup;
72		$this->userFactory = $userFactory;
73	}
74
75	/**
76	 * Set $mType and $mValue based on parsed value of $subpage.
77	 * @param string $subpage
78	 */
79	public function setParameter( $subpage ) {
80		// parse $subpage to pull out the parts
81		$parts = explode( '/', $subpage, 2 );
82		$this->mType = $parts[0];
83		$this->mValue = $parts[1] ?? null;
84	}
85
86	/**
87	 * Handle Special:Redirect/user/xxxx (by redirecting to User:YYYY)
88	 *
89	 * @return Status A good status contains the url to redirect to
90	 */
91	public function dispatchUser() {
92		if ( !ctype_digit( $this->mValue ) ) {
93			// Message: redirect-not-numeric
94			return Status::newFatal( $this->getMessagePrefix() . '-not-numeric' );
95		}
96		$user = $this->userFactory->newFromId( (int)$this->mValue );
97		$user->load(); // Make sure the id is validated by loading the user
98		if ( $user->isAnon() ) {
99			// Message: redirect-not-exists
100			return Status::newFatal( $this->getMessagePrefix() . '-not-exists' );
101		}
102		if ( $user->isHidden() && !$this->getAuthority()->isAllowed( 'hideuser' ) ) {
103			throw new PermissionsError( null, [ 'badaccess-group0' ] );
104		}
105
106		return Status::newGood( [
107			$user->getUserPage()->getFullURL( '', false, PROTO_CURRENT ), 302
108		] );
109	}
110
111	/**
112	 * Handle Special:Redirect/file/xxxx
113	 *
114	 * @return Status A good status contains the url to redirect to
115	 */
116	public function dispatchFile() {
117		try {
118			$title = Title::newFromTextThrow( $this->mValue, NS_FILE );
119			if ( $title && !$title->inNamespace( NS_FILE ) ) {
120				// If the given value contains a namespace enforce file namespace
121				$title = Title::newFromTextThrow( Title::makeName( NS_FILE, $this->mValue ) );
122			}
123		} catch ( MalformedTitleException $e ) {
124			return Status::newFatal( $e->getMessageObject() );
125		}
126		$file = $this->repoGroup->findFile( $title );
127
128		if ( !$file || !$file->exists() ) {
129			// Message: redirect-not-exists
130			return Status::newFatal( $this->getMessagePrefix() . '-not-exists' );
131		}
132		// Default behavior: Use the direct link to the file.
133		$url = $file->getUrl();
134		$request = $this->getRequest();
135		$width = $request->getInt( 'width', -1 );
136		$height = $request->getInt( 'height', -1 );
137
138		// If a width is requested...
139		if ( $width != -1 ) {
140			$mto = $file->transform( [ 'width' => $width, 'height' => $height ] );
141			// ... and we can
142			if ( $mto && !$mto->isError() ) {
143				// ... change the URL to point to a thumbnail.
144				// Note: This url is more temporary as can change
145				// if file is reuploaded and has different aspect ratio.
146				$url = [ $mto->getUrl(), $height === -1 ? 301 : 302 ];
147			}
148		}
149
150		return Status::newGood( $url );
151	}
152
153	/**
154	 * Handle Special:Redirect/revision/xxx
155	 * (by redirecting to index.php?oldid=xxx)
156	 *
157	 * @return Status A good status contains the url to redirect to
158	 */
159	public function dispatchRevision() {
160		$oldid = $this->mValue;
161		if ( !ctype_digit( $oldid ) ) {
162			// Message: redirect-not-numeric
163			return Status::newFatal( $this->getMessagePrefix() . '-not-numeric' );
164		}
165		$oldid = (int)$oldid;
166		if ( $oldid === 0 ) {
167			// Message: redirect-not-exists
168			return Status::newFatal( $this->getMessagePrefix() . '-not-exists' );
169		}
170
171		return Status::newGood( wfAppendQuery( wfScript( 'index' ), [
172			'oldid' => $oldid
173		] ) );
174	}
175
176	/**
177	 * Handle Special:Redirect/page/xxx (by redirecting to index.php?curid=xxx)
178	 *
179	 * @return Status A good status contains the url to redirect to
180	 */
181	public function dispatchPage() {
182		$curid = $this->mValue;
183		if ( !ctype_digit( $curid ) ) {
184			// Message: redirect-not-numeric
185			return Status::newFatal( $this->getMessagePrefix() . '-not-numeric' );
186		}
187		$curid = (int)$curid;
188		if ( $curid === 0 ) {
189			// Message: redirect-not-exists
190			return Status::newFatal( $this->getMessagePrefix() . '-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			// Message: redirect-not-numeric
209			return Status::newFatal( $this->getMessagePrefix() . '-not-numeric' );
210		}
211		$logid = (int)$logid;
212		if ( $logid === 0 ) {
213			// Message: redirect-not-exists
214			return Status::newFatal( $this->getMessagePrefix() . '-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				list( $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			return $status;
273		}
274
275		return false;
276	}
277
278	protected function getFormFields() {
279		$mp = $this->getMessagePrefix();
280		$ns = [
281			// subpage => message
282			// Messages: redirect-user, redirect-page, redirect-revision,
283			// redirect-file, redirect-logid
284			'user' => $mp . '-user',
285			'page' => $mp . '-page',
286			'revision' => $mp . '-revision',
287			'file' => $mp . '-file',
288			'logid' => $mp . '-logid',
289		];
290		$a = [];
291		$a['type'] = [
292			'type' => 'select',
293			'label-message' => $mp . '-lookup', // Message: redirect-lookup
294			'options' => [],
295			'default' => current( array_keys( $ns ) ),
296		];
297		foreach ( $ns as $n => $m ) {
298			$m = $this->msg( $m )->text();
299			$a['type']['options'][$m] = $n;
300		}
301		$a['value'] = [
302			'type' => 'text',
303			'label-message' => $mp . '-value' // Message: redirect-value
304		];
305		// set the defaults according to the parsed subpage path
306		if ( !empty( $this->mType ) ) {
307			$a['type']['default'] = $this->mType;
308		}
309		if ( !empty( $this->mValue ) ) {
310			$a['value']['default'] = $this->mValue;
311		}
312
313		return $a;
314	}
315
316	public function onSubmit( array $data ) {
317		if ( !empty( $data['type'] ) && !empty( $data['value'] ) ) {
318			$this->setParameter( $data['type'] . '/' . $data['value'] );
319		}
320
321		/* if this returns false, will show the form */
322		return $this->dispatch();
323	}
324
325	public function onSuccess() {
326		/* do nothing, we redirect in $this->dispatch if successful. */
327	}
328
329	protected function alterForm( HTMLForm $form ) {
330		/* display summary at top of page */
331		$this->outputHeader();
332		// tweak label on submit button
333		// Message: redirect-submit
334		$form->setSubmitTextMsg( $this->getMessagePrefix() . '-submit' );
335		/* submit form every time */
336		$form->setMethod( 'get' );
337	}
338
339	protected function getDisplayFormat() {
340		return 'ooui';
341	}
342
343	/**
344	 * Return an array of subpages that this special page will accept.
345	 *
346	 * @return string[] subpages
347	 */
348	protected function getSubpagesForPrefixSearch() {
349		return [
350			'file',
351			'page',
352			'revision',
353			'user',
354			'logid',
355		];
356	}
357
358	/**
359	 * @return bool
360	 */
361	public function requiresWrite() {
362		return false;
363	}
364
365	/**
366	 * @return bool
367	 */
368	public function requiresUnblock() {
369		return false;
370	}
371
372	protected function getGroupName() {
373		return 'redirects';
374	}
375}
376