Completed
Branch master (098997)
by
unknown
28:44
created

ChangesListSpecialPage   D

Complexity

Total Complexity 57

Size/Duplication

Total Lines 459
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
dl 0
loc 459
rs 4.5205
c 0
b 0
f 0
wmc 57
lcom 1
cbo 16

23 Methods

Rating   Name   Duplication   Size   Complexity  
B execute() 0 36 6
A getRows() 0 6 1
A getOptions() 0 7 2
A setup() 0 17 3
A getDefaultOptions() 0 21 2
A getCustomFilters() 0 8 2
A fetchOptionsFromRequest() 0 5 1
A parseParameters() 0 3 1
A validateOptions() 0 3 1
F buildMainQueryConds() 0 74 19
B doMainQuery() 0 32 2
A runMainQueryHook() 0 8 1
A getDB() 0 3 1
A webOutput() 0 8 2
A outputFeedLinks() 0 3 1
outputChangesList() 0 1 ?
A doHeader() 0 7 1
A setTopText() 0 3 1
A setBottomText() 0 3 1
A getExtraOptions() 0 3 1
B makeLegend() 0 44 6
A getGroupName() 0 3 1
A addModules() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like ChangesListSpecialPage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ChangesListSpecialPage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Special page which uses a ChangesList to show query results.
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
 * Special page which uses a ChangesList to show query results.
26
 * @todo Way too many public functions, most of them should be protected
27
 *
28
 * @ingroup SpecialPage
29
 */
30
abstract class ChangesListSpecialPage extends SpecialPage {
31
	/** @var string */
32
	protected $rcSubpage;
33
34
	/** @var FormOptions */
35
	protected $rcOptions;
36
37
	/** @var array */
38
	protected $customFilters;
39
40
	/**
41
	 * Main execution point
42
	 *
43
	 * @param string $subpage
44
	 */
45
	public function execute( $subpage ) {
46
		$this->rcSubpage = $subpage;
47
48
		$this->setHeaders();
49
		$this->outputHeader();
50
		$this->addModules();
51
52
		$rows = $this->getRows();
53
		$opts = $this->getOptions();
54
		if ( $rows === false ) {
55
			if ( !$this->including() ) {
56
				$this->doHeader( $opts, 0 );
57
				$this->getOutput()->setStatusCode( 404 );
58
			}
59
60
			return;
61
		}
62
63
		$batch = new LinkBatch;
64
		foreach ( $rows as $row ) {
0 ignored issues
show
Bug introduced by
The expression $rows 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...
65
			$batch->add( NS_USER, $row->rc_user_text );
66
			$batch->add( NS_USER_TALK, $row->rc_user_text );
67
			$batch->add( $row->rc_namespace, $row->rc_title );
68
			if ( $row->rc_source === RecentChange::SRC_LOG ) {
69
				$formatter = LogFormatter::newFromRow( $row );
70
				foreach ( $formatter->getPreloadTitles() as $title ) {
71
					$batch->addObj( $title );
72
				}
73
			}
74
		}
75
		$batch->execute();
76
77
		$this->webOutput( $rows, $opts );
0 ignored issues
show
Bug introduced by
It seems like $rows defined by $this->getRows() on line 52 can also be of type boolean; however, ChangesListSpecialPage::webOutput() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
78
79
		$rows->free();
80
	}
81
82
	/**
83
	 * Get the database result for this special page instance. Used by ApiFeedRecentChanges.
84
	 *
85
	 * @return bool|ResultWrapper Result or false
86
	 */
87
	public function getRows() {
88
		$opts = $this->getOptions();
89
		$conds = $this->buildMainQueryConds( $opts );
90
91
		return $this->doMainQuery( $conds, $opts );
92
	}
93
94
	/**
95
	 * Get the current FormOptions for this request
96
	 *
97
	 * @return FormOptions
98
	 */
99
	public function getOptions() {
100
		if ( $this->rcOptions === null ) {
101
			$this->rcOptions = $this->setup( $this->rcSubpage );
102
		}
103
104
		return $this->rcOptions;
105
	}
106
107
	/**
108
	 * Create a FormOptions object with options as specified by the user
109
	 *
110
	 * @param array $parameters
111
	 *
112
	 * @return FormOptions
113
	 */
114
	public function setup( $parameters ) {
115
		$opts = $this->getDefaultOptions();
116
		foreach ( $this->getCustomFilters() as $key => $params ) {
117
			$opts->add( $key, $params['default'] );
118
		}
119
120
		$opts = $this->fetchOptionsFromRequest( $opts );
121
122
		// Give precedence to subpage syntax
123
		if ( $parameters !== null ) {
124
			$this->parseParameters( $parameters, $opts );
125
		}
126
127
		$this->validateOptions( $opts );
128
129
		return $opts;
130
	}
131
132
	/**
133
	 * Get a FormOptions object containing the default options. By default returns some basic options,
134
	 * you might want to not call parent method and discard them, or to override default values.
135
	 *
136
	 * @return FormOptions
137
	 */
138
	public function getDefaultOptions() {
139
		$config = $this->getConfig();
140
		$opts = new FormOptions();
141
142
		$opts->add( 'hideminor', false );
143
		$opts->add( 'hidebots', false );
144
		$opts->add( 'hideanons', false );
145
		$opts->add( 'hideliu', false );
146
		$opts->add( 'hidepatrolled', false );
147
		$opts->add( 'hidemyself', false );
148
149
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
150
			$opts->add( 'hidecategorization', false );
151
		}
152
153
		$opts->add( 'namespace', '', FormOptions::INTNULL );
154
		$opts->add( 'invert', false );
155
		$opts->add( 'associated', false );
156
157
		return $opts;
158
	}
