Completed
Branch master (d58858)
by
unknown
28:23
created

LogPager::getYear()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * Contain classes to list log entries
4
 *
5
 * Copyright © 2004 Brion Vibber <[email protected]>, 2008 Aaron Schulz
6
 * https://www.mediawiki.org/
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License along
19
 * with this program; if not, write to the Free Software Foundation, Inc.,
20
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 * http://www.gnu.org/copyleft/gpl.html
22
 *
23
 * @file
24
 */
25
26
/**
27
 * @ingroup Pager
28
 */
29
class LogPager extends ReverseChronologicalPager {
30
	/** @var array Log types */
31
	private $types = [];
32
33
	/** @var string Events limited to those by performer when set */
34
	private $performer = '';
35
36
	/** @var string|Title Events limited to those about Title when set */
37
	private $title = '';
38
39
	/** @var string */
40
	private $pattern = '';
41
42
	/** @var string */
43
	private $typeCGI = '';
44
45
	/** @var string */
46
	private $action = '';
47
48
	/** @var LogEventsList */
49
	public $mLogEventsList;
50
51
	/**
52
	 * Constructor
53
	 *
54
	 * @param LogEventsList $list
55
	 * @param string|array $types Log types to show
56
	 * @param string $performer The user who made the log entries
57
	 * @param string|Title $title The page title the log entries are for
58
	 * @param string $pattern Do a prefix search rather than an exact title match
59
	 * @param array $conds Extra conditions for the query
60
	 * @param int|bool $year The year to start from. Default: false
61
	 * @param int|bool $month The month to start from. Default: false
62
	 * @param string $tagFilter Tag
63
	 * @param string $action Specific action (subtype) requested
64
	 */
65
	public function __construct( $list, $types = [], $performer = '', $title = '',
66
		$pattern = '', $conds = [], $year = false, $month = false, $tagFilter = '',
67
		$action = ''
68
	) {
69
		parent::__construct( $list->getContext() );
70
		$this->mConds = $conds;
0 ignored issues
show
Bug introduced by
The property mConds does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
71
72
		$this->mLogEventsList = $list;
73
74
		$this->limitType( $types ); // also excludes hidden types
75
		$this->limitPerformer( $performer );
76
		$this->limitTitle( $title, $pattern );
77
		$this->limitAction( $action );
78
		$this->getDateCond( $year, $month );
79
		$this->mTagFilter = $tagFilter;
0 ignored issues
show
Bug introduced by
The property mTagFilter does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
80
81
		$this->mDb = wfGetDB( DB_SLAVE, 'logpager' );
82
	}
83
84
	public function getDefaultQuery() {
85
		$query = parent::getDefaultQuery();
86
		$query['type'] = $this->typeCGI; // arrays won't work here
87
		$query['user'] = $this->performer;
88
		$query['month'] = $this->mMonth;
89
		$query['year'] = $this->mYear;
90
91
		return $query;
92
	}
93
94
	// Call ONLY after calling $this->limitType() already!
95
	public function getFilterParams() {
96
		global $wgFilterLogTypes;
97
		$filters = [];
98
		if ( count( $this->types ) ) {
99
			return $filters;
100
		}
101
		foreach ( $wgFilterLogTypes as $type => $default ) {
102
			// Avoid silly filtering
103
			if ( $type !== 'patrol' || $this->getUser()->useNPPatrol() ) {
104
				$hide = $this->getRequest()->getInt( "hide_{$type}_log", $default );
105
				$filters[$type] = $hide;
106
				if ( $hide ) {
107
					$this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type );
108
				}
109
			}
110
		}
111
112
		return $filters;
113
	}
114
115
	/**
116
	 * Set the log reader to return only entries of the given type.
117
	 * Type restrictions enforced here
118
	 *
119
	 * @param string|array $types Log types ('upload', 'delete', etc);
120
	 *   empty string means no restriction
121
	 */
122
	private function limitType( $types ) {
123
		global $wgLogRestrictions;
124
125
		$user = $this->getUser();
126
		// If $types is not an array, make it an array
127
		$types = ( $types === '' ) ? [] : (array)$types;
128
		// Don't even show header for private logs; don't recognize it...
129
		$needReindex = false;
130
		foreach ( $types as $type ) {
131
			if ( isset( $wgLogRestrictions[$type] )
132
				&& !$user->isAllowed( $wgLogRestrictions[$type] )
133
			) {
134
				$needReindex = true;
135
				$types = array_diff( $types, [ $type ] );
136
			}
137
		}
138
		if ( $needReindex ) {
139
			// Lots of this code makes assumptions that
140
			// the first entry in the array is $types[0].
141
			$types = array_values( $types );
142
		}
143
		$this->types = $types;
144
		// Don't show private logs to unprivileged users.
145
		// Also, only show them upon specific request to avoid suprises.
146
		$audience = $types ? 'user' : 'public';
147
		$hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience, $user );
148
		if ( $hideLogs !== false ) {
149
			$this->mConds[] = $hideLogs;
150
		}
