Completed
Branch master (62f6c6)
by
unknown
21:31
created

ApiParse   F

Complexity

Total Complexity 147

Size/Duplication

Total Lines 810
Duplicated Lines 6.3 %

Coupling/Cohesion

Components 1
Dependencies 24

Importance

Changes 0
Metric Value
dl 51
loc 810
rs 1.133
c 0
b 0
f 0
wmc 147
lcom 1
cbo 24

18 Methods

Rating   Name   Duplication   Size   Complexity  
F execute() 29 427 93
A makeParserOptions() 0 13 4
B getParsedContent() 0 20 5
A getContent() 0 11 4
A getSectionContent() 0 13 3
B formatSummary() 0 17 8
B formatLangLinks() 0 25 3
B formatCategoryLinks() 0 40 6
A categoriesHtml() 0 6 1
A formatLinks() 0 14 3
A formatIWLinks() 0 19 4
A formatHeadItems() 11 11 2
A formatCss() 11 11 2
A formatLimitReportData() 0 16 3
A setIndexedTagNames() 0 7 3
B getAllowedParams() 0 80 1
A getExamplesMessages() 0 12 1
A getHelpUrls() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ApiParse 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 ApiParse, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Created on Dec 01, 2007
4
 *
5
 * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
6
 *
7
 * This program is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 2 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along
18
 * with this program; if not, write to the Free Software Foundation, Inc.,
19
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
 * http://www.gnu.org/copyleft/gpl.html
21
 *
22
 * @file
23
 */
24
25
/**
26
 * @ingroup API
27
 */
28
class ApiParse extends ApiBase {
29
30
	/** @var string $section */
31
	private $section = null;
32
33
	/** @var Content $content */
34
	private $content = null;
35
36
	/** @var Content $pstContent */
37
	private $pstContent = null;
38
39
	public function execute() {
40
		// The data is hot but user-dependent, like page views, so we set vary cookies
41
		$this->getMain()->setCacheMode( 'anon-public-user-private' );
42
43
		// Get parameters
44
		$params = $this->extractRequestParams();
45
		$text = $params['text'];
46
		$title = $params['title'];
47
		if ( $title === null ) {
48
			$titleProvided = false;
49
			// A title is needed for parsing, so arbitrarily choose one
50
			$title = 'API';
51
		} else {
52
			$titleProvided = true;
53
		}
54
55
		$page = $params['page'];
56
		$pageid = $params['pageid'];
57
		$oldid = $params['oldid'];
58
59
		$model = $params['contentmodel'];
60
		$format = $params['contentformat'];
61
62
		if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) {
63
			$this->dieUsage(
64
				'The page parameter cannot be used together with the text and title parameters',
65
				'params'
66
			);
67
		}
68
69
		$prop = array_flip( $params['prop'] );
70
71
		if ( isset( $params['section'] ) ) {
72
			$this->section = $params['section'];
73
			if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
74
				$this->dieUsage(
75
					'The section parameter must be a valid section id or "new"', 'invalidsection'
76
				);
77
			}
78
		} else {
79
			$this->section = false;
0 ignored issues
show
Documentation Bug introduced by
The property $section was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
80
		}
81
82
		// The parser needs $wgTitle to be set, apparently the
83
		// $title parameter in Parser::parse isn't enough *sigh*
84
		// TODO: Does this still need $wgTitle?
85
		global $wgParser, $wgTitle;
86
87
		$redirValues = null;
88
89
		// Return result
90
		$result = $this->getResult();
