Merge "Rename autonym for 'no' from 'norsk bokmål' to 'norsk'"
[lhc/web/wiklou.git] / includes / api / ApiComparePages.php
1 <?php
2 /**
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @file
20 */
21
22 class ApiComparePages extends ApiBase {
23
24 private $guessed = false, $guessedTitle, $guessedModel, $props;
25
26 public function execute() {
27 $params = $this->extractRequestParams();
28
29 // Parameter validation
30 $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
31 $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
32
33 $this->props = array_flip( $params['prop'] );
34
35 // Cache responses publicly by default. This may be overridden later.
36 $this->getMain()->setCacheMode( 'public' );
37
38 // Get the 'from' Revision and Content
39 list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params );
40
41 // Get the 'to' Revision and Content
42 if ( $params['torelative'] !== null ) {
43 if ( !$relRev ) {
44 $this->dieWithError( 'apierror-compare-relative-to-nothing' );
45 }
46 switch ( $params['torelative'] ) {
47 case 'prev':
48 // Swap 'from' and 'to'
49 $toRev = $fromRev;
50 $toContent = $fromContent;
51 $fromRev = $relRev->getPrevious();
52 $fromContent = $fromRev
53 ? $fromRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
54 : $toContent->getContentHandler()->makeEmptyContent();
55 if ( !$fromContent ) {
56 $this->dieWithError(
57 [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent'
58 );
59 }
60 break;
61
62 case 'next':
63 $toRev = $relRev->getNext();
64 $toContent = $toRev
65 ? $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
66 : $fromContent;
67 if ( !$toContent ) {
68 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
69 }
70 break;
71
72 case 'cur':
73 $title = $relRev->getTitle();
74 $id = $title->getLatestRevID();
75 $toRev = $id ? Revision::newFromId( $id ) : null;
76 if ( !$toRev ) {
77 $this->dieWithError(
78 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
79 );
80 }
81 $toContent = $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
82 if ( !$toContent ) {
83 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
84 }
85 break;
86 }
87 $relRev2 = null;
88 } else {
89 list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params );
90 }
91
92 // Should never happen, but just in case...
93 if ( !$fromContent || !$toContent ) {
94 $this->dieWithError( 'apierror-baddiff' );
95 }
96
97 // Get the diff
98 $context = new DerivativeContext( $this->getContext() );
99 if ( $relRev && $relRev->getTitle() ) {
100 $context->setTitle( $relRev->getTitle() );
101 } elseif ( $relRev2 && $relRev2->getTitle() ) {
102 $context->setTitle( $relRev2->getTitle() );
103 } else {
104 $this->guessTitleAndModel();
105 if ( $this->guessedTitle ) {
106 $context->setTitle( $this->guessedTitle );
107 }
108 }
109 $de = $fromContent->getContentHandler()->createDifferenceEngine(
110 $context,
111 $fromRev ? $fromRev->getId() : 0,
112 $toRev ? $toRev->getId() : 0,
113 /* $rcid = */ null,
114 /* $refreshCache = */ false,
115 /* $unhide = */ true
116 );
117 $de->setContent( $fromContent, $toContent );
118 $difftext = $de->getDiffBody();
119 if ( $difftext === false ) {
120 $this->dieWithError( 'apierror-baddiff' );
121 }
122
123 // Fill in the response
124 $vals = [];
125 $this->setVals( $vals, 'from', $fromRev );
126 $this->setVals( $vals, 'to', $toRev );
127
128 if ( isset( $this->props['rel'] ) ) {
129 if ( $fromRev ) {
130 $rev = $fromRev->getPrevious();
131 if ( $rev ) {
132 $vals['prev'] = $rev->getId();
133 }
134 }
135 if ( $toRev ) {
136 $rev = $toRev->getNext();
137 if ( $rev ) {
138 $vals['next'] = $rev->getId();
139 }
140 }
141 }
142
143 if ( isset( $this->props['diffsize'] ) ) {
144 $vals['diffsize'] = strlen( $difftext );
145 }
146 if ( isset( $this->props['diff'] ) ) {
147 ApiResult::setContentValue( $vals, 'body', $difftext );
148 }
149
150 $this->getResult()->addValue( null, $this->getModuleName(), $vals );
151 }
152
153 /**
154 * Guess an appropriate default Title and content model for this request
155 *
156 * Fills in $this->guessedTitle based on the first of 'fromrev',
157 * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
158 * valid.
159 *
160 * Fills in $this->guessedModel based on the Revision or Title used to
161 * determine $this->guessedTitle, or the 'fromcontentmodel' or
162 * 'tocontentmodel' parameters if no title was guessed.
163 */
164 private function guessTitleAndModel() {
165 if ( $this->guessed ) {
166 return;
167 }
168
169 $this->guessed = true;
170 $params = $this->extractRequestParams();
171
172 foreach ( [ 'from', 'to' ] as $prefix ) {
173 if ( $params["{$prefix}rev"] !== null ) {
174 $revId = $params["{$prefix}rev"];
175 $rev = Revision::newFromId( $revId );
176 if ( !$rev ) {
177 // Titles of deleted revisions aren't secret, per T51088
178 $row = $this->getDB()->selectRow(
179 'archive',
180 array_merge(
181 Revision::selectArchiveFields(),
182 [ 'ar_namespace', 'ar_title' ]
183 ),
184 [ 'ar_rev_id' => $revId ],
185 __METHOD__
186 );
187 if ( $row ) {
188 $rev = Revision::newFromArchiveRow( $row );
189 }
190 }
191 if ( $rev ) {
192 $this->guessedTitle = $rev->getTitle();
193 $this->guessedModel = $rev->getContentModel();
194 break;
195 }
196 }
197
198 if ( $params["{$prefix}title"] !== null ) {
199 $title = Title::newFromText( $params["{$prefix}title"] );
200 if ( $title && !$title->isExternal() ) {
201 $this->guessedTitle = $title;
202 break;
203 }
204 }
205
206 if ( $params["{$prefix}id"] !== null ) {
207 $title = Title::newFromID( $params["{$prefix}id"] );
208 if ( $title ) {
209 $this->guessedTitle = $title;
210 break;
211 }
212 }
213 }
214
215 if ( !$this->guessedModel ) {
216 if ( $this->guessedTitle ) {
217 $this->guessedModel = $this->guessedTitle->getContentModel();
218 } elseif ( $params['fromcontentmodel'] !== null ) {
219 $this->guessedModel = $params['fromcontentmodel'];
220 } elseif ( $params['tocontentmodel'] !== null ) {
221 $this->guessedModel = $params['tocontentmodel'];
222 }
223 }
224 }
225
226 /**
227 * Get the Revision and Content for one side of the diff
228 *
229 * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
230 * 'contentmodel', and 'contentformat' parameters to determine what content
231 * should be diffed.
232 *
233 * Returns three values:
234 * - The revision used to retrieve the content, if any
235 * - The content to be diffed
236 * - The revision specified, if any, even if not used to retrieve the
237 * Content
238 *
239 * @param string $prefix 'from' or 'to'
240 * @param array $params
241 * @return array [ Revision|null, Content, Revision|null ]
242 */
243 private function getDiffContent( $prefix, array $params ) {
244 $title = null;
245 $rev = null;
246 $suppliedContent = $params["{$prefix}text"] !== null;
247
248 // Get the revision and title, if applicable
249 $revId = null;
250 if ( $params["{$prefix}rev"] !== null ) {
251 $revId = $params["{$prefix}rev"];
252 } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
253 if ( $params["{$prefix}title"] !== null ) {
254 $title = Title::newFromText( $params["{$prefix}title"] );
255 if ( !$title || $title->isExternal() ) {
256 $this->dieWithError(
257 [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
258 );
259 }
260 } else {
261 $title = Title::newFromID( $params["{$prefix}id"] );
262 if ( !$title ) {
263 $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
264 }
265 }
266 $revId = $title->getLatestRevID();
267 if ( !$revId ) {
268 $revId = null;
269 // Only die here if we're not using supplied text
270 if ( !$suppliedContent ) {
271 if ( $title->exists() ) {
272 $this->dieWithError(
273 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
274 );
275 } else {
276 $this->dieWithError(
277 [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
278 'missingtitle'
279 );
280 }
281 }
282 }
283 }
284 if ( $revId !== null ) {
285 $rev = Revision::newFromId( $revId );
286 if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
287 // Try the 'archive' table
288 $row = $this->getDB()->selectRow(
289 'archive',
290 array_merge(
291 Revision::selectArchiveFields(),
292 [ 'ar_namespace', 'ar_title' ]
293 ),
294 [ 'ar_rev_id' => $revId ],
295 __METHOD__
296 );
297 if ( $row ) {
298 $rev = Revision::newFromArchiveRow( $row );
299 $rev->isArchive = true;
300 }
301 }
302 if ( !$rev ) {
303 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
304 }
305 $title = $rev->getTitle();
306
307 // If we don't have supplied content, return here. Otherwise,
308 // continue on below with the supplied content.
309 if ( !$suppliedContent ) {
310 $content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
311 if ( !$content ) {
312 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
313 }
314 return [ $rev, $content, $rev ];
315 }
316 }
317
318 // Override $content based on supplied text
319 $model = $params["{$prefix}contentmodel"];
320 $format = $params["{$prefix}contentformat"];
321
322 if ( !$model && $rev ) {
323 $model = $rev->getContentModel();
324 }
325 if ( !$model && $title ) {
326 $model = $title->getContentModel();
327 }
328 if ( !$model ) {
329 $this->guessTitleAndModel();
330 $model = $this->guessedModel;
331 }
332 if ( !$model ) {
333 $model = CONTENT_MODEL_WIKITEXT;
334 $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
335 }
336
337 if ( !$title ) {
338 $this->guessTitleAndModel();
339 $title = $this->guessedTitle;
340 }
341
342 try {
343 $content = ContentHandler::makeContent( $params["{$prefix}text"], $title, $model, $format );
344 } catch ( MWContentSerializationException $ex ) {
345 $this->dieWithException( $ex, [
346 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
347 ] );
348 }
349
350 if ( $params["{$prefix}pst"] ) {
351 if ( !$title ) {
352 $this->dieWithError( 'apierror-compare-no-title' );
353 }
354 $popts = ParserOptions::newFromContext( $this->getContext() );
355 $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
356 }
357
358 return [ null, $content, $rev ];
359 }
360
361 /**
362 * Set value fields from a Revision object
363 * @param array &$vals Result array to set data into
364 * @param string $prefix 'from' or 'to'
365 * @param Revision|null $rev
366 */
367 private function setVals( &$vals, $prefix, $rev ) {
368 if ( $rev ) {
369 $title = $rev->getTitle();
370 if ( isset( $this->props['ids'] ) ) {
371 $vals["{$prefix}id"] = $title->getArticleId();
372 $vals["{$prefix}revid"] = $rev->getId();
373 }
374 if ( isset( $this->props['title'] ) ) {
375 ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
376 }
377 if ( isset( $this->props['size'] ) ) {
378 $vals["{$prefix}size"] = $rev->getSize();
379 }
380
381 $anyHidden = false;
382 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
383 $vals["{$prefix}texthidden"] = true;
384 $anyHidden = true;
385 }
386
387 if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
388 $vals["{$prefix}userhidden"] = true;
389 $anyHidden = true;
390 }
391 if ( isset( $this->props['user'] ) &&
392 $rev->userCan( Revision::DELETED_USER, $this->getUser() )
393 ) {
394 $vals["{$prefix}user"] = $rev->getUserText( Revision::RAW );
395 $vals["{$prefix}userid"] = $rev->getUser( Revision::RAW );
396 }
397
398 if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
399 $vals["{$prefix}commenthidden"] = true;
400 $anyHidden = true;
401 }
402 if ( $rev->userCan( Revision::DELETED_COMMENT, $this->getUser() ) ) {
403 if ( isset( $this->props['comment'] ) ) {
404 $vals["{$prefix}comment"] = $rev->getComment( Revision::RAW );
405 }
406 if ( isset( $this->props['parsedcomment'] ) ) {
407 $vals["{$prefix}parsedcomment"] = Linker::formatComment(
408 $rev->getComment( Revision::RAW ),
409 $rev->getTitle()
410 );
411 }
412 }
413
414 if ( $anyHidden ) {
415 $this->getMain()->setCacheMode( 'private' );
416 if ( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
417 $vals["{$prefix}suppressed"] = true;
418 }
419 }
420
421 if ( !empty( $rev->isArchive ) ) {
422 $this->getMain()->setCacheMode( 'private' );
423 $vals["{$prefix}archive"] = true;
424 }
425 }
426 }
427
428 public function getAllowedParams() {
429 // Parameters for the 'from' and 'to' content
430 $fromToParams = [
431 'title' => null,
432 'id' => [
433 ApiBase::PARAM_TYPE => 'integer'
434 ],
435 'rev' => [
436 ApiBase::PARAM_TYPE => 'integer'
437 ],
438 'text' => [
439 ApiBase::PARAM_TYPE => 'text'
440 ],
441 'pst' => false,
442 'contentformat' => [
443 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
444 ],
445 'contentmodel' => [
446 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
447 ]
448 ];
449
450 $ret = [];
451 foreach ( $fromToParams as $k => $v ) {
452 $ret["from$k"] = $v;
453 }
454 foreach ( $fromToParams as $k => $v ) {
455 $ret["to$k"] = $v;
456 }
457
458 $ret = wfArrayInsertAfter(
459 $ret,
460 [ 'torelative' => [ ApiBase::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
461 'torev'
462 );
463
464 $ret['prop'] = [
465 ApiBase::PARAM_DFLT => 'diff|ids|title',
466 ApiBase::PARAM_TYPE => [
467 'diff',
468 'diffsize',
469 'rel',
470 'ids',
471 'title',
472 'user',
473 'comment',
474 'parsedcomment',
475 'size',
476 ],
477 ApiBase::PARAM_ISMULTI => true,
478 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
479 ];
480
481 return $ret;
482 }
483
484 protected function getExamplesMessages() {
485 return [
486 'action=compare&fromrev=1&torev=2'
487 => 'apihelp-compare-example-1',
488 ];
489 }
490 }