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