91
92
		if ( !is_null( $oldid ) || !is_null( $pageid ) || !is_null( $page ) ) {
93
			if ( $this->section === 'new' ) {
94
					$this->dieUsage(
95
						'section=new cannot be combined with oldid, pageid or page parameters. ' .
96
						'Please use text', 'params'
97
					);
98
			}
99
			if ( !is_null( $oldid ) ) {
100
				// Don't use the parser cache
101
				$rev = Revision::newFromId( $oldid );
102
				if ( !$rev ) {
103
					$this->dieUsage( "There is no revision ID $oldid", 'missingrev' );
104
				}
105
				if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
106
					$this->dieUsage( "You don't have permission to view deleted revisions", 'permissiondenied' );
107
				}
108
109
				$titleObj = $rev->getTitle();
110
				$wgTitle = $titleObj;
111
				$pageObj = WikiPage::factory( $titleObj );
112
				$popts = $this->makeParserOptions( $pageObj, $params );
113
114
				// If for some reason the "oldid" is actually the current revision, it may be cached
115
				// Deliberately comparing $pageObj->getLatest() with $rev->getId(), rather than
116
				// checking $rev->isCurrent(), because $pageObj is what actually ends up being used,
117
				// and if its ->getLatest() is outdated, $rev->isCurrent() won't tell us that.
118
				if ( $rev->getId() == $pageObj->getLatest() ) {
119
					// May get from/save to parser cache
120
					$p_result = $this->getParsedContent( $pageObj, $popts,
121
						$pageid, isset( $prop['wikitext'] ) );
122
				} else { // This is an old revision, so get the text differently
123
					$this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
124
125
					if ( $this->section !== false ) {
126
						$this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getSectionContent...t, 'r' . $rev->getId()) can also be of type boolean. However, the property $content is declared as type object<Content>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
127
					}
128
129
					// Should we save old revision parses to the parser cache?
130
					$p_result = $this->content->getParserOutput( $titleObj, $rev->getId(), $popts );
131
				}
132
			} else { // Not $oldid, but $pageid or $page
133
				if ( $params['redirects'] ) {
134
					$reqParams = [
135
						'redirects' => '',
136
					];
137
					if ( !is_null( $pageid ) ) {
138
						$reqParams['pageids'] = $pageid;
139
					} else { // $page
140
						$reqParams['titles'] = $page;
141
					}
142
					$req = new FauxRequest( $reqParams );
143
					$main = new ApiMain( $req );
144
					$pageSet = new ApiPageSet( $main );
145
					$pageSet->execute();
146
					$redirValues = $pageSet->getRedirectTitlesAsResult( $this->getResult() );
147
148
					$to = $page;
149
					foreach ( $pageSet->getRedirectTitles() as $title ) {
150
						$to = $title->getFullText();
151
					}
152
					$pageParams = [ 'title' => $to ];
153
				} elseif ( !is_null( $pageid ) ) {
154
					$pageParams = [ 'pageid' => $pageid ];
155
				} else { // $page
156
					$pageParams = [ 'title' => $page ];
157
				}
158
159
				$pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
160
				$titleObj = $pageObj->getTitle();
161
				if ( !$titleObj || !$titleObj->exists() ) {
162
					$this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
163
				}
164
				$wgTitle = $titleObj;
165
166
				if ( isset( $prop['revid'] ) ) {
167
					$oldid = $pageObj->getLatest();
168
				}
169
170
				$popts = $this->makeParserOptions( $pageObj, $params );
0 ignored issues
show
Bug introduced by
It seems like $pageObj defined by $this->getTitleOrPageId($pageParams, 'fromdb') on line 159 can be null; however, ApiParse::makeParserOptions() 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...
171
172
				// Don't pollute the parser cache when setting options that aren't
173
				// in ParserOptions::optionsHash()
174
				/// @todo: This should be handled closer to the actual cache instead of here, see T110269
175
				$suppressCache =
176
					$params['disablepp'] ||
177
					$params['disablelimitreport'] ||
178
					$params['preview'] ||
179
					$params['sectionpreview'] ||
180
					$params['disabletidy'];
181
182
				if ( $suppressCache ) {
183
					$this->content = $this->getContent( $pageObj, $pageid );
0 ignored issues
show
Bug introduced by
It seems like $pageObj defined by $this->getTitleOrPageId($pageParams, 'fromdb') on line 159 can be null; however, ApiParse::getContent() 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...
184
					$p_result = $this->content->getParserOutput( $titleObj, null, $popts );
185
				} else {
186
					// Potentially cached
187
					$p_result = $this->getParsedContent( $pageObj, $popts, $pageid,
0 ignored issues
show
Bug introduced by
It seems like $pageObj defined by $this->getTitleOrPageId($pageParams, 'fromdb') on line 159 can be null; however, ApiParse::getParsedContent() 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...
188
						isset( $prop['wikitext'] ) );
189
				}
190
			}
