Merge "(bug 40789) Fixed $wgSecureLogin after-login redirect."
[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 protected $mContentModel;
44 protected $mContentFormat;
45 protected $mContent;
46 protected $mContentHandler;
47
48 // Revision deletion constants
49 const DELETED_TEXT = 1;
50 const DELETED_COMMENT = 2;
51 const DELETED_USER = 4;
52 const DELETED_RESTRICTED = 8;
53 const SUPPRESSED_USER = 12; // convenience
54
55 // Audience options for accessors
56 const FOR_PUBLIC = 1;
57 const FOR_THIS_USER = 2;
58 const RAW = 3;
59
60 /**
61 * Load a page revision from a given revision ID number.
62 * Returns null if no such revision can be found.
63 *
64 * $flags include:
65 * Revision::READ_LATEST : Select the data from the master
66 * Revision::READ_LOCKING : Select & lock the data from the master
67 *
68 * @param $id Integer
69 * @param $flags Integer (optional)
70 * @return Revision or null
71 */
72 public static function newFromId( $id, $flags = 0 ) {
73 return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
74 }
75
76 /**
77 * Load either the current, or a specified, revision
78 * that's attached to a given title. If not attached
79 * to that title, will return null.
80 *
81 * $flags include:
82 * Revision::READ_LATEST : Select the data from the master
83 * Revision::READ_LOCKING : Select & lock the data from the master
84 *
85 * @param $title Title
86 * @param $id Integer (optional)
87 * @param $flags Integer Bitfield (optional)
88 * @return Revision or null
89 */
90 public static function newFromTitle( $title, $id = 0, $flags = null ) {
91 $conds = array(
92 'page_namespace' => $title->getNamespace(),
93 'page_title' => $title->getDBkey()
94 );
95 if ( $id ) {
96 // Use the specified ID
97 $conds['rev_id'] = $id;
98 } else {
99 // Use a join to get the latest revision
100 $conds[] = 'rev_id=page_latest';
101 // Callers assume this will be up-to-date
102 $flags = is_int( $flags ) ? $flags : self::READ_LATEST; // b/c
103 }
104 return self::newFromConds( $conds, (int)$flags );
105 }
106
107 /**
108 * Load either the current, or a specified, revision
109 * that's attached to a given page ID.
110 * Returns null if no such revision can be found.
111 *
112 * $flags include:
113 * Revision::READ_LATEST : Select the data from the master
114 * Revision::READ_LOCKING : Select & lock the data from the master
115 *
116 * @param $revId Integer
117 * @param $pageId Integer (optional)
118 * @param $flags Integer Bitfield (optional)
119 * @return Revision or null
120 */
121 public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
122 $conds = array( 'page_id' => $pageId );
123 if ( $revId ) {
124 $conds['rev_id'] = $revId;
125 } else {
126 // Use a join to get the latest revision
127 $conds[] = 'rev_id = page_latest';
128 }
129 return self::newFromConds( $conds, (int)$flags );
130 }
131
132 /**
133 * Make a fake revision object from an archive table row. This is queried
134 * for permissions or even inserted (as in Special:Undelete)
135 * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
136 *
137 * @param $row
138 * @param $overrides array
139 *
140 * @throws MWException
141 * @return Revision
142 */
143 public static function newFromArchiveRow( $row, $overrides = array() ) {
144 global $wgContentHandlerUseDB;
145
146 $attribs = $overrides + array(
147 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
148 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
149 'comment' => $row->ar_comment,
150 'user' => $row->ar_user,
151 'user_text' => $row->ar_user_text,
152 'timestamp' => $row->ar_timestamp,
153 'minor_edit' => $row->ar_minor_edit,
154 'text_id' => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
155 'deleted' => $row->ar_deleted,
156 'len' => $row->ar_len,
157 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
158 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
159 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
160 );
161
162 if ( !$wgContentHandlerUseDB ) {
163 unset( $attribs['content_model'] );
164 unset( $attribs['content_format'] );
165 }
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::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
269 $rev = self::loadFromConds( $db, $conditions, $flags );
270 if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
271 if ( !( $flags & self::READ_LATEST ) ) {
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::READ_LOCKING ) == self::READ_LOCKING ) {
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 global $wgContentHandlerUseDB;
375
376 $fields = array(
377 'rev_id',
378 'rev_page',
379 'rev_text_id',
380 'rev_timestamp',
381 'rev_comment',
382 'rev_user_text',
383 'rev_user',
384 'rev_minor_edit',
385 'rev_deleted',
386 'rev_len',
387 'rev_parent_id',
388 'rev_sha1',
389 );
390
391 if ( $wgContentHandlerUseDB ) {
392 $fields[] = 'rev_content_format';
393 $fields[] = 'rev_content_model';
394 }
395
396 return $fields;
397 }
398
399 /**
400 * Return the list of text fields that should be selected to read the
401 * revision text
402 * @return array
403 */
404 public static function selectTextFields() {
405 return array(
406 'old_text',
407 'old_flags'
408 );
409 }
410
411 /**
412 * Return the list of page fields that should be selected from page table
413 * @return array
414 */
415 public static function selectPageFields() {
416 return array(
417 'page_namespace',
418 'page_title',
419 'page_id',
420 'page_latest',
421 'page_is_redirect',
422 'page_len',
423 );
424 }
425
426 /**
427 * Return the list of user fields that should be selected from user table
428 * @return array
429 */
430 public static function selectUserFields() {
431 return array( 'user_name' );
432 }
433
434 /**
435 * Do a batched query to get the parent revision lengths
436 * @param $db DatabaseBase
437 * @param $revIds Array
438 * @return array
439 */
440 public static function getParentLengths( $db, array $revIds ) {
441 $revLens = array();
442 if ( !$revIds ) {
443 return $revLens; // empty
444 }
445 wfProfileIn( __METHOD__ );
446 $res = $db->select( 'revision',
447 array( 'rev_id', 'rev_len' ),
448 array( 'rev_id' => $revIds ),
449 __METHOD__ );
450 foreach ( $res as $row ) {
451 $revLens[$row->rev_id] = $row->rev_len;
452 }
453 wfProfileOut( __METHOD__ );
454 return $revLens;
455 }
456
457 /**
458 * Constructor
459 *
460 * @param $row Mixed: either a database row or an array
461 * @throws MWException
462 * @access private
463 */
464 function __construct( $row ) {
465 if( is_object( $row ) ) {
466 $this->mId = intval( $row->rev_id );
467 $this->mPage = intval( $row->rev_page );
468 $this->mTextId = intval( $row->rev_text_id );
469 $this->mComment = $row->rev_comment;
470 $this->mUser = intval( $row->rev_user );
471 $this->mMinorEdit = intval( $row->rev_minor_edit );
472 $this->mTimestamp = $row->rev_timestamp;
473 $this->mDeleted = intval( $row->rev_deleted );
474
475 if( !isset( $row->rev_parent_id ) ) {
476 $this->mParentId = is_null( $row->rev_parent_id ) ? null : 0;
477 } else {
478 $this->mParentId = intval( $row->rev_parent_id );
479 }
480
481 if( !isset( $row->rev_len ) || is_null( $row->rev_len ) ) {
482 $this->mSize = null;
483 } else {
484 $this->mSize = intval( $row->rev_len );
485 }
486
487 if ( !isset( $row->rev_sha1 ) ) {
488 $this->mSha1 = null;
489 } else {
490 $this->mSha1 = $row->rev_sha1;
491 }
492
493 if( isset( $row->page_latest ) ) {
494 $this->mCurrent = ( $row->rev_id == $row->page_latest );
495 $this->mTitle = Title::newFromRow( $row );
496 } else {
497 $this->mCurrent = false;
498 $this->mTitle = null;
499 }
500
501 if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
502 $this->mContentModel = null; # determine on demand if needed
503 } else {
504 $this->mContentModel = strval( $row->rev_content_model );
505 }
506
507 if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
508 $this->mContentFormat = null; # determine on demand if needed
509 } else {
510 $this->mContentFormat = strval( $row->rev_content_format );
511 }
512
513 // Lazy extraction...
514 $this->mText = null;
515 if( isset( $row->old_text ) ) {
516 $this->mTextRow = $row;
517 } else {
518 // 'text' table row entry will be lazy-loaded
519 $this->mTextRow = null;
520 }
521
522 // Use user_name for users and rev_user_text for IPs...
523 $this->mUserText = null; // lazy load if left null
524 if ( $this->mUser == 0 ) {
525 $this->mUserText = $row->rev_user_text; // IP user
526 } elseif ( isset( $row->user_name ) ) {
527 $this->mUserText = $row->user_name; // logged-in user
528 }
529 $this->mOrigUserText = $row->rev_user_text;
530 } elseif( is_array( $row ) ) {
531 // Build a new revision to be saved...
532 global $wgUser; // ugh
533
534
535 # if we have a content object, use it to set the model and type
536 if ( !empty( $row['content'] ) ) {
537 //@todo: when is that set? test with external store setup! check out insertOn() [dk]
538 if ( !empty( $row['text_id'] ) ) {
539 throw new MWException( "Text already stored in external store (id {$row['text_id']}), "
540 . "can't serialize content object" );
541 }
542
543 $row['content_model'] = $row['content']->getModel();
544 # note: mContentFormat is initializes later accordingly
545 # note: content is serialized later in this method!
546 # also set text to null?
547 }
548
549 $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
550 $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
551 $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
552 $this->mUserText = isset( $row['user_text'] ) ? strval( $row['user_text'] ) : $wgUser->getName();
553 $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
554 $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
555 $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestampNow();
556 $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
557 $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
558 $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
559 $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
560
561 $this->mContentModel = isset( $row['content_model'] ) ? strval( $row['content_model'] ) : null;
562 $this->mContentFormat = isset( $row['content_format'] ) ? strval( $row['content_format'] ) : null;
563
564 // Enforce spacing trimming on supplied text
565 $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
566 $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
567 $this->mTextRow = null;
568
569 $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
570
571 // if we have a Content object, override mText and mContentModel
572 if ( !empty( $row['content'] ) ) {
573 if ( !( $row['content'] instanceof Content ) ) {
574 throw new MWException( '`content` field must contain a Content object.' );
575 }
576
577 $handler = $this->getContentHandler();
578 $this->mContent = $row['content'];
579
580 $this->mContentModel = $this->mContent->getModel();
581 $this->mContentHandler = null;
582
583 $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
584 } elseif ( !is_null( $this->mText ) ) {
585 $handler = $this->getContentHandler();
586 $this->mContent = $handler->unserializeContent( $this->mText );
587 }
588
589 // if we have a Title object, override mPage. Useful for testing and convenience.
590 if ( isset( $row['title'] ) ) {
591 $this->mTitle = $row['title'];
592 $this->mPage = $this->mTitle->getArticleID();
593 } else {
594 $this->mTitle = null; // Load on demand if needed
595 }
596
597 // @todo: XXX: really? we are about to create a revision. it will usually then be the current one.
598 $this->mCurrent = false;
599
600 // If we still have no length, see it we have the text to figure it out
601 if ( !$this->mSize ) {
602 if ( !is_null( $this->mContent ) ) {
603 $this->mSize = $this->mContent->getSize();
604 } else {
605 #NOTE: this should never happen if we have either text or content object!
606 $this->mSize = null;
607 }
608 }
609
610 // Same for sha1
611 if ( $this->mSha1 === null ) {
612 $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
613 }
614
615 // force lazy init
616 $this->getContentModel();
617 $this->getContentFormat();
618 } else {
619 throw new MWException( 'Revision constructor passed invalid row format.' );
620 }
621 $this->mUnpatrolled = null;
622 }
623
624 /**
625 * Get revision ID
626 *
627 * @return Integer|null
628 */
629 public function getId() {
630 return $this->mId;
631 }
632
633 /**
634 * Set the revision ID
635 *
636 * @since 1.19
637 * @param $id Integer
638 */
639 public function setId( $id ) {
640 $this->mId = $id;
641 }
642
643 /**
644 * Get text row ID
645 *
646 * @return Integer|null
647 */
648 public function getTextId() {
649 return $this->mTextId;
650 }
651
652 /**
653 * Get parent revision ID (the original previous page revision)
654 *
655 * @return Integer|null
656 */
657 public function getParentId() {
658 return $this->mParentId;
659 }
660
661 /**
662 * Returns the length of the text in this revision, or null if unknown.
663 *
664 * @return Integer|null
665 */
666 public function getSize() {
667 return $this->mSize;
668 }
669
670 /**
671 * Returns the base36 sha1 of the text in this revision, or null if unknown.
672 *
673 * @return String|null
674 */
675 public function getSha1() {
676 return $this->mSha1;
677 }
678
679 /**
680 * Returns the title of the page associated with this entry or null.
681 *
682 * Will do a query, when title is not set and id is given.
683 *
684 * @return Title|null
685 */
686 public function getTitle() {
687 if( isset( $this->mTitle ) ) {
688 return $this->mTitle;
689 }
690 if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
691 $dbr = wfGetDB( DB_SLAVE );
692 $row = $dbr->selectRow(
693 array( 'page', 'revision' ),
694 self::selectPageFields(),
695 array( 'page_id=rev_page',
696 'rev_id' => $this->mId ),
697 __METHOD__ );
698 if ( $row ) {
699 $this->mTitle = Title::newFromRow( $row );
700 }
701 }
702
703 if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
704 $this->mTitle = Title::newFromID( $this->mPage );
705 }
706
707 return $this->mTitle;
708 }
709
710 /**
711 * Set the title of the revision
712 *
713 * @param $title Title
714 */
715 public function setTitle( $title ) {
716 $this->mTitle = $title;
717 }
718
719 /**
720 * Get the page ID
721 *
722 * @return Integer|null
723 */
724 public function getPage() {
725 return $this->mPage;
726 }
727
728 /**
729 * Fetch revision's user id if it's available to the specified audience.
730 * If the specified audience does not have access to it, zero will be
731 * returned.
732 *
733 * @param $audience Integer: one of:
734 * Revision::FOR_PUBLIC to be displayed to all users
735 * Revision::FOR_THIS_USER to be displayed to the given user
736 * Revision::RAW get the ID regardless of permissions
737 * @param $user User object to check for, only if FOR_THIS_USER is passed
738 * to the $audience parameter
739 * @return Integer
740 */
741 public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
742 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
743 return 0;
744 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
745 return 0;
746 } else {
747 return $this->mUser;
748 }
749 }
750
751 /**
752 * Fetch revision's user id without regard for the current user's permissions
753 *
754 * @return String
755 */
756 public function getRawUser() {
757 return $this->mUser;
758 }
759
760 /**
761 * Fetch revision's username if it's available to the specified audience.
762 * If the specified audience does not have access to the username, an
763 * empty string will be returned.
764 *
765 * @param $audience Integer: one of:
766 * Revision::FOR_PUBLIC to be displayed to all users
767 * Revision::FOR_THIS_USER to be displayed to the given user
768 * Revision::RAW get the text regardless of permissions
769 * @param $user User object to check for, only if FOR_THIS_USER is passed
770 * to the $audience parameter
771 * @return string
772 */
773 public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
774 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
775 return '';
776 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
777 return '';
778 } else {
779 return $this->getRawUserText();
780 }
781 }
782
783 /**
784 * Fetch revision's username without regard for view restrictions
785 *
786 * @return String
787 */
788 public function getRawUserText() {
789 if ( $this->mUserText === null ) {
790 $this->mUserText = User::whoIs( $this->mUser ); // load on demand
791 if ( $this->mUserText === false ) {
792 # This shouldn't happen, but it can if the wiki was recovered
793 # via importing revs and there is no user table entry yet.
794 $this->mUserText = $this->mOrigUserText;
795 }
796 }
797 return $this->mUserText;
798 }
799
800 /**
801 * Fetch revision comment if it's available to the specified audience.
802 * If the specified audience does not have access to the comment, an
803 * empty string will be returned.
804 *
805 * @param $audience Integer: one of:
806 * Revision::FOR_PUBLIC to be displayed to all users
807 * Revision::FOR_THIS_USER to be displayed to the given user
808 * Revision::RAW get the text regardless of permissions
809 * @param $user User object to check for, only if FOR_THIS_USER is passed
810 * to the $audience parameter
811 * @return String
812 */
813 function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
814 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
815 return '';
816 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
817 return '';
818 } else {
819 return $this->mComment;
820 }
821 }
822
823 /**
824 * Fetch revision comment without regard for the current user's permissions
825 *
826 * @return String
827 */
828 public function getRawComment() {
829 return $this->mComment;
830 }
831
832 /**
833 * @return Boolean
834 */
835 public function isMinor() {
836 return (bool)$this->mMinorEdit;
837 }
838
839 /**
840 * @return Integer rcid of the unpatrolled row, zero if there isn't one
841 */
842 public function isUnpatrolled() {
843 if( $this->mUnpatrolled !== null ) {
844 return $this->mUnpatrolled;
845 }
846 $dbr = wfGetDB( DB_SLAVE );
847 $this->mUnpatrolled = $dbr->selectField( 'recentchanges',
848 'rc_id',
849 array( // Add redundant user,timestamp condition so we can use the existing index
850 'rc_user_text' => $this->getRawUserText(),
851 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
852 'rc_this_oldid' => $this->getId(),
853 'rc_patrolled' => 0
854 ),
855 __METHOD__
856 );
857 return (int)$this->mUnpatrolled;
858 }
859
860 /**
861 * @param $field int one of DELETED_* bitfield constants
862 *
863 * @return Boolean
864 */
865 public function isDeleted( $field ) {
866 return ( $this->mDeleted & $field ) == $field;
867 }
868
869 /**
870 * Get the deletion bitfield of the revision
871 *
872 * @return int
873 */
874 public function getVisibility() {
875 return (int)$this->mDeleted;
876 }
877
878 /**
879 * Fetch revision text if it's available to the specified audience.
880 * If the specified audience does not have the ability to view this
881 * revision, an empty string will be returned.
882 *
883 * @param $audience Integer: one of:
884 * Revision::FOR_PUBLIC to be displayed to all users
885 * Revision::FOR_THIS_USER to be displayed to the given user
886 * Revision::RAW get the text regardless of permissions
887 * @param $user User object to check for, only if FOR_THIS_USER is passed
888 * to the $audience parameter
889 *
890 * @deprecated in 1.21, use getContent() instead
891 * @todo: replace usage in core
892 * @return String
893 */
894 public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
895 wfDeprecated( __METHOD__, '1.21' );
896
897 $content = $this->getContent( $audience, $user );
898 return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
899 }
900
901 /**
902 * Fetch revision content if it's available to the specified audience.
903 * If the specified audience does not have the ability to view this
904 * revision, null will be returned.
905 *
906 * @param $audience Integer: one of:
907 * Revision::FOR_PUBLIC to be displayed to all users
908 * Revision::FOR_THIS_USER to be displayed to $wgUser
909 * Revision::RAW get the text regardless of permissions
910 * @param $user User object to check for, only if FOR_THIS_USER is passed
911 * to the $audience parameter
912 * @since 1.21
913 * @return Content
914 */
915 public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
916 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
917 return null;
918 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
919 return null;
920 } else {
921 return $this->getContentInternal();
922 }
923 }
924
925 /**
926 * Alias for getText(Revision::FOR_THIS_USER)
927 *
928 * @deprecated since 1.17
929 * @return String
930 */
931 public function revText() {
932 wfDeprecated( __METHOD__, '1.17' );
933 return $this->getText( self::FOR_THIS_USER );
934 }
935
936 /**
937 * Fetch revision text without regard for view restrictions
938 *
939 * @return String
940 *
941 * @deprecated since 1.21. Instead, use Revision::getContent( Revision::RAW )
942 * or Revision::getSerializedData() as appropriate.
943 */
944 public function getRawText() {
945 wfDeprecated( __METHOD__, "1.21" );
946
947 return $this->getText( self::RAW );
948 }
949
950 /**
951 * Fetch original serialized data without regard for view restrictions
952 *
953 * @since 1.21
954 * @return String
955 */
956 public function getSerializedData() {
957 return $this->mText;
958 }
959
960 /**
961 * Gets the content object for the revision
962 *
963 * @since 1.21
964 * @return Content
965 */
966 protected function getContentInternal() {
967 if( is_null( $this->mContent ) ) {
968 // Revision is immutable. Load on demand:
969
970 $handler = $this->getContentHandler();
971 $format = $this->getContentFormat();
972 $title = $this->getTitle();
973
974 if( is_null( $this->mText ) ) {
975 // Load text on demand:
976 $this->mText = $this->loadText();
977 }
978
979 $this->mContent = is_null( $this->mText ) ? null : $handler->unserializeContent( $this->mText, $format );
980 }
981
982 return $this->mContent->copy(); // NOTE: copy() will return $this for immutable content objects
983 }
984
985 /**
986 * Returns the content model for this revision.
987 *
988 * If no content model was stored in the database, $this->getTitle()->getContentModel() is
989 * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
990 * is used as a last resort.
991 *
992 * @return String the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
993 **/
994 public function getContentModel() {
995 if ( !$this->mContentModel ) {
996 $title = $this->getTitle();
997 $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
998
999 assert( !empty( $this->mContentModel ) );
1000 }
1001
1002 return $this->mContentModel;
1003 }
1004
1005 /**
1006 * Returns the content format for this revision.
1007 *
1008 * If no content format was stored in the database, the default format for this
1009 * revision's content model is returned.
1010 *
1011 * @return String the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
1012 **/
1013 public function getContentFormat() {
1014 if ( !$this->mContentFormat ) {
1015 $handler = $this->getContentHandler();
1016 $this->mContentFormat = $handler->getDefaultFormat();
1017
1018 assert( !empty( $this->mContentFormat ) );
1019 }
1020
1021 return $this->mContentFormat;
1022 }
1023
1024 /**
1025 * Returns the content handler appropriate for this revision's content model.
1026 *
1027 * @return ContentHandler
1028 */
1029 public function getContentHandler() {
1030 if ( !$this->mContentHandler ) {
1031 $model = $this->getContentModel();
1032 $this->mContentHandler = ContentHandler::getForModelID( $model );
1033
1034 $format = $this->getContentFormat();
1035
1036 if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
1037 throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
1038 }
1039 }
1040
1041 return $this->mContentHandler;
1042 }
1043
1044 /**
1045 * @return String
1046 */
1047 public function getTimestamp() {
1048 return wfTimestamp( TS_MW, $this->mTimestamp );
1049 }
1050
1051 /**
1052 * @return Boolean
1053 */
1054 public function isCurrent() {
1055 return $this->mCurrent;
1056 }
1057
1058 /**
1059 * Get previous revision for this title
1060 *
1061 * @return Revision or null
1062 */
1063 public function getPrevious() {
1064 if( $this->getTitle() ) {
1065 $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
1066 if( $prev ) {
1067 return self::newFromTitle( $this->getTitle(), $prev );
1068 }
1069 }
1070 return null;
1071 }
1072
1073 /**
1074 * Get next revision for this title
1075 *
1076 * @return Revision or null
1077 */
1078 public function getNext() {
1079 if( $this->getTitle() ) {
1080 $next = $this->getTitle()->getNextRevisionID( $this->getId() );
1081 if ( $next ) {
1082 return self::newFromTitle( $this->getTitle(), $next );
1083 }
1084 }
1085 return null;
1086 }
1087
1088 /**
1089 * Get previous revision Id for this page_id
1090 * This is used to populate rev_parent_id on save
1091 *
1092 * @param $db DatabaseBase
1093 * @return Integer
1094 */
1095 private function getPreviousRevisionId( $db ) {
1096 if( is_null( $this->mPage ) ) {
1097 return 0;
1098 }
1099 # Use page_latest if ID is not given
1100 if( !$this->mId ) {
1101 $prevId = $db->selectField( 'page', 'page_latest',
1102 array( 'page_id' => $this->mPage ),
1103 __METHOD__ );
1104 } else {
1105 $prevId = $db->selectField( 'revision', 'rev_id',
1106 array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ),
1107 __METHOD__,
1108 array( 'ORDER BY' => 'rev_id DESC' ) );
1109 }
1110 return intval( $prevId );
1111 }
1112
1113 /**
1114 * Get revision text associated with an old or archive row
1115 * $row is usually an object from wfFetchRow(), both the flags and the text
1116 * field must be included
1117 *
1118 * @param $row Object: the text data
1119 * @param $prefix String: table prefix (default 'old_')
1120 * @return String: text the text requested or false on failure
1121 */
1122 public static function getRevisionText( $row, $prefix = 'old_' ) {
1123 wfProfileIn( __METHOD__ );
1124
1125 # Get data
1126 $textField = $prefix . 'text';
1127 $flagsField = $prefix . 'flags';
1128
1129 if( isset( $row->$flagsField ) ) {
1130 $flags = explode( ',', $row->$flagsField );
1131 } else {
1132 $flags = array();
1133 }
1134
1135 if( isset( $row->$textField ) ) {
1136 $text = $row->$textField;
1137 } else {
1138 wfProfileOut( __METHOD__ );
1139 return false;
1140 }
1141
1142 # Use external methods for external objects, text in table is URL-only then
1143 if ( in_array( 'external', $flags ) ) {
1144 $url = $text;
1145 $parts = explode( '://', $url, 2 );
1146 if( count( $parts ) == 1 || $parts[1] == '' ) {
1147 wfProfileOut( __METHOD__ );
1148 return false;
1149 }
1150 $text = ExternalStore::fetchFromURL( $url );
1151 }
1152
1153 // If the text was fetched without an error, convert it
1154 if ( $text !== false ) {
1155 if( in_array( 'gzip', $flags ) ) {
1156 # Deal with optional compression of archived pages.
1157 # This can be done periodically via maintenance/compressOld.php, and
1158 # as pages are saved if $wgCompressRevisions is set.
1159 $text = gzinflate( $text );
1160 }
1161
1162 if( in_array( 'object', $flags ) ) {
1163 # Generic compressed storage
1164 $obj = unserialize( $text );
1165 if ( !is_object( $obj ) ) {
1166 // Invalid object
1167 wfProfileOut( __METHOD__ );
1168 return false;
1169 }
1170 $text = $obj->getText();
1171 }
1172
1173 global $wgLegacyEncoding;
1174 if( $text !== false && $wgLegacyEncoding
1175 && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) )
1176 {
1177 # Old revisions kept around in a legacy encoding?
1178 # Upconvert on demand.
1179 # ("utf8" checked for compatibility with some broken
1180 # conversion scripts 2008-12-30)
1181 global $wgContLang;
1182 $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
1183 }
1184 }
1185 wfProfileOut( __METHOD__ );
1186 return $text;
1187 }
1188
1189 /**
1190 * If $wgCompressRevisions is enabled, we will compress data.
1191 * The input string is modified in place.
1192 * Return value is the flags field: contains 'gzip' if the
1193 * data is compressed, and 'utf-8' if we're saving in UTF-8
1194 * mode.
1195 *
1196 * @param $text Mixed: reference to a text
1197 * @return String
1198 */
1199 public static function compressRevisionText( &$text ) {
1200 global $wgCompressRevisions;
1201 $flags = array();
1202
1203 # Revisions not marked this way will be converted
1204 # on load if $wgLegacyCharset is set in the future.
1205 $flags[] = 'utf-8';
1206
1207 if( $wgCompressRevisions ) {
1208 if( function_exists( 'gzdeflate' ) ) {
1209 $text = gzdeflate( $text );
1210 $flags[] = 'gzip';
1211 } else {
1212 wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
1213 }
1214 }
1215 return implode( ',', $flags );
1216 }
1217
1218 /**
1219 * Insert a new revision into the database, returning the new revision ID
1220 * number on success and dies horribly on failure.
1221 *
1222 * @param $dbw DatabaseBase: (master connection)
1223 * @throws MWException
1224 * @return Integer
1225 */
1226 public function insertOn( $dbw ) {
1227 global $wgDefaultExternalStore, $wgContentHandlerUseDB;
1228
1229 wfProfileIn( __METHOD__ );
1230
1231 $this->checkContentModel();
1232
1233 $data = $this->mText;
1234 $flags = self::compressRevisionText( $data );
1235
1236 # Write to external storage if required
1237 if( $wgDefaultExternalStore ) {
1238 // Store and get the URL
1239 $data = ExternalStore::insertToDefault( $data );
1240 if( !$data ) {
1241 throw new MWException( "Unable to store text to external storage" );
1242 }
1243 if( $flags ) {
1244 $flags .= ',';
1245 }
1246 $flags .= 'external';
1247 }
1248
1249 # Record the text (or external storage URL) to the text table
1250 if( !isset( $this->mTextId ) ) {
1251 $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
1252 $dbw->insert( 'text',
1253 array(
1254 'old_id' => $old_id,
1255 'old_text' => $data,
1256 'old_flags' => $flags,
1257 ), __METHOD__
1258 );
1259 $this->mTextId = $dbw->insertId();
1260 }
1261
1262 if ( $this->mComment === null ) $this->mComment = "";
1263
1264 # Record the edit in revisions
1265 $rev_id = isset( $this->mId )
1266 ? $this->mId
1267 : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
1268 $row = array(
1269 'rev_id' => $rev_id,
1270 'rev_page' => $this->mPage,
1271 'rev_text_id' => $this->mTextId,
1272 'rev_comment' => $this->mComment,
1273 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
1274 'rev_user' => $this->mUser,
1275 'rev_user_text' => $this->mUserText,
1276 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
1277 'rev_deleted' => $this->mDeleted,
1278 'rev_len' => $this->mSize,
1279 'rev_parent_id' => is_null( $this->mParentId )
1280 ? $this->getPreviousRevisionId( $dbw )
1281 : $this->mParentId,
1282 'rev_sha1' => is_null( $this->mSha1 )
1283 ? Revision::base36Sha1( $this->mText )
1284 : $this->mSha1,
1285 );
1286
1287 if ( $wgContentHandlerUseDB ) {
1288 //NOTE: Store null for the default model and format, to save space.
1289 //XXX: Makes the DB sensitive to changed defaults. Make this behaviour optional? Only in miser mode?
1290
1291 $model = $this->getContentModel();
1292 $format = $this->getContentFormat();
1293
1294 $title = $this->getTitle();
1295
1296 if ( $title === null ) {
1297 throw new MWException( "Insufficient information to determine the title of the revision's page!" );
1298 }
1299
1300 $defaultModel = ContentHandler::getDefaultModelFor( $title );
1301 $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
1302
1303 $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model;
1304 $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format;
1305 }
1306
1307 $dbw->insert( 'revision', $row, __METHOD__ );
1308
1309 $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
1310
1311 wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
1312
1313 wfProfileOut( __METHOD__ );
1314 return $this->mId;
1315 }
1316
1317 protected function checkContentModel() {
1318 global $wgContentHandlerUseDB;
1319
1320 $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
1321
1322 $model = $this->getContentModel();
1323 $format = $this->getContentFormat();
1324 $handler = $this->getContentHandler();
1325
1326 if ( !$handler->isSupportedFormat( $format ) ) {
1327 $t = $title->getPrefixedDBkey();
1328
1329 throw new MWException( "Can't use format $format with content model $model on $t" );
1330 }
1331
1332 if ( !$wgContentHandlerUseDB && $title ) {
1333 // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
1334
1335 $defaultModel = ContentHandler::getDefaultModelFor( $title );
1336 $defaultHandler = ContentHandler::getForModelID( $defaultModel );
1337 $defaultFormat = $defaultHandler->getDefaultFormat();
1338
1339 if ( $this->getContentModel() != $defaultModel ) {
1340 $t = $title->getPrefixedDBkey();
1341
1342 throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: "
1343 . "model is $model , default for $t is $defaultModel" );
1344 }
1345
1346 if ( $this->getContentFormat() != $defaultFormat ) {
1347 $t = $title->getPrefixedDBkey();
1348
1349 throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: "
1350 . "format is $format, default for $t is $defaultFormat" );
1351 }
1352 }
1353
1354 $content = $this->getContent( Revision::RAW );
1355
1356 if ( !$content->isValid() ) {
1357 $t = $title->getPrefixedDBkey();
1358
1359 throw new MWException( "Content of $t is not valid! Content model is $model" );
1360 }
1361 }
1362
1363 /**
1364 * Get the base 36 SHA-1 value for a string of text
1365 * @param $text String
1366 * @return String
1367 */
1368 public static function base36Sha1( $text ) {
1369 return wfBaseConvert( sha1( $text ), 16, 36, 31 );
1370 }
1371
1372 /**
1373 * Lazy-load the revision's text.
1374 * Currently hardcoded to the 'text' table storage engine.
1375 *
1376 * @return String
1377 */
1378 protected function loadText() {
1379 wfProfileIn( __METHOD__ );
1380
1381 // Caching may be beneficial for massive use of external storage
1382 global $wgRevisionCacheExpiry, $wgMemc;
1383 $textId = $this->getTextId();
1384 $key = wfMemcKey( 'revisiontext', 'textid', $textId );
1385 if( $wgRevisionCacheExpiry ) {
1386 $text = $wgMemc->get( $key );
1387 if( is_string( $text ) ) {
1388 wfDebug( __METHOD__ . ": got id $textId from cache\n" );
1389 wfProfileOut( __METHOD__ );
1390 return $text;
1391 }
1392 }
1393
1394 // If we kept data for lazy extraction, use it now...
1395 if ( isset( $this->mTextRow ) ) {
1396 $row = $this->mTextRow;
1397 $this->mTextRow = null;
1398 } else {
1399 $row = null;
1400 }
1401
1402 if( !$row ) {
1403 // Text data is immutable; check slaves first.
1404 $dbr = wfGetDB( DB_SLAVE );
1405 $row = $dbr->selectRow( 'text',
1406 array( 'old_text', 'old_flags' ),
1407 array( 'old_id' => $this->getTextId() ),
1408 __METHOD__ );
1409 }
1410
1411 if( !$row && wfGetLB()->getServerCount() > 1 ) {
1412 // Possible slave lag!
1413 $dbw = wfGetDB( DB_MASTER );
1414 $row = $dbw->selectRow( 'text',
1415 array( 'old_text', 'old_flags' ),
1416 array( 'old_id' => $this->getTextId() ),
1417 __METHOD__ );
1418 }
1419
1420 $text = self::getRevisionText( $row );
1421
1422 # No negative caching -- negative hits on text rows may be due to corrupted slave servers
1423 if( $wgRevisionCacheExpiry && $text !== false ) {
1424 $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
1425 }
1426
1427 wfProfileOut( __METHOD__ );
1428
1429 return $text;
1430 }
1431
1432 /**
1433 * Create a new null-revision for insertion into a page's
1434 * history. This will not re-save the text, but simply refer
1435 * to the text from the previous version.
1436 *
1437 * Such revisions can for instance identify page rename
1438 * operations and other such meta-modifications.
1439 *
1440 * @param $dbw DatabaseBase
1441 * @param $pageId Integer: ID number of the page to read from
1442 * @param $summary String: revision's summary
1443 * @param $minor Boolean: whether the revision should be considered as minor
1444 * @return Revision|null on error
1445 */
1446 public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
1447 global $wgContentHandlerUseDB;
1448
1449 wfProfileIn( __METHOD__ );
1450
1451 $fields = array( 'page_latest', 'page_namespace', 'page_title',
1452 'rev_text_id', 'rev_len', 'rev_sha1' );
1453
1454 if ( $wgContentHandlerUseDB ) {
1455 $fields[] = 'rev_content_model';
1456 $fields[] = 'rev_content_format';
1457 }
1458
1459 $current = $dbw->selectRow(
1460 array( 'page', 'revision' ),
1461 $fields,
1462 array(
1463 'page_id' => $pageId,
1464 'page_latest=rev_id',
1465 ),
1466 __METHOD__ );
1467
1468 if( $current ) {
1469 $row = array(
1470 'page' => $pageId,
1471 'comment' => $summary,
1472 'minor_edit' => $minor,
1473 'text_id' => $current->rev_text_id,
1474 'parent_id' => $current->page_latest,
1475 'len' => $current->rev_len,
1476 'sha1' => $current->rev_sha1
1477 );
1478
1479 if ( $wgContentHandlerUseDB ) {
1480 $row[ 'content_model' ] = $current->rev_content_model;
1481 $row[ 'content_format' ] = $current->rev_content_format;
1482 }
1483
1484 $revision = new Revision( $row );
1485 $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
1486 } else {
1487 $revision = null;
1488 }
1489
1490 wfProfileOut( __METHOD__ );
1491 return $revision;
1492 }
1493
1494 /**
1495 * Determine if the current user is allowed to view a particular
1496 * field of this revision, if it's marked as deleted.
1497 *
1498 * @param $field Integer:one of self::DELETED_TEXT,
1499 * self::DELETED_COMMENT,
1500 * self::DELETED_USER
1501 * @param $user User object to check, or null to use $wgUser
1502 * @return Boolean
1503 */
1504 public function userCan( $field, User $user = null ) {
1505 return self::userCanBitfield( $this->mDeleted, $field, $user );
1506 }
1507
1508 /**
1509 * Determine if the current user is allowed to view a particular
1510 * field of this revision, if it's marked as deleted. This is used
1511 * by various classes to avoid duplication.
1512 *
1513 * @param $bitfield Integer: current field
1514 * @param $field Integer: one of self::DELETED_TEXT = File::DELETED_FILE,
1515 * self::DELETED_COMMENT = File::DELETED_COMMENT,
1516 * self::DELETED_USER = File::DELETED_USER
1517 * @param $user User object to check, or null to use $wgUser
1518 * @return Boolean
1519 */
1520 public static function userCanBitfield( $bitfield, $field, User $user = null ) {
1521 if( $bitfield & $field ) { // aspect is deleted
1522 if ( $bitfield & self::DELETED_RESTRICTED ) {
1523 $permission = 'suppressrevision';
1524 } elseif ( $field & self::DELETED_TEXT ) {
1525 $permission = 'deletedtext';
1526 } else {
1527 $permission = 'deletedhistory';
1528 }
1529 wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
1530 if ( $user === null ) {
1531 global $wgUser;
1532 $user = $wgUser;
1533 }
1534 return $user->isAllowed( $permission );
1535 } else {
1536 return true;
1537 }
1538 }
1539
1540 /**
1541 * Get rev_timestamp from rev_id, without loading the rest of the row
1542 *
1543 * @param $title Title
1544 * @param $id Integer
1545 * @return String
1546 */
1547 static function getTimestampFromId( $title, $id ) {
1548 $dbr = wfGetDB( DB_SLAVE );
1549 // Casting fix for DB2
1550 if ( $id == '' ) {
1551 $id = 0;
1552 }
1553 $conds = array( 'rev_id' => $id );
1554 $conds['rev_page'] = $title->getArticleID();
1555 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
1556 if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
1557 # Not in slave, try master
1558 $dbw = wfGetDB( DB_MASTER );
1559 $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
1560 }
1561 return wfTimestamp( TS_MW, $timestamp );
1562 }
1563
1564 /**
1565 * Get count of revisions per page...not very efficient
1566 *
1567 * @param $db DatabaseBase
1568 * @param $id Integer: page id
1569 * @return Integer
1570 */
1571 static function countByPageId( $db, $id ) {
1572 $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
1573 array( 'rev_page' => $id ), __METHOD__ );
1574 if( $row ) {
1575 return $row->revCount;
1576 }
1577 return 0;
1578 }
1579
1580 /**
1581 * Get count of revisions per page...not very efficient
1582 *
1583 * @param $db DatabaseBase
1584 * @param $title Title
1585 * @return Integer
1586 */
1587 static function countByTitle( $db, $title ) {
1588 $id = $title->getArticleID();
1589 if( $id ) {
1590 return self::countByPageId( $db, $id );
1591 }
1592 return 0;
1593 }
1594
1595 /**
1596 * Check if no edits were made by other users since
1597 * the time a user started editing the page. Limit to
1598 * 50 revisions for the sake of performance.
1599 *
1600 * @since 1.20
1601 *
1602 * @param DatabaseBase|int $db the Database to perform the check on. May be given as a Database object or
1603 * a database identifier usable with wfGetDB.
1604 * @param int $pageId the ID of the page in question
1605 * @param int $userId the ID of the user in question
1606 * @param string $since look at edits since this time
1607 *
1608 * @return bool True if the given user was the only one to edit since the given timestamp
1609 */
1610 public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
1611 if ( !$userId ) return false;
1612
1613 if ( is_int( $db ) ) {
1614 $db = wfGetDB( $db );
1615 }
1616
1617 $res = $db->select( 'revision',
1618 'rev_user',
1619 array(
1620 'rev_page' => $pageId,
1621 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
1622 ),
1623 __METHOD__,
1624 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
1625 foreach ( $res as $row ) {
1626 if ( $row->rev_user != $userId ) {
1627 return false;
1628 }
1629 }
1630 return true;
1631 }
1632 }