7a33612c9c90f3d3a2b7594e0fef0ef257f5b0d1
[lhc/web/wiklou.git] / includes / Revision.php
1 <?php
2 /**
3 * @package MediaWiki
4 * @todo document
5 */
6
7 /** */
8 require_once( 'Database.php' );
9 require_once( 'Article.php' );
10
11 /**
12 * @package MediaWiki
13 * @todo document
14 */
15 class Revision {
16 const MW_REV_DELETED_TEXT = 1;
17 const MW_REV_DELETED_COMMENT = 2;
18 const MW_REV_DELETED_USER = 4;
19 const MW_REV_DELETED_RESTRICTED = 8;
20
21 /**
22 * Load a page revision from a given revision ID number.
23 * Returns null if no such revision can be found.
24 *
25 * @param int $id
26 * @static
27 * @access public
28 */
29 function newFromId( $id ) {
30 return Revision::newFromConds(
31 array( 'page_id=rev_page',
32 'rev_id' => intval( $id ) ) );
33 }
34
35 /**
36 * Load either the current, or a specified, revision
37 * that's attached to a given title. If not attached
38 * to that title, will return null.
39 *
40 * @param Title $title
41 * @param int $id
42 * @return Revision
43 * @access public
44 * @static
45 */
46 function newFromTitle( &$title, $id = 0 ) {
47 if( $id ) {
48 $matchId = intval( $id );
49 } else {
50 $matchId = 'page_latest';
51 }
52 return Revision::newFromConds(
53 array( "rev_id=$matchId",
54 'page_id=rev_page',
55 'page_namespace' => $title->getNamespace(),
56 'page_title' => $title->getDbkey() ) );
57 }
58
59 /**
60 * Load either the current, or a specified, revision
61 * that's attached to a given page. If not attached
62 * to that page, will return null.
63 *
64 * @param Database $db
65 * @param int $pageid
66 * @param int $id
67 * @return Revision
68 * @access public
69 */
70 function loadFromPageId( &$db, $pageid, $id = 0 ) {
71 $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid ));
72 if( $id ) {
73 $conds['rev_id']=intval($id);
74 } else {
75 $conds[]='rev_id=page_latest';
76 }
77 return Revision::loadFromConds( $db, $conds );
78 }
79
80 /**
81 * Load either the current, or a specified, revision
82 * that's attached to a given page. If not attached
83 * to that page, will return null.
84 *
85 * @param Database $db
86 * @param Title $title
87 * @param int $id
88 * @return Revision
89 * @access public
90 */
91 function loadFromTitle( &$db, $title, $id = 0 ) {
92 if( $id ) {
93 $matchId = intval( $id );
94 } else {
95 $matchId = 'page_latest';
96 }
97 return Revision::loadFromConds(
98 $db,
99 array( "rev_id=$matchId",
100 'page_id=rev_page',
101 'page_namespace' => $title->getNamespace(),
102 'page_title' => $title->getDbkey() ) );
103 }
104
105 /**
106 * Load the revision for the given title with the given timestamp.
107 * WARNING: Timestamps may in some circumstances not be unique,
108 * so this isn't the best key to use.
109 *
110 * @param Database $db
111 * @param Title $title
112 * @param string $timestamp
113 * @return Revision
114 * @access public
115 * @static
116 */
117 function loadFromTimestamp( &$db, &$title, $timestamp ) {
118 return Revision::loadFromConds(
119 $db,
120 array( 'rev_timestamp' => $db->timestamp( $timestamp ),
121 'page_id=rev_page',
122 'page_namespace' => $title->getNamespace(),
123 'page_title' => $title->getDbkey() ) );
124 }
125
126 /**
127 * Given a set of conditions, fetch a revision.
128 *
129 * @param array $conditions
130 * @return Revision
131 * @static
132 * @access private
133 */
134 function newFromConds( $conditions ) {
135 $db =& wfGetDB( DB_SLAVE );
136 $row = Revision::loadFromConds( $db, $conditions );
137 if( is_null( $row ) ) {
138 $dbw =& wfGetDB( DB_MASTER );
139 $row = Revision::loadFromConds( $dbw, $conditions );
140 }
141 return $row;
142 }
143
144 /**
145 * Given a set of conditions, fetch a revision from
146 * the given database connection.
147 *
148 * @param Database $db
149 * @param array $conditions
150 * @return Revision
151 * @static
152 * @access private
153 */
154 function loadFromConds( &$db, $conditions ) {
155 $res = Revision::fetchFromConds( $db, $conditions );
156 if( $res ) {
157 $row = $res->fetchObject();
158 $res->free();
159 if( $row ) {
160 $ret = new Revision( $row );
161 return $ret;
162 }
163 }
164 $ret = null;
165 return $ret;
166 }
167
168 /**
169 * Return a wrapper for a series of database rows to
170 * fetch all of a given page's revisions in turn.
171 * Each row can be fed to the constructor to get objects.
172 *
173 * @param Title $title
174 * @return ResultWrapper
175 * @static
176 * @access public
177 */
178 function fetchAllRevisions( &$title ) {
179 return Revision::fetchFromConds(
180 wfGetDB( DB_SLAVE ),
181 array( 'page_namespace' => $title->getNamespace(),
182 'page_title' => $title->getDbkey(),
183 'page_id=rev_page' ) );
184 }
185
186 /**
187 * Return a wrapper for a series of database rows to
188 * fetch all of a given page's revisions in turn.
189 * Each row can be fed to the constructor to get objects.
190 *
191 * @param Title $title
192 * @return ResultWrapper
193 * @static
194 * @access public
195 */
196 function fetchRevision( &$title ) {
197 return Revision::fetchFromConds(
198 wfGetDB( DB_SLAVE ),
199 array( 'rev_id=page_latest',
200 'page_namespace' => $title->getNamespace(),
201 'page_title' => $title->getDbkey(),
202 'page_id=rev_page' ) );
203 }
204
205 /**
206 * Given a set of conditions, return a ResultWrapper
207 * which will return matching database rows with the
208 * fields necessary to build Revision objects.
209 *
210 * @param Database $db
211 * @param array $conditions
212 * @return ResultWrapper
213 * @static
214 * @access private
215 */
216 function fetchFromConds( &$db, $conditions ) {
217 $res = $db->select(
218 array( 'page', 'revision' ),
219 array( 'page_namespace',
220 'page_title',
221 'page_latest',
222 'rev_id',
223 'rev_page',
224 'rev_text_id',
225 'rev_comment',
226 'rev_user_text',
227 'rev_user',
228 'rev_minor_edit',
229 'rev_timestamp',
230 'rev_deleted' ),
231 $conditions,
232 'Revision::fetchRow',
233 array( 'LIMIT' => 1 ) );
234 $ret = $db->resultObject( $res );
235 return $ret;
236 }
237
238 /**
239 * @param object $row
240 * @access private
241 */
242 function Revision( $row ) {
243 if( is_object( $row ) ) {
244 $this->mId = intval( $row->rev_id );
245 $this->mPage = intval( $row->rev_page );
246 $this->mTextId = intval( $row->rev_text_id );
247 $this->mComment = $row->rev_comment;
248 $this->mUserText = $row->rev_user_text;
249 $this->mUser = intval( $row->rev_user );
250 $this->mMinorEdit = intval( $row->rev_minor_edit );
251 $this->mTimestamp = $row->rev_timestamp;
252 $this->mDeleted = intval( $row->rev_deleted );
253
254 if( isset( $row->page_latest ) ) {
255 $this->mCurrent = ( $row->rev_id == $row->page_latest );
256 $this->mTitle = Title::makeTitle( $row->page_namespace,
257 $row->page_title );
258 } else {
259 $this->mCurrent = false;
260 $this->mTitle = null;
261 }
262
263 if( isset( $row->old_text ) ) {
264 $this->mText = $this->getRevisionText( $row );
265 } else {
266 $this->mText = null;
267 }
268 } elseif( is_array( $row ) ) {
269 // Build a new revision to be saved...
270 global $wgUser;
271
272 $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
273 $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
274 $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
275 $this->mUserText = isset( $row['user_text'] ) ? strval( $row['user_text'] ) : $wgUser->getName();
276 $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
277 $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
278 $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW );
279 $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
280
281 // Enforce spacing trimming on supplied text
282 $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
283 $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
284
285 $this->mTitle = null; # Load on demand if needed
286 $this->mCurrent = false;
287 } else {
288 wfDebugDieBacktrace( 'Revision constructor passed invalid row format.' );
289 }
290 }
291
292 /**#@+
293 * @access public
294 */
295
296 /**
297 * @return int
298 */
299 function getId() {
300 return $this->mId;
301 }
302
303 /**
304 * @return int
305 */
306 function getTextId() {
307 return $this->mTextId;
308 }
309
310 /**
311 * Returns the title of the page associated with this entry.
312 * @return Title
313 */
314 function getTitle() {
315 if( isset( $this->mTitle ) ) {
316 return $this->mTitle;
317 }
318 $dbr =& wfGetDB( DB_SLAVE );
319 $row = $dbr->selectRow(
320 array( 'page', 'revision' ),
321 array( 'page_namespace', 'page_title' ),
322 array( 'page_id=rev_page',
323 'rev_id' => $this->mId ),
324 'Revision::getTItle' );
325 if( $row ) {
326 $this->mTitle = Title::makeTitle( $row->page_namespace,
327 $row->page_title );
328 }
329 return $this->mTitle;
330 }
331
332 /**
333 * @return int
334 */
335 function getPage() {
336 return $this->mPage;
337 }
338
339 /**
340 * Fetch revision's user id if it's available to all users
341 * @return int
342 */
343 function getUser() {
344 if( $this->isDeleted( self::MW_REV_DELETED_USER ) ) {
345 return 0;
346 } else {
347 return $this->mUser;
348 }
349 }
350
351 /**
352 * Fetch revision's user id without regard for the current user's permissions
353 * @return string
354 */
355 function getRawUser() {
356 return $this->mUser;
357 }
358
359 /**
360 * Fetch revision's username if it's available to all users
361 * @return string
362 */
363 function getUserText() {
364 if( $this->isDeleted( self::MW_REV_DELETED_USER ) ) {
365 return "";
366 } else {
367 return $this->mUserText;
368 }
369 }
370
371 /**
372 * Fetch revision's username without regard for view restrictions
373 * @return string
374 */
375 function getRawUserText() {
376 return $this->mUserText;
377 }
378
379 /**
380 * Fetch revision comment if it's available to all users
381 * @return string
382 */
383 function getComment() {
384 if( $this->isDeleted( self::MW_REV_DELETED_COMMENT ) ) {
385 return "";
386 } else {
387 return $this->mComment;
388 }
389 }
390
391 /**
392 * Fetch revision comment without regard for the current user's permissions
393 * @return string
394 */
395 function getRawComment() {
396 return $this->mComment;
397 }
398
399 /**
400 * @return bool
401 */
402 function isMinor() {
403 return (bool)$this->mMinorEdit;
404 }
405
406 /**
407 * int $field one of MW_REV_DELETED_* bitfield constants
408 * @return bool
409 */
410 function isDeleted( $field ) {
411 return ($this->mDeleted & $field) == $field;
412 }
413
414 /**
415 * Fetch revision text if it's available to all users
416 * @return string
417 */
418 function getText() {
419 if( $this->isDeleted( self::MW_REV_DELETED_TEXT ) ) {
420 return "";
421 } else {
422 return $this->getRawText();
423 }
424 }
425
426 /**
427 * Fetch revision text without regard for view restrictions
428 * @return string
429 */
430 function getRawText() {
431 if( is_null( $this->mText ) ) {
432 // Revision text is immutable. Load on demand:
433 $this->mText = $this->loadText();
434 }
435 return $this->mText;
436 }
437
438 /**
439 * @return string
440 */
441 function getTimestamp() {
442 return wfTimestamp(TS_MW, $this->mTimestamp);
443 }
444
445 /**
446 * @return bool
447 */
448 function isCurrent() {
449 return $this->mCurrent;
450 }
451
452 /**
453 * @return Revision
454 */
455 function getPrevious() {
456 $prev = $this->mTitle->getPreviousRevisionID( $this->mId );
457 if ( $prev ) {
458 return Revision::newFromTitle( $this->mTitle, $prev );
459 } else {
460 return null;
461 }
462 }
463
464 /**
465 * @return Revision
466 */
467 function getNext() {
468 $next = $this->mTitle->getNextRevisionID( $this->mId );
469 if ( $next ) {
470 return Revision::newFromTitle( $this->mTitle, $next );
471 } else {
472 return null;
473 }
474 }
475 /**#@-*/
476
477 /**
478 * Get revision text associated with an old or archive row
479 * $row is usually an object from wfFetchRow(), both the flags and the text
480 * field must be included
481 * @static
482 * @param integer $row Id of a row
483 * @param string $prefix table prefix (default 'old_')
484 * @return string $text|false the text requested
485 */
486 function getRevisionText( $row, $prefix = 'old_' ) {
487 $fname = 'Revision::getRevisionText';
488 wfProfileIn( $fname );
489
490 # Get data
491 $textField = $prefix . 'text';
492 $flagsField = $prefix . 'flags';
493
494 if( isset( $row->$flagsField ) ) {
495 $flags = explode( ',', $row->$flagsField );
496 } else {
497 $flags = array();
498 }
499
500 if( isset( $row->$textField ) ) {
501 $text = $row->$textField;
502 } else {
503 wfProfileOut( $fname );
504 return false;
505 }
506
507 # Use external methods for external objects, text in table is URL-only then
508 if ( in_array( 'external', $flags ) ) {
509 $url=$text;
510 @list($proto,$path)=explode('://',$url,2);
511 if ($path=="") {
512 wfProfileOut( $fname );
513 return false;
514 }
515 require_once('ExternalStore.php');
516 $text=ExternalStore::fetchFromURL($url);
517 }
518
519 // If the text was fetched without an error, convert it
520 if ( $text !== false ) {
521 if( in_array( 'gzip', $flags ) ) {
522 # Deal with optional compression of archived pages.
523 # This can be done periodically via maintenance/compressOld.php, and
524 # as pages are saved if $wgCompressRevisions is set.
525 $text = gzinflate( $text );
526 }
527
528 if( in_array( 'object', $flags ) ) {
529 # Generic compressed storage
530 $obj = unserialize( $text );
531 if ( !is_object( $obj ) ) {
532 // Invalid object
533 wfProfileOut( $fname );
534 return false;
535 }
536 $text = $obj->getText();
537 }
538
539 global $wgLegacyEncoding;
540 if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) {
541 # Old revisions kept around in a legacy encoding?
542 # Upconvert on demand.
543 global $wgInputEncoding, $wgContLang;
544 $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding . '//IGNORE', $text );
545 }
546 }
547 wfProfileOut( $fname );
548 return $text;
549 }
550
551 /**
552 * If $wgCompressRevisions is enabled, we will compress data.
553 * The input string is modified in place.
554 * Return value is the flags field: contains 'gzip' if the
555 * data is compressed, and 'utf-8' if we're saving in UTF-8
556 * mode.
557 *
558 * @static
559 * @param mixed $text reference to a text
560 * @return string
561 */
562 function compressRevisionText( &$text ) {
563 global $wgCompressRevisions;
564 $flags = array();
565
566 # Revisions not marked this way will be converted
567 # on load if $wgLegacyCharset is set in the future.
568 $flags[] = 'utf-8';
569
570 if( $wgCompressRevisions ) {
571 if( function_exists( 'gzdeflate' ) ) {
572 $text = gzdeflate( $text );
573 $flags[] = 'gzip';
574 } else {
575 wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
576 }
577 }
578 return implode( ',', $flags );
579 }
580
581 /**
582 * Insert a new revision into the database, returning the new revision ID
583 * number on success and dies horribly on failure.
584 *
585 * @param Database $dbw
586 * @return int
587 */
588 function insertOn( &$dbw ) {
589 global $wgDefaultExternalStore;
590
591 $fname = 'Revision::insertOn';
592 wfProfileIn( $fname );
593
594 $data = $this->mText;
595 $flags = Revision::compressRevisionText( $data );
596
597 # Write to external storage if required
598 if ( $wgDefaultExternalStore ) {
599 if ( is_array( $wgDefaultExternalStore ) ) {
600 // Distribute storage across multiple clusters
601 $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)];
602 } else {
603 $store = $wgDefaultExternalStore;
604 }
605 require_once('ExternalStore.php');
606 // Store and get the URL
607 $data = ExternalStore::insert( $store, $data );
608 if ( !$data ) {
609 # This should only happen in the case of a configuration error, where the external store is not valid
610 wfDebugDieBacktrace( "Unable to store text to external storage $store" );
611 }
612 if ( $flags ) {
613 $flags .= ',';
614 }
615 $flags .= 'external';
616 }
617
618 # Record the text (or external storage URL) to the text table
619 if( !isset( $this->mTextId ) ) {
620 $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
621 $dbw->insert( 'text',
622 array(
623 'old_id' => $old_id,
624 'old_text' => $data,
625 'old_flags' => $flags,
626 ), $fname
627 );
628 $this->mTextId = $dbw->insertId();
629 }
630
631 # Record the edit in revisions
632 $rev_id = isset( $this->mId )
633 ? $this->mId
634 : $dbw->nextSequenceValue( 'rev_rev_id_val' );
635 $dbw->insert( 'revision',
636 array(
637 'rev_id' => $rev_id,
638 'rev_page' => $this->mPage,
639 'rev_text_id' => $this->mTextId,
640 'rev_comment' => $this->mComment,
641 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
642 'rev_user' => $this->mUser,
643 'rev_user_text' => $this->mUserText,
644 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
645 'rev_deleted' => $this->mDeleted,
646 ), $fname
647 );
648
649 $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId();
650 wfProfileOut( $fname );
651 return $this->mId;
652 }
653
654 /**
655 * Lazy-load the revision's text.
656 * Currently hardcoded to the 'text' table storage engine.
657 *
658 * @return string
659 * @access private
660 */
661 function loadText() {
662 $fname = 'Revision::loadText';
663 wfProfileIn( $fname );
664
665 $dbr =& wfGetDB( DB_SLAVE );
666 $row = $dbr->selectRow( 'text',
667 array( 'old_text', 'old_flags' ),
668 array( 'old_id' => $this->getTextId() ),
669 $fname);
670
671 if( !$row ) {
672 $dbw =& wfGetDB( DB_MASTER );
673 $row = $dbw->selectRow( 'text',
674 array( 'old_text', 'old_flags' ),
675 array( 'old_id' => $this->getTextId() ),
676 $fname);
677 }
678
679 $text = Revision::getRevisionText( $row );
680 wfProfileOut( $fname );
681
682 return $text;
683 }
684
685 /**
686 * Create a new null-revision for insertion into a page's
687 * history. This will not re-save the text, but simply refer
688 * to the text from the previous version.
689 *
690 * Such revisions can for instance identify page rename
691 * operations and other such meta-modifications.
692 *
693 * @param Database $dbw
694 * @param int $pageId ID number of the page to read from
695 * @param string $summary
696 * @param bool $minor
697 * @return Revision
698 */
699 function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
700 $fname = 'Revision::newNullRevision';
701 wfProfileIn( $fname );
702
703 $current = $dbw->selectRow(
704 array( 'page', 'revision' ),
705 array( 'page_latest', 'rev_text_id' ),
706 array(
707 'page_id' => $pageId,
708 'page_latest=rev_id',
709 ),
710 $fname );
711
712 if( $current ) {
713 $revision = new Revision( array(
714 'page' => $pageId,
715 'comment' => $summary,
716 'minor_edit' => $minor,
717 'text_id' => $current->rev_text_id,
718 ) );
719 } else {
720 $revision = null;
721 }
722
723 wfProfileOut( $fname );
724 return $revision;
725 }
726
727 /**
728 * Determine if the current user is allowed to view a particular
729 * field of this revision, if it's marked as deleted.
730 * @param int $field one of self::MW_REV_DELETED_TEXT,
731 * self::MW_REV_DELETED_COMMENT,
732 * self::MW_REV_DELETED_USER
733 * @return bool
734 */
735 function userCan( $field ) {
736 if( ( $this->mDeleted & $field ) == $field ) {
737 global $wgUser;
738 $permission = ( $this->mDeleted & self::MW_REV_DELETED_RESTRICTED ) == self::MW_REV_DELETED_RESTRICTED
739 ? 'hiderevision'
740 : 'deleterevision';
741 wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
742 return $wgUser->isAllowed( $permission );
743 } else {
744 return true;
745 }
746 }
747
748 }
749
750 ?>