191
		} else { // Not $oldid, $pageid, $page. Hence based on $text
192
			$titleObj = Title::newFromText( $title );
193
			if ( !$titleObj || $titleObj->isExternal() ) {
194
				$this->dieUsageMsg( [ 'invalidtitle', $title ] );
195
			}
196
			$wgTitle = $titleObj;
197
			if ( $titleObj->canExist() ) {
198
				$pageObj = WikiPage::factory( $titleObj );
0 ignored issues
show
Bug introduced by
It seems like $titleObj defined by \Title::newFromText($title) on line 192 can be null; however, WikiPage::factory() 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...
199
			} else {
200
				// Do like MediaWiki::initializeArticle()
201
				$article = Article::newFromTitle( $titleObj, $this->getContext() );
0 ignored issues
show
Bug introduced by
It seems like $titleObj defined by \Title::newFromText($title) on line 192 can be null; however, Article::newFromTitle() 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...
202
				$pageObj = $article->getPage();
203
			}
204
205
			$popts = $this->makeParserOptions( $pageObj, $params );
206
			$textProvided = !is_null( $text );
207
208
			if ( !$textProvided ) {
209
				if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
210
					$this->setWarning(
211
						"'title' used without 'text', and parsed page properties were requested " .
212
						"(did you mean to use 'page' instead of 'title'?)"
213
					);
214
				}
215
				// Prevent warning from ContentHandler::makeContent()
216
				$text = '';
217
			}
218
219
			// If we are parsing text, do not use the content model of the default
220
			// API title, but default to wikitext to keep BC.
221
			if ( $textProvided && !$titleProvided && is_null( $model ) ) {
222
				$model = CONTENT_MODEL_WIKITEXT;
223
				$this->setWarning( "No 'title' or 'contentmodel' was given, assuming $model." );
224
			}
225
226
			try {
227
				$this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
228
			} catch ( MWContentSerializationException $ex ) {
229
				$this->dieUsage( $ex->getMessage(), 'parseerror' );
230
			}
231
232
			if ( $this->section !== false ) {
233
				if ( $this->section === 'new' ) {
234
					// Insert the section title above the content.
235
					if ( !is_null( $params['sectiontitle'] ) && $params['sectiontitle'] !== '' ) {
236
						$this->content = $this->content->addSectionHeader( $params['sectiontitle'] );
237
					}
238
				} else {
239
					$this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getSectionContent...Obj->getPrefixedText()) can also be of type boolean. However, the property $content is declared as type object<Content>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
240
				}
241
			}
242
243
			if ( $params['pst'] || $params['onlypst'] ) {
244
				$this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts );
0 ignored issues
show
Bug introduced by
It seems like $titleObj defined by \Title::newFromText($title) on line 192 can be null; however, Content::preSaveTransform() 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...
245
			}