159
160
	/**
161
	 * Get custom show/hide filters
162
	 *
163
	 * @return array Map of filter URL param names to properties (msg/default)
164
	 */
165
	protected function getCustomFilters() {
166
		if ( $this->customFilters === null ) {
167
			$this->customFilters = [];
168
			Hooks::run( 'ChangesListSpecialPageFilters', [ $this, &$this->customFilters ] );
169
		}
170
171
		return $this->customFilters;
172
	}
173
174
	/**
175
	 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
176
	 *
177
	 * Intended for subclassing, e.g. to add a backwards-compatibility layer.
178
	 *
179
	 * @param FormOptions $opts
180
	 * @return FormOptions
181
	 */
182
	protected function fetchOptionsFromRequest( $opts ) {
183
		$opts->fetchValuesFromRequest( $this->getRequest() );
184
185
		return $opts;
186
	}
187
188
	/**
189
	 * Process $par and put options found in $opts. Used when including the page.
190
	 *
191
	 * @param string $par
192
	 * @param FormOptions $opts
193
	 */
194
	public function parseParameters( $par, FormOptions $opts ) {
195
		// nothing by default
196
	}
197
198
	/**
199
	 * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
200
	 *
201
	 * @param FormOptions $opts
202
	 */
203
	public function validateOptions( FormOptions $opts ) {
204
		// nothing by default
205
	}
206
207
	/**
208
	 * Return an array of conditions depending of options set in $opts
209
	 *
210
	 * @param FormOptions $opts
211
	 * @return array
212
	 */
213
	public function buildMainQueryConds( FormOptions $opts ) {
214
		$dbr = $this->getDB();
215
		$user = $this->getUser();
216
		$conds = [];
217
218
		// It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on
219
		// what the user meant and either show only bots or force anons to be shown.
220
		$botsonly = false;
221
		$hideanons = $opts['hideanons'];
222
		if ( $opts['hideanons'] && $opts['hideliu'] ) {
223
			if ( $opts['hidebots'] ) {
224
				$hideanons = false;
225
			} else {
226
				$botsonly = true;
227
			}
228
		}
229
230
		// Toggles
231
		if ( $opts['hideminor'] ) {
232
			$conds['rc_minor'] = 0;
233
		}
234
		if ( $opts['hidebots'] ) {
235
			$conds['rc_bot'] = 0;
236
		}
237
		if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
238
			$conds['rc_patrolled'] = 0;
239
		}
