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

includes/api/ApiQueryRevisionsBase.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
 *
4
 *
5
 * Created on Oct 3, 2014 as a split from ApiQueryRevisions
6
 *
7
 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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
 */
26
27
/**
28
 * A base class for functions common to producing a list of revisions.
29
 *
30
 * @ingroup API
31
 */
32
abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
33
34
	protected $limit, $diffto, $difftotext, $difftotextpst, $expandTemplates, $generateXML,
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
35
		$section, $parseContent, $fetchContent, $contentFormat, $setParsedLimit = true;
36
37
	protected $fld_ids = false, $fld_flags = false, $fld_timestamp = false,
38
		$fld_size = false, $fld_sha1 = false, $fld_comment = false,
39
		$fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
40
		$fld_content = false, $fld_tags = false, $fld_contentmodel = false, $fld_parsetree = false;
41
42
	public function execute() {
43
		$this->run();
44
	}
45
46
	public function executeGenerator( $resultPageSet ) {
47
		$this->run( $resultPageSet );
48
	}
49
50
	/**
51
	 * @param ApiPageSet $resultPageSet
52
	 * @return void
53
	 */
54
	abstract protected function run( ApiPageSet $resultPageSet = null );
55
56
	/**
57
	 * Parse the parameters into the various instance fields.
58
	 *
59
	 * @param array $params
60
	 */
