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