Use Doxygen @addtogroup instead of phpdoc @package && @subpackage
[lhc/web/wiklou.git] / includes / Revision.php
1 <?php
2 /**
3 * @todo document
4 */
5
6 /**
7 * @todo document
8 */
9 class Revision {
10 const DELETED_TEXT = 1;
11 const DELETED_COMMENT = 2;
12 const DELETED_USER = 4;
13 const DELETED_RESTRICTED = 8;
14
15 /**
16 * Load a page revision from a given revision ID number.
17 * Returns null if no such revision can be found.
18 *
19 * @param int $id
20 * @access public
21 * @static
22 */
23 public static function newFromId( $id ) {
24 return Revision::newFromConds(
25 array( 'page_id=rev_page',
26 'rev_id' => intval( $id ) ) );
27 }
28
29 /**
30 * Load either the current, or a specified, revision
31 * that's attached to a given title. If not attached
32 * to that title, will return null.
33 *
34 * @param Title $title
35 * @param int $id
36 * @return Revision
37 * @access public
38 * @static
39 */
40 public static function newFromTitle( &$title, $id = 0 ) {
41 if( $id ) {
42 $matchId = intval( $id );
43 } else {
44 $matchId = 'page_latest';
45 }
46 return Revision::newFromConds(
47 array( "rev_id=$matchId",
48 'page_id=rev_page',
49 'page_namespace' => $title->getNamespace(),
50 'page_title' => $title->getDbkey() ) );
51 }
52
53 /**
54 * Load a page revision from a given revision ID number.
55 * Returns null if no such revision can be found.
56 *
57 * @param Database $db
58 * @param int $id
59 * @access public
60 * @static
61 */
62 public static function loadFromId( &$db, $id ) {
63 return Revision::loadFromConds( $db,
64 array( 'page_id=rev_page',
65 'rev_id' => intval( $id ) ) );
66 }
67
68 /**
69 * Load either the current, or a specified, revision
70 * that's attached to a given page. If not attached
71 * to that page, will return null.
72 *
73 * @param Database $db
74 * @param int $pageid
75 * @param int $id
76 * @return Revision
77 * @access public
78 * @static
79 */
80 public static function loadFromPageId( &$db, $pageid, $id = 0 ) {
81 $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid ));
82 if( $id ) {
83 $conds['rev_id']=intval($id);
84 } else {
85 $conds[]='rev_id=page_latest';
86 }
87 return Revision::loadFromConds( $db, $conds );
88 }
89
90 /**
91 * Load either the current, or a specified, revision
92 * that's attached to a given page. If not attached
93 * to that page, will return null.
94 *
95 * @param Database $db
96 * @param Title $title
97 * @param int $id
98 * @return Revision
99 * @access public
100 * @static
101 */
102 public static function loadFromTitle( &$db, $title, $id = 0 ) {
103 if( $id ) {
104 $matchId = intval( $id );
105 } else {
106 $matchId = 'page_latest';
107 }
108 return Revision::loadFromConds(
109 $db,
110 array( "rev_id=$matchId",
111 'page_id=rev_page',
112 'page_namespace' => $title->getNamespace(),
113 'page_title' => $title->getDbkey() ) );
114 }
115
116 /**
117 * Load the revision for the given title with the given timestamp.
118 * WARNING: Timestamps may in some circumstances not be unique,
119 * so this isn't the best key to use.
120 *
121 * @param Database $db
122 * @param Title $title
123 * @param string $timestamp
124 * @return Revision
125 * @access public
126 * @static
127 */
128 public static function loadFromTimestamp( &$db, &$title, $timestamp ) {
129 return Revision::loadFromConds(
130 $db,
131 array( 'rev_timestamp' => $db->timestamp( $timestamp ),
132 'page_id=rev_page',
133 'page_namespace' => $title->getNamespace(),
134 'page_title' => $title->getDbkey() ) );
135 }
136
137 /**
138 * Given a set of conditions, fetch a revision.
139 *
140 * @param array $conditions
141 * @return Revision
142 * @access private
143 * @static
144 */
145 private static function newFromConds( $conditions ) {
146 $db =& wfGetDB( DB_SLAVE );
147 $row = Revision::loadFromConds( $db, $conditions );
148 if( is_null( $row ) ) {
149 $dbw =& wfGetDB( DB_MASTER );
150 $row = Revision::loadFromConds( $dbw, $conditions );
151 }
152 return $row;
153 }
154
155 /**
156 * Given a set of conditions, fetch a revision from
157 * the given database connection.
158 *
159 * @param Database $db
160 * @param array $conditions
161 * @return Revision
162 * @access private
163 * @static
164 */
165 private static function loadFromConds( &$db, $conditions ) {
166 $res = Revision::fetchFromConds( $db, $conditions );
167 if( $res ) {
168 $row = $res->fetchObject();
169 $res->free();
170 if( $row ) {
171 $ret = new Revision( $row );
172 return $ret;
173 }
174 }
175 $ret = null;
176 return $ret;
177 }
178
179 /**
180 * Return a wrapper for a series of database rows to
181 * fetch all of a given page's revisions in turn.
182 * Each row can be fed to the constructor to get objects.
183 *
184 * @param Title $title
185 * @return ResultWrapper
186 * @access public
187 * @static
188 */
189 public static function fetchAllRevisions( &$title ) {
190 return Revision::fetchFromConds(
191 wfGetDB( DB_SLAVE ),
192 array( 'page_namespace' => $title->getNamespace(),
193 'page_title' => $title->getDbkey(),
194 'page_id=rev_page' ) );
195 }
196
197 /**
198 * Return a wrapper for a series of database rows to
199 * fetch all of a given page's revisions in turn.
200 * Each row can be fed to the constructor to get objects.
201 *
202 * @param Title $title
203 * @return ResultWrapper
204 * @access public
205 * @static
206 */
207 public static function fetchRevision( &$title ) {
208 return Revision::fetchFromConds(
209 wfGetDB( DB_SLAVE ),
210 array( 'rev_id=page_latest',
211 'page_namespace' => $title->getNamespace(),
212 'page_title' => $title->getDbkey(),
213 'page_id=rev_page' ) );
214 }
215
216 /**
217 * Given a set of conditions, return a ResultWrapper
218 * which will return matching database rows with the
219 * fields necessary to build Revision objects.
220 *
221 * @param Database $db
222 * @param array $conditions
223 * @return ResultWrapper
224 * @access private
225 * @static
226 */
227 private static function fetchFromConds( &$db, $conditions ) {
228 $res = $db->select(
229 array( 'page', 'revision' ),
230 array( 'page_namespace',
231 'page_title',
232 'page_latest',
233 'rev_id',
234 'rev_page',
235 'rev_text_id',
236 'rev_comment',
237 'rev_user_text',
238 'rev_user',
239 'rev_minor_edit',
240 'rev_timestamp',
241 'rev_deleted' ),
242 $conditions,
243 'Revision::fetchRow',
244 array( 'LIMIT' => 1 ) );
245 $ret = $db->resultObject( $res );
246 return $ret;
247 }
248
249 /**
250 * @param object $row
251 * @access private
252 */
253 function Revision( $row ) {
254 if( is_object( $row ) ) {
255 $this->mId = intval( $row->rev_id );
256 $this->mPage = intval( $row->rev_page );
257 $this->mTextId = intval( $row->rev_text_id );
258 $this->mComment = $row->rev_comment;
259 $this->mUserText = $row->rev_user_text;
260 $this->mUser = intval( $row->rev_user );
261 $this->mMinorEdit = intval( $row->rev_minor_edit );
262 $this->mTimestamp = $row->rev_timestamp;
263 $this->mDeleted = intval( $row->rev_deleted );
264
265 if( isset( $row->page_latest ) ) {
266 $this->mCurrent = ( $row->rev_id == $row->page_latest );
267 $this->mTitle = Title::makeTitle( $row->page_namespace,
268 $row->page_title );
269 } else {
270 $this->mCurrent = false;
271 $this->mTitle = null;
272 }
273
274 // Lazy extraction...
275 $this->mText = null;
276 if( isset( $row->old_text ) ) {
277 $this->mTextRow = $row;
278 } else {
279 // 'text' table row entry will be lazy-loaded
280 $this->mTextRow = null;
281 }
282 } elseif( is_array( $row ) ) {
283 // Build a new revision to be saved...
284 global $wgUser;
285
286 $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
287 $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
288 $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
289 $this->mUserText = isset( $row['user_text'] ) ? strval( $row['user_text'] ) : $wgUser->getName();
290 $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
291 $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
292 $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW );
293 $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
294
295 // Enforce spacing trimming on supplied text
296 $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
297 $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
298 $this->mTextRow = null;
299
300 $this->mTitle = null; # Load on demand if needed
301 $this->mCurrent = false;
302 } else {
303 throw new MWException( 'Revision constructor passed invalid row format.' );
304 }
305 }
306
307 /**#@+
308 * @access public
309 */
310
311 /**
312 * @return int
313 */
314 function getId() {
315 return $this->mId;
316 }
317
318 /**
319 * @return int
320 */
321 function getTextId() {
322 return $this->mTextId;
323 }
324
325 /**
326 * Returns the title of the page associated with this entry.
327 * @return Title
328 */
329 function getTitle() {
330 if( isset( $this->mTitle ) ) {
331 return $this->mTitle;
332 }
333 $dbr =& wfGetDB( DB_SLAVE );
334 $row = $dbr->selectRow(
335 array( 'page', 'revision' ),
336 array( 'page_namespace', 'page_title' ),
337 array( 'page_id=rev_page',
338 'rev_id' => $this->mId ),
339 'Revision::getTitle' );
340 if( $row ) {
341 $this->mTitle = Title::makeTitle( $row->page_namespace,
342 $row->page_title );
343 }
344 return $this->mTitle;
345 }
346
347 /**
348 * Set the title of the revision
349 * @param Title $title
350 */
351 function setTitle( $title ) {
352 $this->mTitle = $title;
353 }
354
355 /**
356 * @return int
357 */
358 function getPage() {
359 return $this->mPage;
360 }
361
362 /**
363 * Fetch revision's user id if it's available to all users
364 * @return int
365 */
366 function getUser() {
367 if( $this->isDeleted( self::DELETED_USER ) ) {
368 return 0;
369 } else {
370 return $this->mUser;
371 }
372 }
373
374 /**
375 * Fetch revision's user id without regard for the current user's permissions
376 * @return string
377 */
378 function getRawUser() {
379 return $this->mUser;
380 }
381
382 /**
383 * Fetch revision's username if it's available to all users
384 * @return string
385 */
386 function getUserText() {
387 if( $this->isDeleted( self::DELETED_USER ) ) {
388 return "";
389 } else {
390 return $this->mUserText;
391 }
392 }
393
394 /**
395 * Fetch revision's username without regard for view restrictions
396 * @return string
397 */
398 function getRawUserText() {
399 return $this->mUserText;
400 }
401
402 /**
403 * Fetch revision comment if it's available to all users
404 * @return string
405 */
406 function getComment() {
407 if( $this->isDeleted( self::DELETED_COMMENT ) ) {
408 return "";
409 } else {
410 return $this->mComment;
411 }
412 }
413
414 /**
415 * Fetch revision comment without regard for the current user's permissions
416 * @return string
417 */
418 function getRawComment() {
419 return $this->mComment;
420 }
421
422 /**
423 * @return bool
424 */
425 function isMinor() {
426 return (bool)$this->mMinorEdit;
427 }
428
429 /**
430 * int $field one of DELETED_* bitfield constants
431 * @return bool
432 */
433 function isDeleted( $field ) {
434 return ($this->mDeleted & $field) == $field;
435 }
436
437 /**
438 * Fetch revision text if it's available to all users
439 * @return string
440 */
441 function getText() {
442 if( $this->isDeleted( self::DELETED_TEXT ) ) {
443 return "";
444 } else {
445 return $this->getRawText();
446 }
447 }
448
449 /**
450 * Fetch revision text without regard for view restrictions
451 * @return string
452 */
453 function getRawText() {
454 if( is_null( $this->mText ) ) {
455 // Revision text is immutable. Load on demand:
456 $this->mText = $this->loadText();
457 }
458 return $this->mText;
459 }
460
461 /**
462 * @return string
463 */
464 function getTimestamp() {
465 return wfTimestamp(TS_MW, $this->mTimestamp);
466 }
467
468 /**
469 * @return bool
470 */
471 function isCurrent() {
472 return $this->mCurrent;
473 }
474
475 /**
476 * @return Revision
477 */
478 function getPrevious() {
479 $prev = $this->mTitle->getPreviousRevisionID( $this->mId );
480 if ( $prev ) {
481 return Revision::newFromTitle( $this->mTitle, $prev );
482 } else {
483 return null;
484 }
485 }
486
487 /**
488 * @return Revision
489 */
490 function getNext() {
491 $next = $this->mTitle->getNextRevisionID( $this->mId );
492 if ( $next ) {
493 return Revision::newFromTitle( $this->mTitle, $next );
494 } else {
495 return null;
496 }
497 }
498 /**#@-*/
499
500 /**
501 * Get revision text associated with an old or archive row
502 * $row is usually an object from wfFetchRow(), both the flags and the text
503 * field must be included
504 * @static
505 * @param integer $row Id of a row
506 * @param string $prefix table prefix (default 'old_')
507 * @return string $text|false the text requested
508 */
509 public static function getRevisionText( $row, $prefix = 'old_' ) {
510 $fname = 'Revision::getRevisionText';
511 wfProfileIn( $fname );
512
513 # Get data
514 $textField = $prefix . 'text';
515 $flagsField = $prefix . 'flags';
516
517 if( isset( $row->$flagsField ) ) {
518 $flags = explode( ',', $row->$flagsField );
519 } else {
520 $flags = array();
521 }
522
523 if( isset( $row->$textField ) ) {
524 $text = $row->$textField;
525 } else {
526 wfProfileOut( $fname );
527 return false;
528 }
529
530 # Use external methods for external objects, text in table is URL-only then
531 if ( in_array( 'external', $flags ) ) {
532 $url=$text;
533 @list(/* $proto */,$path)=explode('://',$url,2);
534 if ($path=="") {
535 wfProfileOut( $fname );
536 return false;
537 }
538 $text=ExternalStore::fetchFromURL($url);
539 }
540
541 // If the text was fetched without an error, convert it
542 if ( $text !== false ) {
543 if( in_array( 'gzip', $flags ) ) {
544 # Deal with optional compression of archived pages.
545 # This can be done periodically via maintenance/compressOld.php, and
546 # as pages are saved if $wgCompressRevisions is set.
547 $text = gzinflate( $text );
548 }
549
550 if( in_array( 'object', $flags ) ) {
551 # Generic compressed storage
552 $obj = unserialize( $text );
553 if ( !is_object( $obj ) ) {
554 // Invalid object
555 wfProfileOut( $fname );
556 return false;
557 }
558 $text = $obj->getText();
559 }
560
561 global $wgLegacyEncoding;
562 if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) {
563 # Old revisions kept around in a legacy encoding?
564 # Upconvert on demand.
565 global $wgInputEncoding, $wgContLang;
566 $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding . '//IGNORE', $text );
567 }
568 }
569 wfProfileOut( $fname );
570 return $text;
571 }
572
573 /**
574 * If $wgCompressRevisions is enabled, we will compress data.
575 * The input string is modified in place.
576 * Return value is the flags field: contains 'gzip' if the
577 * data is compressed, and 'utf-8' if we're saving in UTF-8
578 * mode.
579 *
580 * @static
581 * @param mixed $text reference to a text
582 * @return string
583 */
584 function compressRevisionText( &$text ) {
585 global $wgCompressRevisions;
586 $flags = array();
587
588 # Revisions not marked this way will be converted
589 # on load if $wgLegacyCharset is set in the future.
590 $flags[] = 'utf-8';
591
592 if( $wgCompressRevisions ) {
593 if( function_exists( 'gzdeflate' ) ) {
594 $text = gzdeflate( $text );
595 $flags[] = 'gzip';
596 } else {
597 wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
598 }
599 }
600 return implode( ',', $flags );
601 }
602
603 /**
604 * Insert a new revision into the database, returning the new revision ID
605 * number on success and dies horribly on failure.
606 *
607 * @param Database $dbw
608 * @return int
609 */
610 function insertOn( &$dbw ) {
611 global $wgDefaultExternalStore;
612
613 $fname = 'Revision::insertOn';
614 wfProfileIn( $fname );
615
616 $data = $this->mText;
617 $flags = Revision::compressRevisionText( $data );
618
619 # Write to external storage if required
620 if ( $wgDefaultExternalStore ) {
621 if ( is_array( $wgDefaultExternalStore ) ) {
622 // Distribute storage across multiple clusters
623 $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)];
624 } else {
625 $store = $wgDefaultExternalStore;
626 }
627 // Store and get the URL
628 $data = ExternalStore::insert( $store, $data );
629 if ( !$data ) {
630 # This should only happen in the case of a configuration error, where the external store is not valid
631 throw new MWException( "Unable to store text to external storage $store" );
632 }
633 if ( $flags ) {
634 $flags .= ',';
635 }
636 $flags .= 'external';
637 }
638
639 # Record the text (or external storage URL) to the text table
640 if( !isset( $this->mTextId ) ) {
641 $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
642 $dbw->insert( 'text',
643 array(
644 'old_id' => $old_id,
645 'old_text' => $data,
646 'old_flags' => $flags,
647 ), $fname
648 );
649 $this->mTextId = $dbw->insertId();
650 }
651
652 # Record the edit in revisions
653 $rev_id = isset( $this->mId )
654 ? $this->mId
655 : $dbw->nextSequenceValue( 'rev_rev_id_val' );
656 $dbw->insert( 'revision',
657 array(
658 'rev_id' => $rev_id,
659 'rev_page' => $this->mPage,
660 'rev_text_id' => $this->mTextId,
661 'rev_comment' => $this->mComment,
662 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
663 'rev_user' => $this->mUser,
664 'rev_user_text' => $this->mUserText,
665 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
666 'rev_deleted' => $this->mDeleted,
667 ), $fname
668 );
669
670 $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId();
671 wfProfileOut( $fname );
672 return $this->mId;
673 }
674
675 /**
676 * Lazy-load the revision's text.
677 * Currently hardcoded to the 'text' table storage engine.
678 *
679 * @return string
680 * @access private
681 */
682 function loadText() {
683 $fname = 'Revision::loadText';
684 wfProfileIn( $fname );
685
686 // Caching may be beneficial for massive use of external storage
687 global $wgRevisionCacheExpiry, $wgMemc;
688 $key = wfMemcKey( 'revisiontext', 'textid', $this->getTextId() );
689 if( $wgRevisionCacheExpiry ) {
690 $text = $wgMemc->get( $key );
691 if( is_string( $text ) ) {
692 wfProfileOut( $fname );
693 return $text;
694 }
695 }
696
697 // If we kept data for lazy extraction, use it now...
698 if ( isset( $this->mTextRow ) ) {
699 $row = $this->mTextRow;
700 $this->mTextRow = null;
701 } else {
702 $row = null;
703 }
704
705 if( !$row ) {
706 // Text data is immutable; check slaves first.
707 $dbr =& wfGetDB( DB_SLAVE );
708 $row = $dbr->selectRow( 'text',
709 array( 'old_text', 'old_flags' ),
710 array( 'old_id' => $this->getTextId() ),
711 $fname);
712 }
713
714 if( !$row ) {
715 // Possible slave lag!
716 $dbw =& wfGetDB( DB_MASTER );
717 $row = $dbw->selectRow( 'text',
718 array( 'old_text', 'old_flags' ),
719 array( 'old_id' => $this->getTextId() ),
720 $fname);
721 }
722
723 $text = Revision::getRevisionText( $row );
724
725 if( $wgRevisionCacheExpiry ) {
726 $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
727 }
728
729 wfProfileOut( $fname );
730
731 return $text;
732 }
733
734 /**
735 * Create a new null-revision for insertion into a page's
736 * history. This will not re-save the text, but simply refer
737 * to the text from the previous version.
738 *
739 * Such revisions can for instance identify page rename
740 * operations and other such meta-modifications.
741 *
742 * @param Database $dbw
743 * @param int $pageId ID number of the page to read from
744 * @param string $summary
745 * @param bool $minor
746 * @return Revision
747 */
748 function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
749 $fname = 'Revision::newNullRevision';
750 wfProfileIn( $fname );
751
752 $current = $dbw->selectRow(
753 array( 'page', 'revision' ),
754 array( 'page_latest', 'rev_text_id' ),
755 array(
756 'page_id' => $pageId,
757 'page_latest=rev_id',
758 ),
759 $fname );
760
761 if( $current ) {
762 $revision = new Revision( array(
763 'page' => $pageId,
764 'comment' => $summary,
765 'minor_edit' => $minor,
766 'text_id' => $current->rev_text_id,
767 ) );
768 } else {
769 $revision = null;
770 }
771
772 wfProfileOut( $fname );
773 return $revision;
774 }
775
776 /**
777 * Determine if the current user is allowed to view a particular
778 * field of this revision, if it's marked as deleted.
779 * @param int $field one of self::DELETED_TEXT,
780 * self::DELETED_COMMENT,
781 * self::DELETED_USER
782 * @return bool
783 */
784 function userCan( $field ) {
785 if( ( $this->mDeleted & $field ) == $field ) {
786 global $wgUser;
787 $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED
788 ? 'hiderevision'
789 : 'deleterevision';
790 wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
791 return $wgUser->isAllowed( $permission );
792 } else {
793 return true;
794 }
795 }
796
797
798 /**
799 * Get rev_timestamp from rev_id, without loading the rest of the row
800 * @param integer $id
801 */
802 static function getTimestampFromID( $id ) {
803 $dbr =& wfGetDB( DB_SLAVE );
804 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
805 array( 'rev_id' => $id ), __METHOD__ );
806 if ( $timestamp === false ) {
807 # Not in slave, try master
808 $dbw =& wfGetDB( DB_MASTER );
809 $timestamp = $dbw->selectField( 'revision', 'rev_timestamp',
810 array( 'rev_id' => $id ), __METHOD__ );
811 }
812 return $timestamp;
813 }
814
815 static function countByPageId( $db, $id ) {
816 $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
817 array( 'rev_page' => $id ), __METHOD__ );
818 if( $row ) {
819 return $row->revCount;
820 }
821 return 0;
822 }
823
824 static function countByTitle( $db, $title ) {
825 $id = $title->getArticleId();
826 if( $id ) {
827 return Revision::countByPageId( $db, $id );
828 }
829 return 0;
830 }
831 }
832
833 /**
834 * Aliases for backwards compatibility with 1.6
835 */
836 define( 'MW_REV_DELETED_TEXT', Revision::DELETED_TEXT );
837 define( 'MW_REV_DELETED_COMMENT', Revision::DELETED_COMMENT );
838 define( 'MW_REV_DELETED_USER', Revision::DELETED_USER );
839 define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED );
840
841
842 ?>