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