ApiQueryBacklinksprop::run()   F
last analyzed

Complexity

Conditions 46
Paths > 20000

Size

Total Lines 236
Code Lines 149

Duplication

Lines 5
Ratio 2.12 %

Importance

Changes 0
Metric Value
cc 46
eloc 149
nc 547202
nop 1
dl 5
loc 236
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * API module to handle links table back-queries
4
 *
5
 * Created on Aug 19, 2014
6
 *
7
 * Copyright © 2014 Brad Jorsch <[email protected]>
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 * @since 1.24
26
 */
27
28
/**
29
 * This implements prop=redirects, prop=linkshere, prop=catmembers,
30
 * prop=transcludedin, and prop=fileusage
31
 *
32
 * @ingroup API
33
 * @since 1.24
34
 */
35
class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
36
37
	// Data for the various modules implemented by this class
38
	private static $settings = [
39
		'redirects' => [
40
			'code' => 'rd',
41
			'prefix' => 'rd',
42
			'linktable' => 'redirect',
43
			'props' => [
44
				'fragment',
45
			],
46
			'showredirects' => false,
47
			'show' => [
48
				'fragment',
49
				'!fragment',
50
			],
51
		],
52
		'linkshere' => [
53
			'code' => 'lh',
54
			'prefix' => 'pl',
55
			'linktable' => 'pagelinks',
56
			'indexes' => [ 'pl_namespace', 'pl_backlinks_namespace' ],
57
			'from_namespace' => true,
58
			'showredirects' => true,
59
		],
60
		'transcludedin' => [
61
			'code' => 'ti',
62
			'prefix' => 'tl',
63
			'linktable' => 'templatelinks',
64
			'indexes' => [ 'tl_namespace', 'tl_backlinks_namespace' ],
65
			'from_namespace' => true,
66
			'showredirects' => true,
67
		],
68
		'fileusage' => [
69
			'code' => 'fu',
70
			'prefix' => 'il',
71
			'linktable' => 'imagelinks',
72
			'indexes' => [ 'il_to', 'il_backlinks_namespace' ],
73
			'from_namespace' => true,
74
			'to_namespace' => NS_FILE,
75
			'exampletitle' => 'File:Example.jpg',
76
			'showredirects' => true,
77
		],
78
	];
79
80
	public function __construct( ApiQuery $query, $moduleName ) {
81
		parent::__construct( $query, $moduleName, self::$settings[$moduleName]['code'] );
82
	}
83
84
	public function execute() {
85
		$this->run();
86
	}
87
88
	public function executeGenerator( $resultPageSet ) {
89
		$this->run( $resultPageSet );
90
	}
91
92
	/**
93
	 * @param ApiPageSet $resultPageSet
94
	 */
