Completed
Branch master (04580f)
by
unknown
27:14
created

SpecialRedirect::getDisplayFormat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 3
rs 10
c 1
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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
24
/**
25
 * A special page that redirects to: the user for a numeric user id,
26
 * the file for a given filename, or the page for a given revision id.
27
 *
28
 * @ingroup SpecialPage
29
 * @since 1.22
30
 */
31
class SpecialRedirect extends FormSpecialPage {
32
33
	/**
34
	 * The type of the redirect (user/file/revision)
35
	 *
36
	 * @var string $mType
37
	 * @example 'user'
38
	 */
39
	protected $mType;
40
41
	/**
42
	 * The identifier/value for the redirect (which id, which file)
43
	 *
44
	 * @var string $mValue
45
	 * @example '42'
46
	 */
47
	protected $mValue;
48
49
	function __construct() {
50
		parent::__construct( 'Redirect' );
51
		$this->mType = null;
52
		$this->mValue = null;
53
	}
54
55
	/**
56
	 * Set $mType and $mValue based on parsed value of $subpage.
57
	 * @param string $subpage
58
	 */
59
	function setParameter( $subpage ) {
60
		// parse $subpage to pull out the parts
61
		$parts = explode( '/', $subpage, 2 );
62
		$this->mType = count( $parts ) > 0 ? $parts[0] : null;
63
		$this->mValue = count( $parts ) > 1 ? $parts[1] : null;
64
	}
65
66
	/**
67
	 * Handle Special:Redirect/user/xxxx (by redirecting to User:YYYY)
68
	 *
69
	 * @return string|null Url to redirect to, or null if $mValue is invalid.
70
	 */
71
	function dispatchUser() {
72
		if ( !ctype_digit( $this->mValue ) ) {
73
			return null;
74
		}
75
		$user = User::newFromId( (int)$this->mValue );
76
		$username = $user->getName(); // load User as side-effect
77
		if ( $user->isAnon() ) {
78
			return null;
79
		}
80
		$userpage = Title::makeTitle( NS_USER, $username );
81
82
		return $userpage->getFullURL( '', false, PROTO_CURRENT );
83
	}
84
85
	/**
86
	 * Handle Special:Redirect/file/xxxx
87
	 *
88
	 * @return string|null Url to redirect to, or null if $mValue is not found.
89
	 */
90
	function dispatchFile() {
91
		$title = Title::makeTitleSafe( NS_FILE, $this->mValue );
92
93
		if ( !$title instanceof Title ) {
94
			return null;
95
		}
96
		$file = wfFindFile( $title );
97
98
		if ( !$file || !$file->exists() ) {
99
			return null;
100
		}
101
		// Default behavior: Use the direct link to the file.
102
		$url = $file->getUrl();
103
		$request = $this->getRequest();
104
		$width = $request->getInt( 'width', -1 );
105
		$height = $request->getInt( 'height', -1 );
106
107
		// If a width is requested...
108
		if ( $width != -1 ) {
109
			$mto = $file->transform( [ 'width' => $width, 'height' => $height ] );
110
			// ... and we can
111
			if ( $mto && !$mto->isError() ) {
112
				// ... change the URL to point to a thumbnail.
113
				$url = $mto->getUrl();
114
			}
115
		}
116
117
		return $url;
118
	}
119
120
	/**
121
	 * Handle Special:Redirect/revision/xxx
122
	 * (by redirecting to index.php?oldid=xxx)
123
	 *
124
	 * @return string|null Url to redirect to, or null if $mValue is invalid.
125
	 */
126 View Code Duplication
	function dispatchRevision() {
127
		$oldid = $this->mValue;
128
		if ( !ctype_digit( $oldid ) ) {
129
			return null;
130
		}
131
		$oldid = (int)$oldid;
132
		if ( $oldid === 0 ) {
133
			return null;
134
		}
135
136
		return wfAppendQuery( wfScript( 'index' ), [
137
			'oldid' => $oldid
138
		] );
139
	}
140
141
	/**
142
	 * Handle Special:Redirect/page/xxx (by redirecting to index.php?curid=xxx)
143
	 *
144
	 * @return string|null Url to redirect to, or null if $mValue is invalid.
145
	 */
146 View Code Duplication
	function dispatchPage() {
147
		$curid = $this->mValue;
148
		if ( !ctype_digit( $curid ) ) {
149
			return null;
150
		}
151
		$curid = (int)$curid;
152
		if ( $curid === 0 ) {
153
			return null;
154
		}
155
156
		return wfAppendQuery( wfScript( 'index' ), [
157
			'curid' => $curid
158
		] );
159
	}
160
161
	/**
162
	 * Handle Special:Redirect/logid/xxx
163
	 * (by redirecting to index.php?title=Special:Log)
164
	 *
165
	 * @since 1.27
166
	 * @return string|null Url to redirect to, or null if $mValue is invalid.
167
	 */
168
	function dispatchLog() {
169
		$logid = $this->mValue;
170
		if ( !ctype_digit( $logid ) ) {
171
			return null;
172
		}
173
		$logid = (int)$logid;
174
		if ( $logid === 0 ) {
175
			return null;
176
		}
177
178
		$logparams = [
179
			'log_id',
180
			'log_timestamp',
181
			'log_type',
182
			'log_user_text',
183
		];
184
185
		$dbr = wfGetDB( DB_SLAVE );
186
187
		// Gets the nested SQL statement which
188
		// returns timestamp of the log with the given log ID
189
		$inner = $dbr->selectSQLText(
190
			'logging',
191
			[ 'log_timestamp' ],
192
			[ 'log_id' => $logid ]
193
		);
194
195
		// Returns all fields mentioned in $logparams of the logs
196
		// with the same timestamp as the one returned by the statement above
197
		$logsSameTimestamps = $dbr->select(
198
			'logging',
199
			$logparams,
200
			[ "log_timestamp = ($inner)" ]
201
		);
202
		if ( $logsSameTimestamps->numRows() === 0 ) {
203
			return null;
204
		}
205
206
		// Stores the row with the same log ID as the one given
207
		$rowMain = [];
208
		foreach ( $logsSameTimestamps as $row ) {
0 ignored issues
show
Bug introduced by
The expression $logsSameTimestamps of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
209
			if ( (int)$row->log_id === $logid ) {
210
				$rowMain = $row;
211
			}
212
		}
213
214
		array_shift( $logparams );
215
216
		// Stores all the rows with the same values in each column
217
		// as $rowMain
218
		foreach ( $logparams as $cond ) {
219
			$matchedRows = [];
220
			foreach ( $logsSameTimestamps as $row ) {
0 ignored issues
show
Bug introduced by
The expression $logsSameTimestamps of type boolean|object<ResultWrapper>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
221
				if ( $row->$cond === $rowMain->$cond ) {
222
					$matchedRows[] = $row;
223
				}
224
			}
225
			if ( count( $matchedRows ) === 1 ) {
226
				break;
227
			}
228
			$logsSameTimestamps = $matchedRows;
229
		}
230
		$query = [ 'title' => 'Special:Log', 'limit' => count( $matchedRows ) ];
0 ignored issues
show
Bug introduced by
The variable $matchedRows does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
231
232
		// A map of database field names from table 'logging' to the values of $logparams
233
		$keys = [
234
			'log_timestamp' => 'offset',
235
			'log_type' => 'type',
236
			'log_user_text' => 'user'
237
		];
238
239
		foreach ( $logparams as $logKey ) {
240
			$query[$keys[$logKey]] = $matchedRows[0]->$logKey;
241
		}
242
		$query['offset'] = $query['offset'] + 1;
243
		$url = $query;
244
245
		return wfAppendQuery( wfScript( 'index' ), $url );
246
	}
247
248
	/**
249
	 * Use appropriate dispatch* method to obtain a redirection URL,
250
	 * and either: redirect, set a 404 error code and error message,
251
	 * or do nothing (if $mValue wasn't set) allowing the form to be
252
	 * displayed.
253
	 *
254
	 * @return bool True if a redirect was successfully handled.
255
	 */
256
	function dispatch() {
257
		// the various namespaces supported by Special:Redirect
258
		switch ( $this->mType ) {
259
			case 'user':
260
				$url = $this->dispatchUser();
261
				break;
262
			case 'file':
263
				$url = $this->dispatchFile();
264
				break;
265
			case 'revision':
266
				$url = $this->dispatchRevision();
267
				break;
268
			case 'page':
269
				$url = $this->dispatchPage();
270
				break;
271
			case 'logid':
272
				$url = $this->dispatchLog();
273
				break;
274
			default:
275
				$url = null;
276
				break;
277
		}
278
		if ( $url ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
279
			$this->getOutput()->redirect( $url );
280
281
			return true;
282
		}
283
		if ( !is_null( $this->mValue ) ) {
284
			$this->getOutput()->setStatusCode( 404 );
285
			// Message: redirect-not-exists
286
			$msg = $this->getMessagePrefix() . '-not-exists';
287
288
			return Status::newFatal( $msg );
289
		}
290
291
		return false;
292
	}
293
294
	protected function getFormFields() {
295
		$mp = $this->getMessagePrefix();
296
		$ns = [
297
			// subpage => message
298
			// Messages: redirect-user, redirect-page, redirect-revision,
299
			// redirect-file, redirect-logid
300
			'user' => $mp . '-user',
301
			'page' => $mp . '-page',
302
			'revision' => $mp . '-revision',
303
			'file' => $mp . '-file',
304
			'logid' => $mp . '-logid',
305
		];
306
		$a = [];
307
		$a['type'] = [
308
			'type' => 'select',
309
			'label-message' => $mp . '-lookup', // Message: redirect-lookup
310
			'options' => [],
311
			'default' => current( array_keys( $ns ) ),
312
		];
313
		foreach ( $ns as $n => $m ) {
314
			$m = $this->msg( $m )->text();
315
			$a['type']['options'][$m] = $n;
316
		}
317
		$a['value'] = [
318
			'type' => 'text',
319
			'label-message' => $mp . '-value' // Message: redirect-value
320
		];
321
		// set the defaults according to the parsed subpage path
322
		if ( !empty( $this->mType ) ) {
323
			$a['type']['default'] = $this->mType;
324
		}
325
		if ( !empty( $this->mValue ) ) {
326
			$a['value']['default'] = $this->mValue;
327
		}
328
329
		return $a;
330
	}
331
332
	public function onSubmit( array $data ) {
333
		if ( !empty( $data['type'] ) && !empty( $data['value'] ) ) {
334
			$this->setParameter( $data['type'] . '/' . $data['value'] );
335
		}
336
337
		/* if this returns false, will show the form */
338
		return $this->dispatch();
339
	}
340
341
	public function onSuccess() {
342
		/* do nothing, we redirect in $this->dispatch if successful. */
343
	}
344
345
	protected function alterForm( HTMLForm $form ) {
346
		/* display summary at top of page */
347
		$this->outputHeader();
348
		// tweak label on submit button
349
		// Message: redirect-submit
350
		$form->setSubmitTextMsg( $this->getMessagePrefix() . '-submit' );
351
		/* submit form every time */
352
		$form->setMethod( 'get' );
353
	}
354
355
	protected function getDisplayFormat() {
356
		return 'ooui';
357
	}
358
359
	/**
360
	 * Return an array of subpages that this special page will accept.
361
	 *
362
	 * @return string[] subpages
363
	 */
364
	protected function getSubpagesForPrefixSearch() {
365
		return [
366
			'file',
367
			'page',
368
			'revision',
369
			'user',
370
			'logid',
371
		];
372
	}
373
374
	/**
375
	 * @return bool
376
	 */
377
	public function requiresWrite() {
378
		return false;
379
	}
380
381
	/**
382
	 * @return bool
383
	 */
384
	public function requiresUnblock() {
385
		return false;
386
	}
387
388
	protected function getGroupName() {
389
		return 'redirects';
390
	}
391
}
392