151
		if ( count( $types ) ) {
152
			$this->mConds['log_type'] = $types;
153
			// Set typeCGI; used in url param for paging
154
			if ( count( $types ) == 1 ) {
155
				$this->typeCGI = $types[0];
156
			}
157
		}
158
	}
159
160
	/**
161
	 * Set the log reader to return only entries by the given user.
162
	 *
163
	 * @param string $name (In)valid user name
164
	 * @return void
165
	 */
166
	private function limitPerformer( $name ) {
167
		if ( $name == '' ) {
168
			return;
169
		}
170
		$usertitle = Title::makeTitleSafe( NS_USER, $name );
171
		if ( is_null( $usertitle ) ) {
172
			return;
173
		}
174
		/* Fetch userid at first, if known, provides awesome query plan afterwards */
175
		$userid = User::idFromName( $name );
176
		if ( !$userid ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userid of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
177
			$this->mConds['log_user_text'] = IP::sanitizeIP( $name );
178
		} else {
179
			$this->mConds['log_user'] = $userid;
180
		}
181
		// Paranoia: avoid brute force searches (bug 17342)
182
		$user = $this->getUser();
183
		if ( !$user->isAllowed( 'deletedhistory' ) ) {
184
			$this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
185
		} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
186
			$this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
187
				' != ' . LogPage::SUPPRESSED_USER;
188
		}
189
190
		$this->performer = $usertitle->getText();
191
	}
192
193
	/**
194
	 * Set the log reader to return only entries affecting the given page.
195
	 * (For the block and rights logs, this is a user page.)
196
	 *
197
	 * @param string|Title $page Title name
198
	 * @param string $pattern
199
	 * @return void
200
	 */
201
	private function limitTitle( $page, $pattern ) {
202
		global $wgMiserMode, $wgUserrightsInterwikiDelimiter;
203
204
		if ( $page instanceof Title ) {
205
			$title = $page;
206
		} else {
207
			$title = Title::newFromText( $page );
208
			if ( strlen( $page ) == 0 || !$title instanceof Title ) {
209
				return;
210
			}
211
		}
212
213
		$this->title = $title->getPrefixedText();
214
		$ns = $title->getNamespace();
215
		$db = $this->mDb;
216
217
		$doUserRightsLogLike = false;
218
		if ( $this->types == [ 'rights' ] ) {
219
			$parts = explode( $wgUserrightsInterwikiDelimiter, $title->getDBkey() );
220
			if ( count( $parts ) == 2 ) {
221
				list( $name, $database ) = array_map( 'trim', $parts );
222
				if ( strstr( $database, '*' ) ) { // Search for wildcard in database name
223
					$doUserRightsLogLike = true;
224
				}
225
			}
226
		}
227
228
		/**
229
		 * Using the (log_namespace, log_title, log_timestamp) index with a
230
		 * range scan (LIKE) on the first two parts, instead of simple equality,
231
		 * makes it unusable for sorting.  Sorted retrieval using another index
232
		 * would be possible, but then we might have to scan arbitrarily many
233
		 * nodes of that index. Therefore, we need to avoid this if $wgMiserMode
234
		 * is on.
235
		 *
236
		 * This is not a problem with simple title matches, because then we can
237
		 * use the page_time index.  That should have no more than a few hundred
238
		 * log entries for even the busiest pages, so it can be safely scanned
239
		 * in full to satisfy an impossible condition on user or similar.
240
		 */
241
		$this->mConds['log_namespace'] = $ns;
242
		if ( $doUserRightsLogLike ) {
243
			$params = [ $name . $wgUserrightsInterwikiDelimiter ];
0 ignored issues
show
Bug introduced by
The variable $name 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...
244
			foreach ( explode( '*', $database ) as $databasepart ) {
0 ignored issues
show
Bug introduced by
The variable $database 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...
245
				$params[] = $databasepart;
246
				$params[] = $db->anyString();
247
			}
248
			array_pop( $params ); // Get rid of the last % we added.
249
			$this->mConds[] = 'log_title' . $db->buildLike( $params );
250
		} elseif ( $pattern && !$wgMiserMode ) {
251
			$this->mConds[] = 'log_title' . $db->buildLike( $title->getDBkey(), $db->anyString() );
252
			$this->pattern = $pattern;
253
		} else {
254
			$this->mConds['log_title'] = $title->getDBkey();
255
		}
256
		// Paranoia: avoid brute force searches (bug 17342)
257
		$user = $this->getUser();
258 View Code Duplication
		if ( !$user->isAllowed( 'deletedhistory' ) ) {
259
			$this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
260
		} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
261
			$this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
262
				' != ' . LogPage::SUPPRESSED_ACTION;
263
		}
264
	}
265
266
	/**
267
	 * Set the log_action field to a specified value (or values)
268
	 *
269
	 * @param string $action
270
	 */
