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