240
		if ( $botsonly ) {
241
			$conds['rc_bot'] = 1;
242
		} else {
243
			if ( $opts['hideliu'] ) {
244
				$conds[] = 'rc_user = 0';
245
			}
246
			if ( $hideanons ) {
247
				$conds[] = 'rc_user != 0';
248
			}
249
		}
250
		if ( $opts['hidemyself'] ) {
251
			if ( $user->getId() ) {
252
				$conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
253
			} else {
254
				$conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
255
			}
256
		}
257
		if ( $this->getConfig()->get( 'RCWatchCategoryMembership' )
258
			&& $opts['hidecategorization'] === true
259
		) {
260
			$conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
261
		}
262
263
		// Namespace filtering
264
		if ( $opts['namespace'] !== '' ) {
265
			$selectedNS = $dbr->addQuotes( $opts['namespace'] );
266
			$operator = $opts['invert'] ? '!=' : '=';
267
			$boolean = $opts['invert'] ? 'AND' : 'OR';
268
269
			// Namespace association (bug 2429)
270
			if ( !$opts['associated'] ) {
271
				$condition = "rc_namespace $operator $selectedNS";
272
			} else {
273
				// Also add the associated namespace
274
				$associatedNS = $dbr->addQuotes(
275
					MWNamespace::getAssociated( $opts['namespace'] )
276
				);
277
				$condition = "(rc_namespace $operator $selectedNS "
278
					. $boolean
279
					. " rc_namespace $operator $associatedNS)";
280
			}
281
282
			$conds[] = $condition;
283
		}
284
285
		return $conds;
286
	}
287
288
	/**
289
	 * Process the query
290
	 *
291
	 * @param array $conds
292
	 * @param FormOptions $opts
293
	 * @return bool|ResultWrapper Result or false
294
	 */
295
	public function doMainQuery( $conds, $opts ) {
296
		$tables = [ 'recentchanges' ];
297
		$fields = RecentChange::selectFields();
298
		$query_options = [];
299
		$join_conds = [];
300
301
		ChangeTags::modifyDisplayQuery(
302
			$tables,
303
			$fields,
304
			$conds,
305
			$join_conds,
306
			$query_options,
307
			''
308
		);
309
310
		if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
311
			$opts )
312
		) {
313
			return false;
314
		}
315
316
		$dbr = $this->getDB();
317
318
		return $dbr->select(
319
			$tables,
320
			$fields,
321
			$conds,
322
			__METHOD__,
323
			$query_options,
324
			$join_conds
325
		);
326
	}
327
328
	protected function runMainQueryHook( &$tables, &$fields, &$conds,
329
		&$query_options, &$join_conds, $opts
330
	) {
331
		return Hooks::run(
332
			'ChangesListSpecialPageQuery',
333
			[ $this->getName(), &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ]
334
		);
335
	}
336
337
	/**
338
	 * Return a IDatabase object for reading
339
	 *
340
	 * @return IDatabase
341
	 */
342
	protected function getDB() {
343
		return wfGetDB( DB_REPLICA );
344
	}
345
346
	/**
347
	 * Send output to the OutputPage object, only called if not used feeds
348
	 *
349
	 * @param ResultWrapper $rows Database rows
350
	 * @param FormOptions $opts
351
	 */
352
	public function webOutput( $rows, $opts ) {
353
		if ( !$this->including() ) {
354
			$this->outputFeedLinks();
355
			$this->doHeader( $opts, $rows->numRows() );
356
		}
357
358
		$this->outputChangesList( $rows, $opts );
359
	}
360
361
	/**
362
	 * Output feed links.
363
	 */
364
	public function outputFeedLinks() {
365
		// nothing by default
366
	}
367
368
	/**
369
	 * Build and output the actual changes list.
370
	 *
371
	 * @param ResultWrapper $rows Database rows
372
	 * @param FormOptions $opts
373
	 */
