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