271
	private function limitAction( $action ) {
272
		global $wgActionFilteredLogs;
273
		// Allow to filter the log by actions
274
		$type = $this->typeCGI;
275
		if ( $type === '' ) {
276
			// nothing to do
277
			return;
278
		}
279
		$actions = $wgActionFilteredLogs;
280
		if ( isset( $actions[$type] ) ) {
281
			// log type can be filtered by actions
282
			$this->mLogEventsList->setAllowedActions( array_keys( $actions[$type] ) );
283
			if ( $action !== '' && isset( $actions[$type][$action] ) ) {
284
				// add condition to query
285
				$this->mConds['log_action'] = $actions[$type][$action];
286
				$this->action = $action;
287
			}
288
		}
289
	}
290
291
	/**
292
	 * Constructs the most part of the query. Extra conditions are sprinkled in
293
	 * all over this class.
294
	 * @return array
295
	 */
296
	public function getQueryInfo() {
297
		$basic = DatabaseLogEntry::getSelectQueryData();
298
299
		$tables = $basic['tables'];
300
		$fields = $basic['fields'];
301
		$conds = $basic['conds'];
302
		$options = $basic['options'];
303
		$joins = $basic['join_conds'];
304
305
		$index = [];
306
		# Add log_search table if there are conditions on it.
307
		# This filters the results to only include log rows that have
308
		# log_search records with the specified ls_field and ls_value values.
309
		if ( array_key_exists( 'ls_field', $this->mConds ) ) {
310
			$tables[] = 'log_search';
311
			$index['log_search'] = 'ls_field_val';
312
			$index['logging'] = 'PRIMARY';
313
			if ( !$this->hasEqualsClause( 'ls_field' )
314
				|| !$this->hasEqualsClause( 'ls_value' )
315
			) {
316
				# Since (ls_field,ls_value,ls_logid) is unique, if the condition is
317
				# to match a specific (ls_field,ls_value) tuple, then there will be
318
				# no duplicate log rows. Otherwise, we need to remove the duplicates.
319
				$options[] = 'DISTINCT';
320
			}
321
		}
322
		if ( count( $index ) ) {
323
			$options['USE INDEX'] = $index;
324
		}
325
		# Don't show duplicate rows when using log_search
326
		$joins['log_search'] = [ 'INNER JOIN', 'ls_log_id=log_id' ];
327
328
		$info = [
329
			'tables' => $tables,
330
			'fields' => $fields,
331
			'conds' => array_merge( $conds, $this->mConds ),
332
			'options' => $options,
333
			'join_conds' => $joins,
334
		];
335
		# Add ChangeTags filter query
336
		ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'],
337
			$info['join_conds'], $info['options'], $this->mTagFilter );
338
339
		return $info;
340
	}
341
342
	/**
343
	 * Checks if $this->mConds has $field matched to a *single* value
344
	 * @param string $field
345
	 * @return bool
346
	 */
347
	protected function hasEqualsClause( $field ) {
348
		return (
349
			array_key_exists( $field, $this->mConds ) &&
350
			( !is_array( $this->mConds[$field] ) || count( $this->mConds[$field] ) == 1 )
351
		);
352
	}
353
354
	function getIndexField() {
355
		return 'log_timestamp';
356
	}
357
358
	public function getStartBody() {
359
		# Do a link batch query
360
		if ( $this->getNumRows() > 0 ) {
361
			$lb = new LinkBatch;
362
			foreach ( $this->mResult as $row ) {
363
				$lb->add( $row->log_namespace, $row->log_title );
364
				$lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
0 ignored issues
show
Bug introduced by
It seems like \Title::makeTitleSafe(NS_USER, $row->user_name) can be null; however, addObj() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
365
				$lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
0 ignored issues
show
Bug introduced by
It seems like \Title::makeTitleSafe(NS..._TALK, $row->user_name) can be null; however, addObj() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
366
				$formatter = LogFormatter::newFromRow( $row );
367
				foreach ( $formatter->getPreloadTitles() as $title ) {
368
					$lb->addObj( $title );
369
				}
370
			}
371
			$lb->execute();
372
			$this->mResult->seek( 0 );
373
		}
374
375
		return '';
376
	}
377
378
	public function formatRow( $row ) {
379
		return $this->mLogEventsList->logLine( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by parameter $row on line 378 can also be of type array; however, LogEventsList::logLine() does only seem to accept object<stdClass>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
380
	}
381
382
	public function getType() {
383
		return $this->types;
384
	}
385
386
	/**
387
	 * @return string
388
	 */
389
	public function getPerformer() {
390
		return $this->performer;
391
	}
392
393
	/**
394
	 * @return string
395
	 */
396
	public function getPage() {
397
		return $this->title;
398
	}
399
400
	public function getPattern() {
401
		return $this->pattern;
402
	}
403
404
	public function getYear() {
405
		return $this->mYear;
406
	}
407
408
	public function getMonth() {
409
		return $this->mMonth;
410
	}
411
412
	public function getTagFilter() {
413
		return $this->mTagFilter;
414
	}
415
416
	public function getAction() {
417
		return $this->action;
418
	}
419
420
	public function doQuery() {
421
		// Workaround MySQL optimizer bug
422
		$this->mDb->setBigSelects();
423
		parent::doQuery();
424
		$this->mDb->setBigSelects( 'default' );
425
	}
426
}
427