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