Merge "HttpFunctions: Increase code coverage"
[lhc/web/wiklou.git] / includes / page / PageArchive.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21 use MediaWiki\MediaWikiServices;
22 use Wikimedia\Rdbms\ResultWrapper;
23 use Wikimedia\Rdbms\IDatabase;
24
25 /**
26 * Used to show archived pages and eventually restore them.
27 */
28 class PageArchive {
29 /** @var Title */
30 protected $title;
31
32 /** @var Status */
33 protected $fileStatus;
34
35 /** @var Status */
36 protected $revisionStatus;
37
38 /** @var Config */
39 protected $config;
40
41 public function __construct( $title, Config $config = null ) {
42 if ( is_null( $title ) ) {
43 throw new MWException( __METHOD__ . ' given a null title.' );
44 }
45 $this->title = $title;
46 if ( $config === null ) {
47 wfDebug( __METHOD__ . ' did not have a Config object passed to it' );
48 $config = MediaWikiServices::getInstance()->getMainConfig();
49 }
50 $this->config = $config;
51 }
52
53 public function doesWrites() {
54 return true;
55 }
56
57 /**
58 * List all deleted pages recorded in the archive table. Returns result
59 * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
60 * namespace/title.
61 *
62 * @return ResultWrapper
63 */
64 public static function listAllPages() {
65 $dbr = wfGetDB( DB_REPLICA );
66
67 return self::listPages( $dbr, '' );
68 }
69
70 /**
71 * List deleted pages recorded in the archive table matching the
72 * given title prefix.
73 * Returns result wrapper with (ar_namespace, ar_title, count) fields.
74 *
75 * @param string $prefix Title prefix
76 * @return ResultWrapper
77 */
78 public static function listPagesByPrefix( $prefix ) {
79 $dbr = wfGetDB( DB_REPLICA );
80
81 $title = Title::newFromText( $prefix );
82 if ( $title ) {
83 $ns = $title->getNamespace();
84 $prefix = $title->getDBkey();
85 } else {
86 // Prolly won't work too good
87 // @todo handle bare namespace names cleanly?
88 $ns = 0;
89 }
90
91 $conds = [
92 'ar_namespace' => $ns,
93 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
94 ];
95
96 return self::listPages( $dbr, $conds );
97 }
98
99 /**
100 * @param IDatabase $dbr
101 * @param string|array $condition
102 * @return bool|ResultWrapper
103 */
104 protected static function listPages( $dbr, $condition ) {
105 return $dbr->select(
106 [ 'archive' ],
107 [
108 'ar_namespace',
109 'ar_title',
110 'count' => 'COUNT(*)'
111 ],
112 $condition,
113 __METHOD__,
114 [
115 'GROUP BY' => [ 'ar_namespace', 'ar_title' ],
116 'ORDER BY' => [ 'ar_namespace', 'ar_title' ],
117 'LIMIT' => 100,
118 ]
119 );
120 }
121
122 /**
123 * List the revisions of the given page. Returns result wrapper with
124 * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
125 *
126 * @return ResultWrapper
127 */
128 public function listRevisions() {
129 $dbr = wfGetDB( DB_REPLICA );
130
131 $tables = [ 'archive' ];
132
133 $fields = [
134 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
135 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
136 ];
137
138 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
139 $fields[] = 'ar_content_format';
140 $fields[] = 'ar_content_model';
141 }
142
143 $conds = [ 'ar_namespace' => $this->title->getNamespace(),
144 'ar_title' => $this->title->getDBkey() ];
145
146 $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
147
148 $join_conds = [];
149
150 ChangeTags::modifyDisplayQuery(
151 $tables,
152 $fields,
153 $conds,
154 $join_conds,
155 $options,
156 ''
157 );
158
159 return $dbr->select( $tables,
160 $fields,
161 $conds,
162 __METHOD__,
163 $options,
164 $join_conds
165 );
166 }
167
168 /**
169 * List the deleted file revisions for this page, if it's a file page.
170 * Returns a result wrapper with various filearchive fields, or null
171 * if not a file page.
172 *
173 * @return ResultWrapper
174 * @todo Does this belong in Image for fuller encapsulation?
175 */
176 public function listFiles() {
177 if ( $this->title->getNamespace() != NS_FILE ) {
178 return null;
179 }
180
181 $dbr = wfGetDB( DB_REPLICA );
182 return $dbr->select(
183 'filearchive',
184 ArchivedFile::selectFields(),
185 [ 'fa_name' => $this->title->getDBkey() ],
186 __METHOD__,
187 [ 'ORDER BY' => 'fa_timestamp DESC' ]
188 );
189 }
190
191 /**
192 * Return a Revision object containing data for the deleted revision.
193 * Note that the result *may* or *may not* have a null page ID.
194 *
195 * @param string $timestamp
196 * @return Revision|null
197 */
198 public function getRevision( $timestamp ) {
199 $dbr = wfGetDB( DB_REPLICA );
200
201 $fields = [
202 'ar_rev_id',
203 'ar_text',
204 'ar_comment',
205 'ar_user',
206 'ar_user_text',
207 'ar_timestamp',
208 'ar_minor_edit',
209 'ar_flags',
210 'ar_text_id',
211 'ar_deleted',
212 'ar_len',
213 'ar_sha1',
214 ];
215
216 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
217 $fields[] = 'ar_content_format';
218 $fields[] = 'ar_content_model';
219 }
220
221 $row = $dbr->selectRow( 'archive',
222 $fields,
223 [ 'ar_namespace' => $this->title->getNamespace(),
224 'ar_title' => $this->title->getDBkey(),
225 'ar_timestamp' => $dbr->timestamp( $timestamp ) ],
226 __METHOD__ );
227
228 if ( $row ) {
229 return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
230 }
231
232 return null;
233 }
234
235 /**
236 * Return the most-previous revision, either live or deleted, against
237 * the deleted revision given by timestamp.
238 *
239 * May produce unexpected results in case of history merges or other
240 * unusual time issues.
241 *
242 * @param string $timestamp
243 * @return Revision|null Null when there is no previous revision
244 */
245 public function getPreviousRevision( $timestamp ) {
246 $dbr = wfGetDB( DB_REPLICA );
247
248 // Check the previous deleted revision...
249 $row = $dbr->selectRow( 'archive',
250 'ar_timestamp',
251 [ 'ar_namespace' => $this->title->getNamespace(),
252 'ar_title' => $this->title->getDBkey(),
253 'ar_timestamp < ' .
254 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
255 __METHOD__,
256 [
257 'ORDER BY' => 'ar_timestamp DESC',
258 'LIMIT' => 1 ] );
259 $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
260
261 $row = $dbr->selectRow( [ 'page', 'revision' ],
262 [ 'rev_id', 'rev_timestamp' ],
263 [
264 'page_namespace' => $this->title->getNamespace(),
265 'page_title' => $this->title->getDBkey(),
266 'page_id = rev_page',
267 'rev_timestamp < ' .
268 $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
269 __METHOD__,
270 [
271 'ORDER BY' => 'rev_timestamp DESC',
272 'LIMIT' => 1 ] );
273 $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
274 $prevLiveId = $row ? intval( $row->rev_id ) : null;
275
276 if ( $prevLive && $prevLive > $prevDeleted ) {
277 // Most prior revision was live
278 return Revision::newFromId( $prevLiveId );
279 } elseif ( $prevDeleted ) {
280 // Most prior revision was deleted
281 return $this->getRevision( $prevDeleted );
282 }
283
284 // No prior revision on this page.
285 return null;
286 }
287
288 /**
289 * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
290 *
291 * @param object $row Database row
292 * @return string
293 */
294 public function getTextFromRow( $row ) {
295 if ( is_null( $row->ar_text_id ) ) {
296 // An old row from MediaWiki 1.4 or previous.
297 // Text is embedded in this row in classic compression format.
298 return Revision::getRevisionText( $row, 'ar_' );
299 }
300
301 // New-style: keyed to the text storage backend.
302 $dbr = wfGetDB( DB_REPLICA );
303 $text = $dbr->selectRow( 'text',
304 [ 'old_text', 'old_flags' ],
305 [ 'old_id' => $row->ar_text_id ],
306 __METHOD__ );
307
308 return Revision::getRevisionText( $text );
309 }
310
311 /**
312 * Fetch (and decompress if necessary) the stored text of the most
313 * recently edited deleted revision of the page.
314 *
315 * If there are no archived revisions for the page, returns NULL.
316 *
317 * @return string|null
318 */
319 public function getLastRevisionText() {
320 $dbr = wfGetDB( DB_REPLICA );
321 $row = $dbr->selectRow( 'archive',
322 [ 'ar_text', 'ar_flags', 'ar_text_id' ],
323 [ 'ar_namespace' => $this->title->getNamespace(),
324 'ar_title' => $this->title->getDBkey() ],
325 __METHOD__,
326 [ 'ORDER BY' => 'ar_timestamp DESC' ] );
327
328 if ( $row ) {
329 return $this->getTextFromRow( $row );
330 }
331
332 return null;
333 }
334
335 /**
336 * Quick check if any archived revisions are present for the page.
337 *
338 * @return bool
339 */
340 public function isDeleted() {
341 $dbr = wfGetDB( DB_REPLICA );
342 $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
343 [ 'ar_namespace' => $this->title->getNamespace(),
344 'ar_title' => $this->title->getDBkey() ],
345 __METHOD__
346 );
347
348 return ( $n > 0 );
349 }
350
351 /**
352 * Restore the given (or all) text and file revisions for the page.
353 * Once restored, the items will be removed from the archive tables.
354 * The deletion log will be updated with an undeletion notice.
355 *
356 * This also sets Status objects, $this->fileStatus and $this->revisionStatus
357 * (depending what operations are attempted).
358 *
359 * @param array $timestamps Pass an empty array to restore all revisions,
360 * otherwise list the ones to undelete.
361 * @param string $comment
362 * @param array $fileVersions
363 * @param bool $unsuppress
364 * @param User $user User performing the action, or null to use $wgUser
365 * @param string|string[] $tags Change tags to add to log entry
366 * ($user should be able to add the specified tags before this is called)
367 * @return array|bool array(number of file revisions restored, number of image revisions
368 * restored, log message) on success, false on failure.
369 */
370 public function undelete( $timestamps, $comment = '', $fileVersions = [],
371 $unsuppress = false, User $user = null, $tags = null
372 ) {
373 // If both the set of text revisions and file revisions are empty,
374 // restore everything. Otherwise, just restore the requested items.
375 $restoreAll = empty( $timestamps ) && empty( $fileVersions );
376
377 $restoreText = $restoreAll || !empty( $timestamps );
378 $restoreFiles = $restoreAll || !empty( $fileVersions );
379
380 if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
381 $img = wfLocalFile( $this->title );
382 $img->load( File::READ_LATEST );
383 $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
384 if ( !$this->fileStatus->isOK() ) {
385 return false;
386 }
387 $filesRestored = $this->fileStatus->successCount;
388 } else {
389 $filesRestored = 0;
390 }
391
392 if ( $restoreText ) {
393 $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
394 if ( !$this->revisionStatus->isOK() ) {
395 return false;
396 }
397
398 $textRestored = $this->revisionStatus->getValue();
399 } else {
400 $textRestored = 0;
401 }
402
403 // Touch the log!
404
405 if ( $textRestored && $filesRestored ) {
406 $reason = wfMessage( 'undeletedrevisions-files' )
407 ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
408 } elseif ( $textRestored ) {
409 $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
410 ->inContentLanguage()->text();
411 } elseif ( $filesRestored ) {
412 $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
413 ->inContentLanguage()->text();
414 } else {
415 wfDebug( "Undelete: nothing undeleted...\n" );
416
417 return false;
418 }
419
420 if ( trim( $comment ) != '' ) {
421 $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
422 }
423
424 if ( $user === null ) {
425 global $wgUser;
426 $user = $wgUser;
427 }
428
429 $logEntry = new ManualLogEntry( 'delete', 'restore' );
430 $logEntry->setPerformer( $user );
431 $logEntry->setTarget( $this->title );
432 $logEntry->setComment( $reason );
433 $logEntry->setTags( $tags );
434
435 Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
436
437 $logid = $logEntry->insert();
438 $logEntry->publish( $logid );
439
440 return [ $textRestored, $filesRestored, $reason ];
441 }
442
443 /**
444 * This is the meaty bit -- It restores archived revisions of the given page
445 * to the revision table.
446 *
447 * @param array $timestamps Pass an empty array to restore all revisions,
448 * otherwise list the ones to undelete.
449 * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
450 * @param string $comment
451 * @throws ReadOnlyError
452 * @return Status Status object containing the number of revisions restored on success
453 */
454 private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
455 if ( wfReadOnly() ) {
456 throw new ReadOnlyError();
457 }
458
459 $dbw = wfGetDB( DB_MASTER );
460 $dbw->startAtomic( __METHOD__ );
461
462 $restoreAll = empty( $timestamps );
463
464 # Does this page already exist? We'll have to update it...
465 $article = WikiPage::factory( $this->title );
466 # Load latest data for the current page (T33179)
467 $article->loadPageData( 'fromdbmaster' );
468 $oldcountable = $article->isCountable();
469
470 $page = $dbw->selectRow( 'page',
471 [ 'page_id', 'page_latest' ],
472 [ 'page_namespace' => $this->title->getNamespace(),
473 'page_title' => $this->title->getDBkey() ],
474 __METHOD__,
475 [ 'FOR UPDATE' ] // lock page
476 );
477
478 if ( $page ) {
479 $makepage = false;
480 # Page already exists. Import the history, and if necessary
481 # we'll update the latest revision field in the record.
482
483 # Get the time span of this page
484 $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
485 [ 'rev_id' => $page->page_latest ],
486 __METHOD__ );
487
488 if ( $previousTimestamp === false ) {
489 wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
490
491 $status = Status::newGood( 0 );
492 $status->warning( 'undeleterevision-missing' );
493 $dbw->endAtomic( __METHOD__ );
494
495 return $status;
496 }
497 } else {
498 # Have to create a new article...
499 $makepage = true;
500 $previousTimestamp = 0;
501 }
502
503 $oldWhere = [
504 'ar_namespace' => $this->title->getNamespace(),
505 'ar_title' => $this->title->getDBkey(),
506 ];
507 if ( !$restoreAll ) {
508 $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
509 }
510
511 $fields = [
512 'ar_id',
513 'ar_rev_id',
514 'rev_id',
515 'ar_text',
516 'ar_comment',
517 'ar_user',
518 'ar_user_text',
519 'ar_timestamp',
520 'ar_minor_edit',
521 'ar_flags',
522 'ar_text_id',
523 'ar_deleted',
524 'ar_page_id',
525 'ar_len',
526 'ar_sha1'
527 ];
528
529 if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
530 $fields[] = 'ar_content_format';
531 $fields[] = 'ar_content_model';
532 }
533
534 /**
535 * Select each archived revision...
536 */
537 $result = $dbw->select(
538 [ 'archive', 'revision' ],
539 $fields,
540 $oldWhere,
541 __METHOD__,
542 /* options */
543 [ 'ORDER BY' => 'ar_timestamp' ],
544 [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ]
545 );
546
547 $rev_count = $result->numRows();
548 if ( !$rev_count ) {
549 wfDebug( __METHOD__ . ": no revisions to restore\n" );
550
551 $status = Status::newGood( 0 );
552 $status->warning( "undelete-no-results" );
553 $dbw->endAtomic( __METHOD__ );
554
555 return $status;
556 }
557
558 // We use ar_id because there can be duplicate ar_rev_id even for the same
559 // page. In this case, we may be able to restore the first one.
560 $restoreFailedArIds = [];
561
562 // Map rev_id to the ar_id that is allowed to use it. When checking later,
563 // if it doesn't match, the current ar_id can not be restored.
564
565 // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
566 // rev_id is taken before we even start the restore).
567 $allowedRevIdToArIdMap = [];
568
569 $latestRestorableRow = null;
570
571 foreach ( $result as $row ) {
572 if ( $row->ar_rev_id ) {
573 // rev_id is taken even before we start restoring.
574 if ( $row->ar_rev_id === $row->rev_id ) {
575 $restoreFailedArIds[] = $row->ar_id;
576 $allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
577 } else {
578 // rev_id is not taken yet in the DB, but it might be taken
579 // by a prior revision in the same restore operation. If
580 // not, we need to reserve it.
581 if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
582 $restoreFailedArIds[] = $row->ar_id;
583 } else {
584 $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
585 $latestRestorableRow = $row;
586 }
587 }
588 } else {
589 // If ar_rev_id is null, there can't be a collision, and a
590 // rev_id will be chosen automatically.
591 $latestRestorableRow = $row;
592 }
593 }
594
595 $result->seek( 0 ); // move back
596
597 $oldPageId = 0;
598 if ( $latestRestorableRow !== null ) {
599 $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
600
601 // grab the content to check consistency with global state before restoring the page.
602 $revision = Revision::newFromArchiveRow( $latestRestorableRow,
603 [
604 'title' => $article->getTitle(), // used to derive default content model
605 ]
606 );
607 $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
608 $content = $revision->getContent( Revision::RAW );
609
610 // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
611 $status = $content->prepareSave( $article, 0, -1, $user );
612 if ( !$status->isOK() ) {
613 $dbw->endAtomic( __METHOD__ );
614
615 return $status;
616 }
617 }
618
619 $newid = false; // newly created page ID
620 $restored = 0; // number of revisions restored
621 /** @var Revision $revision */
622 $revision = null;
623
624 // If there are no restorable revisions, we can skip most of the steps.
625 if ( $latestRestorableRow === null ) {
626 $failedRevisionCount = $rev_count;
627 } else {
628 if ( $makepage ) {
629 // Check the state of the newest to-be version...
630 if ( !$unsuppress
631 && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
632 ) {
633 $dbw->endAtomic( __METHOD__ );
634
635 return Status::newFatal( "undeleterevdel" );
636 }
637 // Safe to insert now...
638 $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
639 if ( $newid === false ) {
640 // The old ID is reserved; let's pick another
641 $newid = $article->insertOn( $dbw );
642 }
643 $pageId = $newid;
644 } else {
645 // Check if a deleted revision will become the current revision...
646 if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
647 // Check the state of the newest to-be version...
648 if ( !$unsuppress
649 && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
650 ) {
651 $dbw->endAtomic( __METHOD__ );
652
653 return Status::newFatal( "undeleterevdel" );
654 }
655 }
656
657 $newid = false;
658 $pageId = $article->getId();
659 }
660
661 foreach ( $result as $row ) {
662 // Check for key dupes due to needed archive integrity.
663 if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
664 continue;
665 }
666 // Insert one revision at a time...maintaining deletion status
667 // unless we are specifically removing all restrictions...
668 $revision = Revision::newFromArchiveRow( $row,
669 [
670 'page' => $pageId,
671 'title' => $this->title,
672 'deleted' => $unsuppress ? 0 : $row->ar_deleted
673 ] );
674
675 $revision->insertOn( $dbw );
676 $restored++;
677
678 Hooks::run( 'ArticleRevisionUndeleted',
679 [ &$this->title, $revision, $row->ar_page_id ] );
680 }
681
682 // Now that it's safely stored, take it out of the archive
683 // Don't delete rows that we failed to restore
684 $toDeleteConds = $oldWhere;
685 $failedRevisionCount = count( $restoreFailedArIds );
686 if ( $failedRevisionCount > 0 ) {
687 $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
688 }
689
690 $dbw->delete( 'archive',
691 $toDeleteConds,
692 __METHOD__ );
693 }
694
695 $status = Status::newGood( $restored );
696
697 if ( $failedRevisionCount > 0 ) {
698 $status->warning(
699 wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
700 }
701
702 // Was anything restored at all?
703 if ( $restored ) {
704 $created = (bool)$newid;
705 // Attach the latest revision to the page...
706 $wasnew = $article->updateIfNewerOn( $dbw, $revision );
707 if ( $created || $wasnew ) {
708 // Update site stats, link tables, etc
709 $article->doEditUpdates(
710 $revision,
711 User::newFromName( $revision->getUserText( Revision::RAW ), false ),
712 [
713 'created' => $created,
714 'oldcountable' => $oldcountable,
715 'restored' => true
716 ]
717 );
718 }
719
720 Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] );
721 if ( $this->title->getNamespace() == NS_FILE ) {
722 DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
723 }
724 }
725
726 $dbw->endAtomic( __METHOD__ );
727
728 return $status;
729 }
730
731 /**
732 * @return Status
733 */
734 public function getFileStatus() {
735 return $this->fileStatus;
736 }
737
738 /**
739 * @return Status
740 */
741 public function getRevisionStatus() {
742 return $this->revisionStatus;
743 }
744 }