61
	protected function parseParameters( $params ) {
62
		if ( !is_null( $params['difftotext'] ) ) {
63
			$this->difftotext = $params['difftotext'];
64
			$this->difftotextpst = $params['difftotextpst'];
65
		} elseif ( !is_null( $params['diffto'] ) ) {
66
			if ( $params['diffto'] == 'cur' ) {
67
				$params['diffto'] = 0;
68
			}
69
			if ( ( !ctype_digit( $params['diffto'] ) || $params['diffto'] < 0 )
70
				&& $params['diffto'] != 'prev' && $params['diffto'] != 'next'
71
			) {
72
				$p = $this->getModulePrefix();
73
				$this->dieUsage(
74
					"{$p}diffto must be set to a non-negative number, \"prev\", \"next\" or \"cur\"",
75
					'diffto'
76
				);
77
			}
78
			// Check whether the revision exists and is readable,
79
			// DifferenceEngine returns a rather ambiguous empty
80
			// string if that's not the case
81
			if ( $params['diffto'] != 0 ) {
82
				$difftoRev = Revision::newFromId( $params['diffto'] );
83
				if ( !$difftoRev ) {
84
					$this->dieUsageMsg( [ 'nosuchrevid', $params['diffto'] ] );
85
				}
86
				if ( !$difftoRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
87
					$this->setWarning( "Couldn't diff to r{$difftoRev->getId()}: content is hidden" );
88
					$params['diffto'] = null;
89
				}
90
			}
91
			$this->diffto = $params['diffto'];
92
		}
93
94
		$prop = array_flip( $params['prop'] );
95
96
		$this->fld_ids = isset( $prop['ids'] );
97
		$this->fld_flags = isset( $prop['flags'] );
98
		$this->fld_timestamp = isset( $prop['timestamp'] );
99
		$this->fld_comment = isset( $prop['comment'] );
100
		$this->fld_parsedcomment = isset( $prop['parsedcomment'] );
101
		$this->fld_size = isset( $prop['size'] );
102
		$this->fld_sha1 = isset( $prop['sha1'] );
103
		$this->fld_content = isset( $prop['content'] );
104
		$this->fld_contentmodel = isset( $prop['contentmodel'] );
105
		$this->fld_userid = isset( $prop['userid'] );
106
		$this->fld_user = isset( $prop['user'] );
107
		$this->fld_tags = isset( $prop['tags'] );
108
		$this->fld_parsetree = isset( $prop['parsetree'] );
109
110
		if ( !empty( $params['contentformat'] ) ) {
111
			$this->contentFormat = $params['contentformat'];
112
		}
113
114
		$this->limit = $params['limit'];
115
116
		$this->fetchContent = $this->fld_content || !is_null( $this->diffto )
117
			|| !is_null( $this->difftotext ) || $this->fld_parsetree;
118
119
		$smallLimit = false;
120
		if ( $this->fetchContent ) {
121
			$smallLimit = true;
122
			$this->expandTemplates = $params['expandtemplates'];
123
			$this->generateXML = $params['generatexml'];
124
			$this->parseContent = $params['parse'];
125
			if ( $this->parseContent ) {
126
				// Must manually initialize unset limit
127
				if ( is_null( $this->limit ) ) {
128
					$this->limit = 1;
129
				}
130
			}
131
			if ( isset( $params['section'] ) ) {
132
				$this->section = $params['section'];
133
			} else {
134
				$this->section = false;
135
			}
136
		}
137
138
		$userMax = $this->parseContent ? 1 : ( $smallLimit ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
139
		$botMax = $this->parseContent ? 1 : ( $smallLimit ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
140
		if ( $this->limit == 'max' ) {
141
			$this->limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
142
			if ( $this->setParsedLimit ) {
143
				$this->getResult()->addParsedLimit( $this->getModuleName(), $this->limit );
144
			}
145
		}
146
147
		if ( is_null( $this->limit ) ) {
148
			$this->limit = 10;
149
		}
150
		$this->validateLimit( 'limit', $this->limit, 1, $userMax, $botMax );
151
	}
152
153
	/**
154
	 * Extract information from the Revision
155
	 *
156
	 * @param Revision $revision
157
	 * @param object $row Should have a field 'ts_tags' if $this->fld_tags is set
158
	 * @return array
159
	 */
160
	protected function extractRevisionInfo( Revision $revision, $row ) {
161
		$title = $revision->getTitle();
162
		$user = $this->getUser();
163
		$vals = [];
164
		$anyHidden = false;
165
166
		if ( $this->fld_ids ) {
167
			$vals['revid'] = intval( $revision->getId() );
168
			if ( !is_null( $revision->getParentId() ) ) {
169
				$vals['parentid'] = intval( $revision->getParentId() );
170
			}
171
		}
172
173
		if ( $this->fld_flags ) {
174
			$vals['minor'] = $revision->isMinor();
175
		}
176
177
		if ( $this->fld_user || $this->fld_userid ) {
178
			if ( $revision->isDeleted( Revision::DELETED_USER ) ) {
179
				$vals['userhidden'] = true;
180
				$anyHidden = true;
181
			}
182
			if ( $revision->userCan( Revision::DELETED_USER, $user ) ) {
183
				if ( $this->fld_user ) {
184
					$vals['user'] = $revision->getUserText( Revision::RAW );
185
				}
186
				$userid = $revision->getUser( Revision::RAW );
187
				if ( !$userid ) {
188
					$vals['anon'] = true;
189
				}
190
191
				if ( $this->fld_userid ) {
192
					$vals['userid'] = $userid;
193
				}
194
			}
195
		}
196
197
		if ( $this->fld_timestamp ) {
198
			$vals['timestamp'] = wfTimestamp( TS_ISO_8601, $revision->getTimestamp() );
199
		}
200
201
		if ( $this->fld_size ) {
202
			if ( !is_null( $revision->getSize() ) ) {
203
				$vals['size'] = intval( $revision->getSize() );
204
			} else {
205
				$vals['size'] = 0;
206
			}
207
		}
208
209
		if ( $this->fld_sha1 ) {
210
			if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
211
				$vals['sha1hidden'] = true;
212
				$anyHidden = true;
213
			}
214
			if ( $revision->userCan( Revision::DELETED_TEXT, $user ) ) {
215
				if ( $revision->getSha1() != '' ) {
216
					$vals['sha1'] = Wikimedia\base_convert( $revision->getSha1(), 36, 16, 40 );
217
				} else {
218
					$vals['sha1'] = '';
219
				}
220
			}
221
		}
222
223
		if ( $this->fld_contentmodel ) {
224
			$vals['contentmodel'] = $revision->getContentModel();
225
		}
226
227
		if ( $this->fld_comment || $this->fld_parsedcomment ) {
228
			if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) {
229
				$vals['commenthidden'] = true;
230
				$anyHidden = true;
231
			}
232
			if ( $revision->userCan( Revision::DELETED_COMMENT, $user ) ) {
233
				$comment = $revision->getComment( Revision::RAW );
234
235
				if ( $this->fld_comment ) {
236
					$vals['comment'] = $comment;
237
				}
238
239
				if ( $this->fld_parsedcomment ) {
240
					$vals['parsedcomment'] = Linker::formatComment( $comment, $title );
241
				}
242
			}
243
		}
244
245 View Code Duplication
		if ( $this->fld_tags ) {
246
			if ( $row->ts_tags ) {
247
				$tags = explode( ',', $row->ts_tags );
248
				ApiResult::setIndexedTagName( $tags, 'tag' );
249
				$vals['tags'] = $tags;
250
			} else {
251
				$vals['tags'] = [];
252
			}
253
		}
254
255
		$content = null;
256
		global $wgParser;
257
		if ( $this->fetchContent ) {
258
			$content = $revision->getContent( Revision::FOR_THIS_USER, $this->getUser() );
259
			// Expand templates after getting section content because
260
			// template-added sections don't count and Parser::preprocess()
261
			// will have less input
262
			if ( $content && $this->section !== false ) {
263
				$content = $content->getSection( $this->section, false );
264
				if ( !$content ) {
265
					$this->dieUsage(
266
						"There is no section {$this->section} in r" . $revision->getId(),
267
						'nosuchsection'
268
					);
269
				}
270
			}
271
			if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
272
				$vals['texthidden'] = true;
273
				$anyHidden = true;
274
			} elseif ( !$content ) {
275
				$vals['textmissing'] = true;
276
			}
277
		}