246
			if ( $params['onlypst'] ) {
247
				// Build a result and bail out
248
				$result_array = [];
249
				$result_array['text'] = $this->pstContent->serialize( $format );
250
				$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
251
				if ( isset( $prop['wikitext'] ) ) {
252
					$result_array['wikitext'] = $this->content->serialize( $format );
253
					$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
254
				}
255 View Code Duplication
				if ( !is_null( $params['summary'] ) ||
256
					( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
257
				) {
258
					$result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
0 ignored issues
show
Bug introduced by
It seems like $titleObj defined by \Title::newFromText($title) on line 192 can be null; however, ApiParse::formatSummary() 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...
259
					$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
260
				}
261
262
				$result->addValue( null, $this->getModuleName(), $result_array );
263
264
				return;
265
			}
266
267
			// Not cached (save or load)
268
			if ( $params['pst'] ) {
269
				$p_result = $this->pstContent->getParserOutput( $titleObj, null, $popts );
270
			} else {
271
				$p_result = $this->content->getParserOutput( $titleObj, null, $popts );
0 ignored issues
show
Bug introduced by
It seems like $titleObj defined by \Title::newFromText($title) on line 192 can be null; however, Content::getParserOutput() 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...
272
			}
273
		}
274
275
		$result_array = [];
276
277
		$result_array['title'] = $titleObj->getPrefixedText();
278
		$result_array['pageid'] = $pageid ?: $pageObj->getId();
279
280
		if ( !is_null( $oldid ) ) {
281
			$result_array['revid'] = intval( $oldid );
282
		}
283
284
		if ( $params['redirects'] && !is_null( $redirValues ) ) {
285
			$result_array['redirects'] = $redirValues;
286
		}
287
288
		if ( $params['disabletoc'] ) {
289
			$p_result->setTOCEnabled( false );
290
		}
291
292
		if ( isset( $prop['text'] ) ) {
293
			$result_array['text'] = $p_result->getText();
294
			$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
295
		}