95
	private function run( ApiPageSet $resultPageSet = null ) {
96
		$settings = self::$settings[$this->getModuleName()];
97
98
		$db = $this->getDB();
99
		$params = $this->extractRequestParams();
100
		$prop = array_flip( $params['prop'] );
101
		$emptyString = $db->addQuotes( '' );
102
103
		$pageSet = $this->getPageSet();
104
		$titles = $pageSet->getGoodAndMissingTitles();
105
		$map = $pageSet->getGoodAndMissingTitlesByNamespace();
106
107
		// Determine our fields to query on
108
		$p = $settings['prefix'];
109
		$hasNS = !isset( $settings['to_namespace'] );
110
		if ( $hasNS ) {
111
			$bl_namespace = "{$p}_namespace";
112
			$bl_title = "{$p}_title";
113
		} else {
114
			$bl_namespace = $settings['to_namespace'];
115
			$bl_title = "{$p}_to";
116
117
			$titles = array_filter( $titles, function ( $t ) use ( $bl_namespace ) {
118
				return $t->getNamespace() === $bl_namespace;
119
			} );
120
			$map = array_intersect_key( $map, [ $bl_namespace => true ] );
121
		}
122
		$bl_from = "{$p}_from";
123
124
		if ( !$titles ) {
125
			return; // nothing to do
126
		}
127
128
		// Figure out what we're sorting by, and add associated WHERE clauses.
129
		// MySQL's query planner screws up if we include a field in ORDER BY
130
		// when it's constant in WHERE, so we have to test that for each field.
131
		$sortby = [];
132
		if ( $hasNS && count( $map ) > 1 ) {
133
			$sortby[$bl_namespace] = 'ns';
134
		}
135
		$theTitle = null;
136
		foreach ( $map as $nsTitles ) {
137
			reset( $nsTitles );
138
			$key = key( $nsTitles );
139
			if ( $theTitle === null ) {
140
				$theTitle = $key;
141
			}
142
			if ( count( $nsTitles ) > 1 || $key !== $theTitle ) {
143
				$sortby[$bl_title] = 'title';
144
				break;
145
			}
146
		}
147
		$miser_ns = null;
148
		if ( $params['namespace'] !== null ) {
149
			if ( empty( $settings['from_namespace'] ) ) {
150 View Code Duplication
				if ( $this->getConfig()->get( 'MiserMode' ) ) {
151
					$miser_ns = $params['namespace'];
152
				} else {
153
					$this->addWhereFld( 'page_namespace', $params['namespace'] );
154
				}
155
			} else {
156
				$this->addWhereFld( "{$p}_from_namespace", $params['namespace'] );
157
				if ( !empty( $settings['from_namespace'] ) && count( $params['namespace'] ) > 1 ) {
158
					$sortby["{$p}_from_namespace"] = 'int';
159
				}
160
			}
161
		}
162
		$sortby[$bl_from] = 'int';
163
164
		// Now use the $sortby to figure out the continuation
165
		if ( !is_null( $params['continue'] ) ) {
166
			$cont = explode( '|', $params['continue'] );
167
			$this->dieContinueUsageIf( count( $cont ) != count( $sortby ) );
168
			$where = '';
169
			$i = count( $sortby ) - 1;
170
			foreach ( array_reverse( $sortby, true ) as $field => $type ) {
171
				$v = $cont[$i];
172
				switch ( $type ) {
173
					case 'ns':
174
					case 'int':
175
						$v = (int)$v;
176
						$this->dieContinueUsageIf( $v != $cont[$i] );
177
						break;
178
					default:
179
						$v = $db->addQuotes( $v );
180
						break;
181
				}
182
183
				if ( $where === '' ) {
184
					$where = "$field >= $v";
185
				} else {
186
					$where = "$field > $v OR ($field = $v AND ($where))";
187
				}
188
189
				$i--;
190
			}
191
			$this->addWhere( $where );
192
		}
193
194
		// Populate the rest of the query
195
		$this->addTables( [ $settings['linktable'], 'page' ] );
196
		$this->addWhere( "$bl_from = page_id" );
197
198
		if ( $this->getModuleName() === 'redirects' ) {
199
			$this->addWhere( "rd_interwiki = $emptyString OR rd_interwiki IS NULL" );
200
		}
201
202
		$this->addFields( array_keys( $sortby ) );
203
		$this->addFields( [ 'bl_namespace' => $bl_namespace, 'bl_title' => $bl_title ] );
204
		if ( is_null( $resultPageSet ) ) {
205
			$fld_pageid = isset( $prop['pageid'] );
206
			$fld_title = isset( $prop['title'] );
207
			$fld_redirect = isset( $prop['redirect'] );
208
209
			$this->addFieldsIf( 'page_id', $fld_pageid );
210
			$this->addFieldsIf( [ 'page_title', 'page_namespace' ], $fld_title );
211
			$this->addFieldsIf( 'page_is_redirect', $fld_redirect );
212
213
			// prop=redirects
214
			$fld_fragment = isset( $prop['fragment'] );
215
			$this->addFieldsIf( 'rd_fragment', $fld_fragment );
216
		} else {
217
			$this->addFields( $resultPageSet->getPageTableFields() );
218
		}
219
220
		$this->addFieldsIf( 'page_namespace', $miser_ns !== null );
221
222
		if ( $hasNS ) {
223
			$lb = new LinkBatch( $titles );
224
			$this->addWhere( $lb->constructSet( $p, $db ) );
0 ignored issues
show
Bug introduced by
It seems like $lb->constructSet($p, $db) targeting LinkBatch::constructSet() can also be of type boolean; however, ApiQueryBase::addWhere() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that 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...
225
		} else {
226
			$where = [];
227
			foreach ( $titles as $t ) {
228
				if ( $t->getNamespace() == $bl_namespace ) {
229
					$where[] = "$bl_title = " . $db->addQuotes( $t->getDBkey() );
230
				}
231
			}
232
			$this->addWhere( $db->makeList( $where, LIST_OR ) );
233
		}
234
235
		if ( $params['show'] !== null ) {
236
			// prop=redirects only
237
			$show = array_flip( $params['show'] );
238
			if ( isset( $show['fragment'] ) && isset( $show['!fragment'] ) ||
239
				isset( $show['redirect'] ) && isset( $show['!redirect'] )
240
			) {
241
				$this->dieUsageMsg( 'show' );
242
			}
243
			$this->addWhereIf( "rd_fragment != $emptyString", isset( $show['fragment'] ) );
244
			$this->addWhereIf(
245
				"rd_fragment = $emptyString OR rd_fragment IS NULL",
246
				isset( $show['!fragment'] )
247
			);
248
			$this->addWhereIf( [ 'page_is_redirect' => 1 ], isset( $show['redirect'] ) );
249
			$this->addWhereIf( [ 'page_is_redirect' => 0 ], isset( $show['!redirect'] ) );
250
		}
251
252
		// Override any ORDER BY from above with what we calculated earlier.
253
		$this->addOption( 'ORDER BY', array_keys( $sortby ) );
254
255
		// MySQL's optimizer chokes if we have too many values in "$bl_title IN
256
		// (...)" and chooses the wrong index, so specify the correct index to
257
		// use for the query. See T139056 for details.
258
		if ( !empty( $settings['indexes'] ) ) {
259
			list( $idxNoFromNS, $idxWithFromNS ) = $settings['indexes'];
260
			if ( $params['namespace'] !== null && !empty( $settings['from_namespace'] ) ) {
261
				$this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxWithFromNS ] );
262
			} else {
263
				$this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxNoFromNS ] );
