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