296
297 View Code Duplication
		if ( !is_null( $params['summary'] ) ||
298
			( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
299
		) {
300
			$result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
301
			$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
302
		}
303
304
		if ( isset( $prop['langlinks'] ) ) {
305
			$langlinks = $p_result->getLanguageLinks();
306
307
			if ( $params['effectivelanglinks'] ) {
308
				// Link flags are ignored for now, but may in the future be
309
				// included in the result.
310
				$linkFlags = [];
311
				Hooks::run( 'LanguageLinks', [ $titleObj, &$langlinks, &$linkFlags ] );
312
			}
313
		} else {
314
			$langlinks = false;
315
		}
316
317
		if ( isset( $prop['langlinks'] ) ) {
318
			$result_array['langlinks'] = $this->formatLangLinks( $langlinks );
319
		}
320
		if ( isset( $prop['categories'] ) ) {
321
			$result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
322
		}
323
		if ( isset( $prop['categorieshtml'] ) ) {
324
			$result_array['categorieshtml'] = $this->categoriesHtml( $p_result->getCategories() );
325
			$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
326
		}
327
		if ( isset( $prop['links'] ) ) {
328
			$result_array['links'] = $this->formatLinks( $p_result->getLinks() );
329
		}
330
		if ( isset( $prop['templates'] ) ) {
331
			$result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
332
		}
333
		if ( isset( $prop['images'] ) ) {
334
			$result_array['images'] = array_keys( $p_result->getImages() );
335
		}
336
		if ( isset( $prop['externallinks'] ) ) {
337
			$result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
338
		}
339
		if ( isset( $prop['sections'] ) ) {
340
			$result_array['sections'] = $p_result->getSections();
341
		}
342
343
		if ( isset( $prop['displaytitle'] ) ) {
344
			$result_array['displaytitle'] = $p_result->getDisplayTitle() ?:
345
				$titleObj->getPrefixedText();
346
		}
347
348
		if ( isset( $prop['headitems'] ) || isset( $prop['headhtml'] ) ) {
349
			$context = $this->getContext();
350
			$context->setTitle( $titleObj );
351
			$context->getOutput()->addParserOutputMetadata( $p_result );
352
353
			if ( isset( $prop['headitems'] ) ) {
354
				$headItems = $this->formatHeadItems( $p_result->getHeadItems() );
355
356
				$css = $this->formatCss( $context->getOutput()->buildCssLinksArray() );
357
358
				$scripts = [ $context->getOutput()->getHeadScripts() ];
359
360
				$result_array['headitems'] = array_merge( $headItems, $css, $scripts );
361
			}
362
363
			if ( isset( $prop['headhtml'] ) ) {
364
				$result_array['headhtml'] = $context->getOutput()->headElement( $context->getSkin() );
365
				$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
366
			}
367
		}
368
369
		if ( isset( $prop['modules'] ) ) {
370
			$result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
371
			$result_array['modulescripts'] = array_values( array_unique( $p_result->getModuleScripts() ) );
372
			$result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
373
			// To be removed in 1.27
374
			$result_array['modulemessages'] = [];
375
			$this->setWarning( 'modulemessages is deprecated since MediaWiki 1.26' );
376
		}
377
378
		if ( isset( $prop['jsconfigvars'] ) ) {
379
			$result_array['jsconfigvars'] =
380
				ApiResult::addMetadataToResultVars( $p_result->getJsConfigVars() );
381
		}
382
383 View Code Duplication
		if ( isset( $prop['encodedjsconfigvars'] ) ) {
384
			$result_array['encodedjsconfigvars'] = FormatJson::encode(
385
				$p_result->getJsConfigVars(), false, FormatJson::ALL_OK
386
			);
387
			$result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
388
		}
389
390 View Code Duplication
		if ( isset( $prop['modules'] ) &&
391
			!isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
392
			$this->setWarning( 'Property "modules" was set but not "jsconfigvars" ' .
393
				'or "encodedjsconfigvars". Configuration variables are necessary ' .
394
				'for proper module usage.' );
395
		}
396
397
		if ( isset( $prop['indicators'] ) ) {
398
			$result_array['indicators'] = (array)$p_result->getIndicators();
399
			ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
400
		}
401
402
		if ( isset( $prop['iwlinks'] ) ) {
403
			$result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
404
		}
405
406
		if ( isset( $prop['wikitext'] ) ) {
407
			$result_array['wikitext'] = $this->content->serialize( $format );
408
			$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
409
			if ( !is_null( $this->pstContent ) ) {
410
				$result_array['psttext'] = $this->pstContent->serialize( $format );
411
				$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
412
			}
413
		}
414
		if ( isset( $prop['properties'] ) ) {
415
			$result_array['properties'] = (array)$p_result->getProperties();
416
			ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
417
		}
418
419
		if ( isset( $prop['limitreportdata'] ) ) {
420
			$result_array['limitreportdata'] =
421
				$this->formatLimitReportData( $p_result->getLimitReportData() );
422
		}
423
		if ( isset( $prop['limitreporthtml'] ) ) {
424
			$result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
425
			$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
426
		}
427
428
		if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
429
			if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
430
				$this->dieUsage( 'parsetree is only supported for wikitext content', 'notwikitext' );
431
			}
432
433
			$wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
434
			$dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
435 View Code Duplication
			if ( is_callable( [ $dom, 'saveXML' ] ) ) {
436
				$xml = $dom->saveXML();
437
			} else {
438
				$xml = $dom->__toString();
439
			}
440
			$result_array['parsetree'] = $xml;
441
			$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
442
		}
443
444
		$result_mapping = [
445
			'redirects' => 'r',
446
			'langlinks' => 'll',
447
			'categories' => 'cl',
448
			'links' => 'pl',
449
			'templates' => 'tl',
450
			'images' => 'img',
451
			'externallinks' => 'el',
452
			'iwlinks' => 'iw',
453
			'sections' => 's',
454
			'headitems' => 'hi',
455
			'modules' => 'm',
456
			'indicators' => 'ind',
457
			'modulescripts' => 'm',
458
			'modulestyles' => 'm',
459
			'modulemessages' => 'm',
460
			'properties' => 'pp',
461
			'limitreportdata' => 'lr',
462
		];
463
		$this->setIndexedTagNames( $result_array, $result_mapping );
464
		$result->addValue( null, $this->getModuleName(), $result_array );
465
	}