264
			}
265
		}
266
267
		// MySQL (or at least 5.5.5-10.0.23-MariaDB) chooses a really bad query
268
		// plan if it thinks there will be more matching rows in the linktable
269
		// than are in page. Use STRAIGHT_JOIN here to force it to use the
270
		// intended, fast plan. See T145079 for details.
271
		$this->addOption( 'STRAIGHT_JOIN' );
272
273
		$this->addOption( 'LIMIT', $params['limit'] + 1 );
274
275
		$res = $this->select( __METHOD__ );
276
277
		if ( is_null( $resultPageSet ) ) {
278
			$count = 0;
279
			foreach ( $res as $row ) {
280
				if ( ++$count > $params['limit'] ) {
281
					// We've reached the one extra which shows that
282
					// there are additional pages to be had. Stop here...
283
					$this->setContinue( $row, $sortby );
284
					break;
285
				}
286
287
				if ( $miser_ns !== null && !in_array( $row->page_namespace, $miser_ns ) ) {
288
					// Miser mode namespace check
289
					continue;
290
				}
291
292
				// Get the ID of the current page
293
				$id = $map[$row->bl_namespace][$row->bl_title];
294
295
				$vals = [];
296
				if ( $fld_pageid ) {
0 ignored issues
show
Bug introduced by
The variable $fld_pageid 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...
297
					$vals['pageid'] = (int)$row->page_id;
298
				}
299
				if ( $fld_title ) {
0 ignored issues
show
Bug introduced by
The variable $fld_title 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...
300
					ApiQueryBase::addTitleInfo( $vals,
301
						Title::makeTitle( $row->page_namespace, $row->page_title )
302
					);
303
				}
304
				if ( $fld_fragment && $row->rd_fragment !== null && $row->rd_fragment !== '' ) {
0 ignored issues
show
Bug introduced by
The variable $fld_fragment 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...
305
					$vals['fragment'] = $row->rd_fragment;
306
				}
307
				if ( $fld_redirect ) {
0 ignored issues
show
Bug introduced by
The variable $fld_redirect 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...
308
					$vals['redirect'] = (bool)$row->page_is_redirect;
309
				}
310
				$fit = $this->addPageSubItem( $id, $vals );
311
				if ( !$fit ) {
312
					$this->setContinue( $row, $sortby );
313
					break;
314
				}
315
			}
316
		} else {
317
			$titles = [];
318
			$count = 0;
319
			foreach ( $res as $row ) {
320
				if ( ++$count > $params['limit'] ) {
321
					// We've reached the one extra which shows that
322
					// there are additional pages to be had. Stop here...
323
					$this->setContinue( $row, $sortby );
324
					break;
325
				}
326
				$titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
327
			}
328
			$resultPageSet->populateFromTitles( $titles );
329
		}