278
		if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) {
279
			if ( $content ) {
280
				if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
281
					$t = $content->getNativeData(); # note: don't set $text
282
283
					$wgParser->startExternalParse(
284
						$title,
285
						ParserOptions::newFromContext( $this->getContext() ),
286
						Parser::OT_PREPROCESS
287
					);
288
					$dom = $wgParser->preprocessToDom( $t );
289 View Code Duplication
					if ( is_callable( [ $dom, 'saveXML' ] ) ) {
290
						$xml = $dom->saveXML();
291
					} else {
292
						$xml = $dom->__toString();
293
					}
294
					$vals['parsetree'] = $xml;
295 View Code Duplication
				} else {
296
					$vals['badcontentformatforparsetree'] = true;
297
					$this->setWarning( 'Conversion to XML is supported for wikitext only, ' .
298
						$title->getPrefixedDBkey() .
299
						' uses content model ' . $content->getModel() );
300
				}
301
			}
302
		}
303
304
		if ( $this->fld_content && $content ) {
305
			$text = null;
306
307
			if ( $this->expandTemplates && !$this->parseContent ) {
308
				# XXX: implement template expansion for all content types in ContentHandler?
309
				if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
310
					$text = $content->getNativeData();
311
312
					$text = $wgParser->preprocess(
313
						$text,
314
						$title,
315
						ParserOptions::newFromContext( $this->getContext() )
316
					);
317 View Code Duplication
				} else {
318
					$this->setWarning( 'Template expansion is supported for wikitext only, ' .
319
						$title->getPrefixedDBkey() .
320
						' uses content model ' . $content->getModel() );
321
					$vals['badcontentformat'] = true;
322
					$text = false;
323
				}
324
			}
325
			if ( $this->parseContent ) {
326
				$po = $content->getParserOutput(
327
					$title,
328
					$revision->getId(),
329
					ParserOptions::newFromContext( $this->getContext() )
330
				);
331
				$text = $po->getText();
332
			}
333
334
			if ( $text === null ) {
335
				$format = $this->contentFormat ?: $content->getDefaultFormat();
336
				$model = $content->getModel();
337
338
				if ( !$content->isSupportedFormat( $format ) ) {
339
					$name = $title->getPrefixedDBkey();
340
					$this->setWarning( "The requested format {$this->contentFormat} is not " .
341
						"supported for content model $model used by $name" );
342
					$vals['badcontentformat'] = true;
343
					$text = false;
344
				} else {
345
					$text = $content->serialize( $format );
346
					// always include format and model.
347
					// Format is needed to deserialize, model is needed to interpret.
348
					$vals['contentformat'] = $format;
349
					$vals['contentmodel'] = $model;
350
				}
351
			}
352
353
			if ( $text !== false ) {
354
				ApiResult::setContentValue( $vals, 'content', $text );
355
			}
356
		}