466
467
	/**
468
	 * Constructs a ParserOptions object
469
	 *
470
	 * @param WikiPage $pageObj
471
	 * @param array $params
472
	 *
473
	 * @return ParserOptions
474
	 */
475
	protected function makeParserOptions( WikiPage $pageObj, array $params ) {
476
477
		$popts = $pageObj->makeParserOptions( $this->getContext() );
478
		$popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
479
		$popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
480
		$popts->setIsSectionPreview( $params['sectionpreview'] );
481
		$popts->setEditSection( !$params['disableeditsection'] );
482
		if ( $params['disabletidy'] ) {
483
			$popts->setTidy( false );
484
		}
485
486
		return $popts;
487
	}
488
489
	/**
490
	 * @param WikiPage $page
491
	 * @param ParserOptions $popts
492
	 * @param int $pageId
493
	 * @param bool $getWikitext
494
	 * @return ParserOutput
495
	 */
496
	private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) {
497
		$this->content = $this->getContent( $page, $pageId );
498
499
		if ( $this->section !== false && $this->content !== null ) {
500
			// Not cached (save or load)
501
			return $this->content->getParserOutput( $page->getTitle(), null, $popts );
502
		}
503
504
		// Try the parser cache first
505
		// getParserOutput will save to Parser cache if able
506
		$pout = $page->getParserOutput( $popts );
507
		if ( !$pout ) {
508
			$this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
509
		}
510
		if ( $getWikitext ) {
511
			$this->content = $page->getContent( Revision::RAW );
512
		}
513
514
		return $pout;
515
	}
516
517
	/**
518
	 * Get the content for the given page and the requested section.
519
	 *
520
	 * @param WikiPage $page
521
	 * @param int $pageId
522
	 * @return Content
523
	 */
524
	private function getContent( WikiPage $page, $pageId = null ) {
525
		$content = $page->getContent( Revision::RAW ); // XXX: really raw?
526
527
		if ( $this->section !== false && $content !== null ) {
528
			$content = $this->getSectionContent(
529
				$content,
530
				!is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText()
531
			);
532
		}
533
		return $content;
534
	}
535
536
	/**
537
	 * Extract the requested section from the given Content
538
	 *
539
	 * @param Content $content
540
	 * @param string $what Identifies the content in error messages, e.g. page title.
541
	 * @return Content|bool
542
	 */
543
	private function getSectionContent( Content $content, $what ) {
544
		// Not cached (save or load)
545
		$section = $content->getSection( $this->section );
546
		if ( $section === false ) {
547
			$this->dieUsage( "There is no section {$this->section} in $what", 'nosuchsection' );
548
		}
549
		if ( $section === null ) {
550
			$this->dieUsage( "Sections are not supported by $what", 'nosuchsection' );
551
			$section = false;
552
		}
553
554
		return $section;
555
	}
556
557
	/**
558
	 * This mimicks the behavior of EditPage in formatting a summary
559
	 *
560
	 * @param Title $title of the page being parsed
561
	 * @param Array $params the API parameters of the request
562
	 * @return Content|bool
563
	 */
564
	private function formatSummary( $title, $params ) {
565
		global $wgParser;
566
		$summary = !is_null( $params['summary'] ) ? $params['summary'] : '';
567
		$sectionTitle = !is_null( $params['sectiontitle'] ) ? $params['sectiontitle'] : '';
568
569
		if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
570
			if ( $sectionTitle !== '' ) {
571
				$summary = $params['sectiontitle'];
572
			}
573
			if ( $summary !== '' ) {
574
				$summary = wfMessage( 'newsectionsummary' )
575
					->rawParams( $wgParser->stripSectionName( $summary ) )
576
						->inContentLanguage()->text();
577
			}
578
		}
579
		return Linker::formatComment( $summary, $title, $this->section === 'new' );
580
	}
