Completed
Branch master (939199)
by
unknown
39:35
created

includes/logging/LogPager.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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;
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;
80
81
		$this->mDb = wfGetDB( DB_REPLICA, '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
		// Normalize username first so that non-existent users used
175
		// in maintenance scripts work
176
		$name = $usertitle->getText();
177
		/* Fetch userid at first, if known, provides awesome query plan afterwards */
178
		$userid = User::idFromName( $name );
179
		if ( !$userid ) {
180
			$this->mConds['log_user_text'] = IP::sanitizeIP( $name );
181
		} else {
182
			$this->mConds['log_user'] = $userid;
183
		}
184
		// Paranoia: avoid brute force searches (bug 17342)
185
		$user = $this->getUser();
186
		if ( !$user->isAllowed( 'deletedhistory' ) ) {
187
			$this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
188
		} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
189
			$this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
190
				' != ' . LogPage::SUPPRESSED_USER;
191
		}
192
193
		$this->performer = $name;
194
	}
195
196
	/**
197
	 * Set the log reader to return only entries affecting the given page.
198
	 * (For the block and rights logs, this is a user page.)
199
	 *
200
	 * @param string|Title $page Title name
201
	 * @param string $pattern
202
	 * @return void
203
	 */
204
	private function limitTitle( $page, $pattern ) {
205
		global $wgMiserMode, $wgUserrightsInterwikiDelimiter;
206
207
		if ( $page instanceof Title ) {
208
			$title = $page;
209
		} else {
210
			$title = Title::newFromText( $page );
211
			if ( strlen( $page ) == 0 || !$title instanceof Title ) {
212
				return;
213
			}
214
		}
215
216
		$this->title = $title->getPrefixedText();
217
		$ns = $title->getNamespace();
218
		$db = $this->mDb;
219
220
		$doUserRightsLogLike = false;
221
		if ( $this->types == [ 'rights' ] ) {
222
			$parts = explode( $wgUserrightsInterwikiDelimiter, $title->getDBkey() );
223
			if ( count( $parts ) == 2 ) {
224
				list( $name, $database ) = array_map( 'trim', $parts );
225
				if ( strstr( $database, '*' ) ) { // Search for wildcard in database name
226
					$doUserRightsLogLike = true;
227
				}
228
			}
229
		}
230
231
		/**
232
		 * Using the (log_namespace, log_title, log_timestamp) index with a
233
		 * range scan (LIKE) on the first two parts, instead of simple equality,
234
		 * makes it unusable for sorting.  Sorted retrieval using another index
235
		 * would be possible, but then we might have to scan arbitrarily many
236
		 * nodes of that index. Therefore, we need to avoid this if $wgMiserMode
237
		 * is on.
238
		 *
239
		 * This is not a problem with simple title matches, because then we can
240
		 * use the page_time index.  That should have no more than a few hundred
241
		 * log entries for even the busiest pages, so it can be safely scanned
242
		 * in full to satisfy an impossible condition on user or similar.
243
		 */
244
		$this->mConds['log_namespace'] = $ns;
245
		if ( $doUserRightsLogLike ) {
246
			$params = [ $name . $wgUserrightsInterwikiDelimiter ];
0 ignored issues
show
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...
247
			foreach ( explode( '*', $database ) as $databasepart ) {
0 ignored issues
show
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...
248
				$params[] = $databasepart;
249
				$params[] = $db->anyString();
250
			}
251
			array_pop( $params ); // Get rid of the last % we added.
252
			$this->mConds[] = 'log_title' . $db->buildLike( $params );
253
		} elseif ( $pattern && !$wgMiserMode ) {
254
			$this->mConds[] = 'log_title' . $db->buildLike( $title->getDBkey(), $db->anyString() );
255
			$this->pattern = $pattern;
256
		} else {
257
			$this->mConds['log_title'] = $title->getDBkey();
258
		}
259
		// Paranoia: avoid brute force searches (bug 17342)
260
		$user = $this->getUser();
261 View Code Duplication
		if ( !$user->isAllowed( 'deletedhistory' ) ) {
262
			$this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
263
		} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
264
			$this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
265
				' != ' . LogPage::SUPPRESSED_ACTION;
266
		}
267
	}
268
269
	/**
270
	 * Set the log_action field to a specified value (or values)
271
	 *
272
	 * @param string $action
273
	 */
