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