581
582
	private function formatLangLinks( $links ) {
583
		$result = [];
584
		foreach ( $links as $link ) {
585
			$entry = [];
586
			$bits = explode( ':', $link, 2 );
587
			$title = Title::newFromText( $link );
588
589
			$entry['lang'] = $bits[0];
590
			if ( $title ) {
591
				$entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
592
				// localised language name in 'uselang' language
593
				$entry['langname'] = Language::fetchLanguageName(
594
					$title->getInterwiki(),
595
					$this->getLanguage()->getCode()
596
				);
597
598
				// native language name
599
				$entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
600
			}
601
			ApiResult::setContentValue( $entry, 'title', $bits[1] );
602
			$result[] = $entry;
603
		}
604
605
		return $result;
606
	}
607
608
	private function formatCategoryLinks( $links ) {
609
		$result = [];
610
611
		if ( !$links ) {
612
			return $result;
613
		}
614
615
		// Fetch hiddencat property
616
		$lb = new LinkBatch;
617
		$lb->setArray( [ NS_CATEGORY => $links ] );
618
		$db = $this->getDB();
619
		$res = $db->select( [ 'page', 'page_props' ],
620
			[ 'page_title', 'pp_propname' ],
621
			$lb->constructSet( 'page', $db ),
0 ignored issues
show
Bug introduced by
It seems like $lb->constructSet('page', $db) targeting LinkBatch::constructSet() can also be of type boolean; however, DatabaseBase::select() does only seem to accept string, 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...
622
			__METHOD__,
623
			[],
624
			[ 'page_props' => [
625
				'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
626
			] ]
627
		);
628
		$hiddencats = [];
629
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res 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...
630
			$hiddencats[$row->page_title] = isset( $row->pp_propname );
631
		}
632
633
		foreach ( $links as $link => $sortkey ) {
634
			$entry = [];
635
			$entry['sortkey'] = $sortkey;
636
			// array keys will cast numeric category names to ints, so cast back to string
637
			ApiResult::setContentValue( $entry, 'category', (string)$link );
638
			if ( !isset( $hiddencats[$link] ) ) {
639
				$entry['missing'] = true;
640
			} elseif ( $hiddencats[$link] ) {
641
				$entry['hidden'] = true;
642
			}
643
			$result[] = $entry;
644
		}
645
646
		return $result;
647
	}
648
649
	private function categoriesHtml( $categories ) {
650
		$context = $this->getContext();
651
		$context->getOutput()->addCategoryLinks( $categories );
652
653
		return $context->getSkin()->getCategories();
654
	}
655
656
	private function formatLinks( $links ) {
657
		$result = [];
658
		foreach ( $links as $ns => $nslinks ) {
659
			foreach ( $nslinks as $title => $id ) {
660
				$entry = [];
661
				$entry['ns'] = $ns;
662
				ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
663
				$entry['exists'] = $id != 0;
664
				$result[] = $entry;
665
			}
666
		}
667
668
		return $result;
669
	}
670
671
	private function formatIWLinks( $iw ) {
672
		$result = [];
673
		foreach ( $iw as $prefix => $titles ) {
674
			foreach ( array_keys( $titles ) as $title ) {
675
				$entry = [];
676
				$entry['prefix'] = $prefix;
677
678
				$title = Title::newFromText( "{$prefix}:{$title}" );
679
				if ( $title ) {
680
					$entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
681
				}
682
683
				ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
684
				$result[] = $entry;
685
			}
686
		}
687
688
		return $result;
689
	}
690
691 View Code Duplication
	private function formatHeadItems( $headItems ) {
692
		$result = [];
693
		foreach ( $headItems as $tag => $content ) {
694
			$entry = [];
695
			$entry['tag'] = $tag;
696
			ApiResult::setContentValue( $entry, 'content', $content );
697
			$result[] = $entry;
698
		}
699
700
		return $result;
701
	}
702
703 View Code Duplication
	private function formatCss( $css ) {
704
		$result = [];
705
		foreach ( $css as $file => $link ) {
706
			$entry = [];
707
			$entry['file'] = $file;
708
			ApiResult::setContentValue( $entry, 'link', $link );
709
			$result[] = $entry;
710
		}
711
712
		return $result;
713
	}
