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