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