714
715
	private function formatLimitReportData( $limitReportData ) {
716
		$result = [];
717
718
		foreach ( $limitReportData as $name => $value ) {
719
			$entry = [];
720
			$entry['name'] = $name;
721
			if ( !is_array( $value ) ) {
722
				$value = [ $value ];
723
			}
724
			ApiResult::setIndexedTagNameRecursive( $value, 'param' );
725
			$entry = array_merge( $entry, $value );
726
			$result[] = $entry;
727
		}
728
729
		return $result;
730
	}
731
732
	private function setIndexedTagNames( &$array, $mapping ) {
733
		foreach ( $mapping as $key => $name ) {
734
			if ( isset( $array[$key] ) ) {
735
				ApiResult::setIndexedTagName( $array[$key], $name );
736
			}
737
		}
738
	}
739
740
	public function getAllowedParams() {
741
		return [
742
			'title' => null,
743
			'text' => [
744
				ApiBase::PARAM_TYPE => 'text',
745
			],
746
			'summary' => null,
747
			'page' => null,
748
			'pageid' => [
749
				ApiBase::PARAM_TYPE => 'integer',
750
			],
751
			'redirects' => false,
752
			'oldid' => [
753
				ApiBase::PARAM_TYPE => 'integer',
754
			],
755
			'prop' => [
756
				ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
757
					'images|externallinks|sections|revid|displaytitle|iwlinks|properties',
758
				ApiBase::PARAM_ISMULTI => true,
759
				ApiBase::PARAM_TYPE => [
760
					'text',
761
					'langlinks',
762
					'categories',
763
					'categorieshtml',
764
					'links',
765
					'templates',
766
					'images',
767
					'externallinks',
768
					'sections',
769
					'revid',
770
					'displaytitle',
771
					'headitems',
772
					'headhtml',
773
					'modules',
774
					'jsconfigvars',
775
					'encodedjsconfigvars',
776
					'indicators',
777
					'iwlinks',
778
					'wikitext',
779
					'properties',
780
					'limitreportdata',
781
					'limitreporthtml',
782
					'parsetree',
783
				],
784
				ApiBase::PARAM_HELP_MSG_PER_VALUE => [
785
					'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
786
				],
787
			],
788
			'pst' => false,
789
			'onlypst' => false,
790
			'effectivelanglinks' => false,
791
			'section' => null,
792
			'sectiontitle' => [
793
				ApiBase::PARAM_TYPE => 'string',
794
			],
795
			'disablepp' => [
796
				ApiBase::PARAM_DFLT => false,
797
				ApiBase::PARAM_DEPRECATED => true,
798
			],
799
			'disablelimitreport' => false,
800
			'disableeditsection' => false,
801
			'disabletidy' => false,
802
			'generatexml' => [
803
				ApiBase::PARAM_DFLT => false,
804
				ApiBase::PARAM_HELP_MSG => [
805
					'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
806
				],
807
				ApiBase::PARAM_DEPRECATED => true,
808
			],
809
			'preview' => false,
810
			'sectionpreview' => false,
811
			'disabletoc' => false,
812
			'contentformat' => [
813
				ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
814
			],
815
			'contentmodel' => [
816
				ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
817
			]
818
		];
819
	}
820
821
	protected function getExamplesMessages() {
822
		return [
823
			'action=parse&page=Project:Sandbox'
824
				=> 'apihelp-parse-example-page',
825
			'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
826
				=> 'apihelp-parse-example-text',
827
			'action=parse&text={{PAGENAME}}&title=Test'
828
				=> 'apihelp-parse-example-texttitle',
829
			'action=parse&summary=Some+[[link]]&prop='
830
				=> 'apihelp-parse-example-summary',
831
		];
832
	}
833
834
	public function getHelpUrls() {
835
		return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#parse';
836
	}
837
}
838