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