374
	abstract public function outputChangesList( $rows, $opts );
375
376
	/**
377
	 * Set the text to be displayed above the changes
378
	 *
379
	 * @param FormOptions $opts
380
	 * @param int $numRows Number of rows in the result to show after this header
381
	 */
382
	public function doHeader( $opts, $numRows ) {
383
		$this->setTopText( $opts );
384
385
		// @todo Lots of stuff should be done here.
386
387
		$this->setBottomText( $opts );
388
	}
389
390
	/**
391
	 * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
392
	 * or similar methods to print the text.
393
	 *
394
	 * @param FormOptions $opts
395
	 */
396
	public function setTopText( FormOptions $opts ) {
397
		// nothing by default
398
	}
399
400
	/**
401
	 * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
402
	 * or similar methods to print the text.
403
	 *
404
	 * @param FormOptions $opts
405
	 */
406
	public function setBottomText( FormOptions $opts ) {
407
		// nothing by default
408
	}
409
410
	/**
411
	 * Get options to be displayed in a form
412
	 * @todo This should handle options returned by getDefaultOptions().
413
	 * @todo Not called by anything, should be called by something… doHeader() maybe?
414
	 *
415
	 * @param FormOptions $opts
416
	 * @return array
417
	 */
418
	public function getExtraOptions( $opts ) {
419
		return [];
420
	}
421
422
	/**
423
	 * Return the legend displayed within the fieldset
424
	 *
425
	 * @return string
426
	 */
427
	public function makeLegend() {
428
		$context = $this->getContext();
429
		$user = $context->getUser();
430
		# The legend showing what the letters and stuff mean
431
		$legend = Html::openElement( 'dl' ) . "\n";
432
		# Iterates through them and gets the messages for both letter and tooltip
433
		$legendItems = $context->getConfig()->get( 'RecentChangesFlags' );
434
		if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
435
			unset( $legendItems['unpatrolled'] );
436
		}
437
		foreach ( $legendItems as $key => $item ) { # generate items of the legend
438
			$label = isset( $item['legend'] ) ? $item['legend'] : $item['title'];
439
			$letter = $item['letter'];
440
			$cssClass = isset( $item['class'] ) ? $item['class'] : $key;
441
442
			$legend .= Html::element( 'dt',
443
				[ 'class' => $cssClass ], $context->msg( $letter )->text()
444
			) . "\n" .
445
			Html::rawElement( 'dd',
446
				[ 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ],
447
				$context->msg( $label )->parse()
448
			) . "\n";
449
		}
450
		# (+-123)
451
		$legend .= Html::rawElement( 'dt',
452
			[ 'class' => 'mw-plusminus-pos' ],
453
			$context->msg( 'recentchanges-legend-plusminus' )->parse()
454
		) . "\n";
455
		$legend .= Html::element(
456
			'dd',
457
			[ 'class' => 'mw-changeslist-legend-plusminus' ],
458
			$context->msg( 'recentchanges-label-plusminus' )->text()
459
		) . "\n";
460
		$legend .= Html::closeElement( 'dl' ) . "\n";
461
462
		# Collapsibility
463
		$legend =
464
			'<div class="mw-changeslist-legend">' .
465
				$context->msg( 'recentchanges-legend-heading' )->parse() .
466
				'<div class="mw-collapsible-content">' . $legend . '</div>' .
467
			'</div>';
468
469
		return $legend;
470
	}
471
472
	/**
473
	 * Add page-specific modules.
474
	 */
475
	protected function addModules() {
476
		$out = $this->getOutput();
477
		// Styles and behavior for the legend box (see makeLegend())
478
		$out->addModuleStyles( [
479
			'mediawiki.special.changeslist.legend',
480
			'mediawiki.special.changeslist',
481
		] );
482
		$out->addModules( 'mediawiki.special.changeslist.legend.js' );
483
	}
484
485
	protected function getGroupName() {
486
		return 'changes';
487
	}
488
}
489