357
358
		if ( $content && ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) ) {
359
			static $n = 0; // Number of uncached diffs we've had
360
361
			if ( $n < $this->getConfig()->get( 'APIMaxUncachedDiffs' ) ) {
362
				$vals['diff'] = [];
363
				$context = new DerivativeContext( $this->getContext() );
364
				$context->setTitle( $title );
365
				$handler = $revision->getContentHandler();
366
367
				if ( !is_null( $this->difftotext ) ) {
368
					$model = $title->getContentModel();
369
370
					if ( $this->contentFormat
371
						&& !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat )
372
					) {
373
						$name = $title->getPrefixedDBkey();
374
						$this->setWarning( "The requested format {$this->contentFormat} is not " .
375
							"supported for content model $model used by $name" );
376
						$vals['diff']['badcontentformat'] = true;
377
						$engine = null;
378
					} else {
379
						$difftocontent = ContentHandler::makeContent(
380
							$this->difftotext,
381
							$title,
382
							$model,
383
							$this->contentFormat
384
						);
385
386
						if ( $this->difftotextpst ) {
387
							$popts = ParserOptions::newFromContext( $this->getContext() );
388
							$difftocontent = $difftocontent->preSaveTransform( $title, $user, $popts );
0 ignored issues
show
It seems like $title defined by $revision->getTitle() on line 161 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...
389
						}
390
391
						$engine = $handler->createDifferenceEngine( $context );
392
						$engine->setContent( $content, $difftocontent );
393
					}
394
				} else {
395
					$engine = $handler->createDifferenceEngine( $context, $revision->getId(), $this->diffto );
396
					$vals['diff']['from'] = $engine->getOldid();
397
					$vals['diff']['to'] = $engine->getNewid();
398
				}
399
				if ( $engine ) {
400
					$difftext = $engine->getDiffBody();
401
					ApiResult::setContentValue( $vals['diff'], 'body', $difftext );
402
					if ( !$engine->wasCacheHit() ) {
403
						$n++;
404
					}
405
				}
406
			} else {
407
				$vals['diff']['notcached'] = true;
408
			}
409
		}
410
411
		if ( $anyHidden && $revision->isDeleted( Revision::DELETED_RESTRICTED ) ) {
412
			$vals['suppressed'] = true;
413
		}
414
415
		return $vals;
416
	}
417
418
	public function getCacheMode( $params ) {
419
		if ( $this->userCanSeeRevDel() ) {
420
			return 'private';
421
		}
422
423
		return 'public';
424
	}
425
426
	public function getAllowedParams() {
427
		return [
428
			'prop' => [
429
				ApiBase::PARAM_ISMULTI => true,
430
				ApiBase::PARAM_DFLT => 'ids|timestamp|flags|comment|user',
431
				ApiBase::PARAM_TYPE => [
432
					'ids',
433
					'flags',
434
					'timestamp',
435
					'user',
436
					'userid',
437
					'size',
438
					'sha1',
439
					'contentmodel',
440
					'comment',
441
					'parsedcomment',
442
					'content',
443
					'tags',
444
					'parsetree',
445
				],
446
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-prop',
447
				ApiBase::PARAM_HELP_MSG_PER_VALUE => [
448
					'ids' => 'apihelp-query+revisions+base-paramvalue-prop-ids',
449
					'flags' => 'apihelp-query+revisions+base-paramvalue-prop-flags',
450
					'timestamp' => 'apihelp-query+revisions+base-paramvalue-prop-timestamp',
451
					'user' => 'apihelp-query+revisions+base-paramvalue-prop-user',
452
					'userid' => 'apihelp-query+revisions+base-paramvalue-prop-userid',
453
					'size' => 'apihelp-query+revisions+base-paramvalue-prop-size',
454
					'sha1' => 'apihelp-query+revisions+base-paramvalue-prop-sha1',
455
					'contentmodel' => 'apihelp-query+revisions+base-paramvalue-prop-contentmodel',
456
					'comment' => 'apihelp-query+revisions+base-paramvalue-prop-comment',
457
					'parsedcomment' => 'apihelp-query+revisions+base-paramvalue-prop-parsedcomment',
458
					'content' => 'apihelp-query+revisions+base-paramvalue-prop-content',
459
					'tags' => 'apihelp-query+revisions+base-paramvalue-prop-tags',
460
					'parsetree' => [ 'apihelp-query+revisions+base-paramvalue-prop-parsetree',
461
						CONTENT_MODEL_WIKITEXT ],
462
				],
463
			],
464
			'limit' => [
465
				ApiBase::PARAM_TYPE => 'limit',
466
				ApiBase::PARAM_MIN => 1,
467
				ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
468
				ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
469
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-limit',
470
			],
471
			'expandtemplates' => [
472
				ApiBase::PARAM_DFLT => false,
473
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-expandtemplates',
474
			],
475
			'generatexml' => [
476
				ApiBase::PARAM_DFLT => false,
477
				ApiBase::PARAM_DEPRECATED => true,
478
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-generatexml',
479
			],
480
			'parse' => [
481
				ApiBase::PARAM_DFLT => false,
482
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-parse',
483
			],
484
			'section' => [
485
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-section',
486
			],
487
			'diffto' => [
488
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-diffto',
489
			],
490
			'difftotext' => [
491
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotext',
492
			],
493
			'difftotextpst' => [
494
				ApiBase::PARAM_DFLT => false,
495
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotextpst',
496
			],
497
			'contentformat' => [
498
				ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
499
				ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat',
500
			],
501
		];
502
	}
503
504
}
505