330
	}
331
332
	private function setContinue( $row, $sortby ) {
333
		$cont = [];
334
		foreach ( $sortby as $field => $v ) {
335
			$cont[] = $row->$field;
336
		}
337
		$this->setContinueEnumParameter( 'continue', implode( '|', $cont ) );
338
	}
339
340
	public function getCacheMode( $params ) {
341
		return 'public';
342
	}
343
344
	public function getAllowedParams() {
345
		$settings = self::$settings[$this->getModuleName()];
346
347
		$ret = [
348
			'prop' => [
349
				ApiBase::PARAM_TYPE => [
350
					'pageid',
351
					'title',
352
				],
353
				ApiBase::PARAM_ISMULTI => true,
354
				ApiBase::PARAM_DFLT => 'pageid|title',
355
				ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
356
			],
357
			'namespace' => [
358
				ApiBase::PARAM_ISMULTI => true,
359
				ApiBase::PARAM_TYPE => 'namespace',
360
			],
361
			'show' => null, // Will be filled/removed below
362
			'limit' => [
363
				ApiBase::PARAM_DFLT => 10,
364
				ApiBase::PARAM_TYPE => 'limit',
365
				ApiBase::PARAM_MIN => 1,
366
				ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
367
				ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
368
			],
369
			'continue' => [
370
				ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
371
			],
372
		];
373
374
		if ( empty( $settings['from_namespace'] ) && $this->getConfig()->get( 'MiserMode' ) ) {
375
			$ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [
376
				'api-help-param-limited-in-miser-mode',
377
			];
378
		}
379
380
		if ( !empty( $settings['showredirects'] ) ) {
381
			$ret['prop'][ApiBase::PARAM_TYPE][] = 'redirect';
382
			$ret['prop'][ApiBase::PARAM_DFLT] .= '|redirect';
383
		}
384
		if ( isset( $settings['props'] ) ) {
385
			$ret['prop'][ApiBase::PARAM_TYPE] = array_merge(
386
				$ret['prop'][ApiBase::PARAM_TYPE], $settings['props']
387
			);
388
		}
389
390
		$show = [];
391
		if ( !empty( $settings['showredirects'] ) ) {
392
			$show[] = 'redirect';
393
			$show[] = '!redirect';
394
		}
395
		if ( isset( $settings['show'] ) ) {
396
			$show = array_merge( $show, $settings['show'] );
397
		}
398
		if ( $show ) {
399
			$ret['show'] = [
400
				ApiBase::PARAM_TYPE => $show,
401
				ApiBase::PARAM_ISMULTI => true,
402
			];
403
		} else {
404
			unset( $ret['show'] );
405
		}
406
407
		return $ret;
408
	}
409
410
	protected function getExamplesMessages() {
411
		$settings = self::$settings[$this->getModuleName()];
412
		$name = $this->getModuleName();
413
		$path = $this->getModulePath();
414
		$title = isset( $settings['exampletitle'] ) ? $settings['exampletitle'] : 'Main Page';
415
		$etitle = rawurlencode( $title );
416
417
		return [
418
			"action=query&prop={$name}&titles={$etitle}"
419
				=> "apihelp-$path-example-simple",
420
			"action=query&generator={$name}&titles={$etitle}&prop=info"
421
				=> "apihelp-$path-example-generator",
422
		];
423
	}
424
425
	public function getHelpUrls() {
426
		$name = ucfirst( $this->getModuleName() );
427
		return "https://www.mediawiki.org/wiki/API:{$name}";
428
	}
429
}
430