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