]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiComparePages.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.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 }