274
	private function limitAction( $action ) {
275
		global $wgActionFilteredLogs;
276
		// Allow to filter the log by actions
277
		$type = $this->typeCGI;
278
		if ( $type === '' ) {
279
			// nothing to do
280
			return;
281
		}
282
		$actions = $wgActionFilteredLogs;
283
		if ( isset( $actions[$type] ) ) {
284
			// log type can be filtered by actions
285
			$this->mLogEventsList->setAllowedActions( array_keys( $actions[$type] ) );
286
			if ( $action !== '' && isset( $actions[$type][$action] ) ) {
287
				// add condition to query
288
				$this->mConds['log_action'] = $actions[$type][$action];
289
				$this->action = $action;
290
			}
291
		}
292
	}
293
294
	/**
295
	 * Constructs the most part of the query. Extra conditions are sprinkled in
296
	 * all over this class.
297
	 * @return array
298
	 */
299
	public function getQueryInfo() {
300
		$basic = DatabaseLogEntry::getSelectQueryData();
301
302
		$tables = $basic['tables'];
303
		$fields = $basic['fields'];
304
		$conds = $basic['conds'];
305
		$options = $basic['options'];
306
		$joins = $basic['join_conds'];
307
308
		$index = [];
309
		# Add log_search table if there are conditions on it.
310
		# This filters the results to only include log rows that have
311
		# log_search records with the specified ls_field and ls_value values.
312
		if ( array_key_exists( 'ls_field', $this->mConds ) ) {
313
			$tables[] = 'log_search';
314
			$index['log_search'] = 'ls_field_val';
315
			$index['logging'] = 'PRIMARY';
316
			if ( !$this->hasEqualsClause( 'ls_field' )
317
				|| !$this->hasEqualsClause( 'ls_value' )
318
			) {
319
				# Since (ls_field,ls_value,ls_logid) is unique, if the condition is
320
				# to match a specific (ls_field,ls_value) tuple, then there will be
321
				# no duplicate log rows. Otherwise, we need to remove the duplicates.
322
				$options[] = 'DISTINCT';
323
			}
324
		}
325
		if ( count( $index ) ) {
326
			$options['USE INDEX'] = $index;
327
		}
328
		# Don't show duplicate rows when using log_search
329
		$joins['log_search'] = [ 'INNER JOIN', 'ls_log_id=log_id' ];
330
331
		$info = [
332
			'tables' => $tables,
333
			'fields' => $fields,
334
			'conds' => array_merge( $conds, $this->mConds ),
335
			'options' => $options,
336
			'join_conds' => $joins,
337
		];
338
		# Add ChangeTags filter query
339
		ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'],
340
			$info['join_conds'], $info['options'], $this->mTagFilter );
341
342
		return $info;
343
	}
344
345
	/**
346
	 * Checks if $this->mConds has $field matched to a *single* value
347
	 * @param string $field
348
	 * @return bool
349
	 */
350
	protected function hasEqualsClause( $field ) {
351
		return (
352
			array_key_exists( $field, $this->mConds ) &&
353
			( !is_array( $this->mConds[$field] ) || count( $this->mConds[$field] ) == 1 )
354
		);
355
	}
356
357
	function getIndexField() {
358
		return 'log_timestamp';
359
	}
360
361
	public function getStartBody() {
362
		# Do a link batch query
363
		if ( $this->getNumRows() > 0 ) {
364
			$lb = new LinkBatch;
365
			foreach ( $this->mResult as $row ) {
366
				$lb->add( $row->log_namespace, $row->log_title );
367
				$lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
368
				$lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
369
				$formatter = LogFormatter::newFromRow( $row );
370
				foreach ( $formatter->getPreloadTitles() as $title ) {
371
					$lb->addObj( $title );
372
				}
373
			}
374
			$lb->execute();
375
			$this->mResult->seek( 0 );
376
		}
377
378
		return '';
379
	}
380
381
	public function formatRow( $row ) {
382
		return $this->mLogEventsList->logLine( $row );
383
	}
384
385
	public function getType() {
386
		return $this->types;
387
	}
388
389
	/**
390
	 * Guaranteed to either return a valid title string or a Zero-Length String
391
	 *
392
	 * @return string
393
	 */
394
	public function getPerformer() {
395
		return $this->performer;
396
	}
397
398
	/**
399
	 * @return string
400
	 */
401
	public function getPage() {
402
		return $this->title;
403
	}
404
405
	public function getPattern() {
406
		return $this->pattern;
407
	}
408
409
	public function getYear() {
410
		return $this->mYear;
411
	}
412
413
	public function getMonth() {
414
		return $this->mMonth;
415
	}
416
417
	public function getTagFilter() {
418
		return $this->mTagFilter;
419
	}
420
421
	public function getAction() {
422
		return $this->action;
423
	}
424
425
	public function doQuery() {
426
		// Workaround MySQL optimizer bug
427
		$this->mDb->setBigSelects();
428
		parent::doQuery();
429
		$this->mDb->setBigSelects( 'default' );
430
	}
431
}
432