* (bug 2053) Move comment whitespace trimming from edit page to save;
[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 /**
17 * Load a page revision from a given revision ID number.
18 * Returns null if no such revision can be found.
19 *
20 * @param int $id
21 * @static
22 * @access public
23 */
24 function &newFromId( $id ) {
25 return Revision::newFromConds(
26 array( 'page_id=rev_page',
27 'rev_id' => IntVal( $id ) ) );
28 }
29
30 /**
31 * Load either the current, or a specified, revision
32 * that's attached to a given title. If not attached
33 * to that title, will return null.
34 *
35 * @param Title $title
36 * @param int $id
37 * @return Revision
38 * @access public
39 * @static
40 */
41 function &newFromTitle( &$title, $id = 0 ) {
42 if( $id ) {
43 $matchId = IntVal( $id );
44 } else {
45 $matchId = 'page_latest';
46 }
47 return Revision::newFromConds(
48 array( "rev_id=$matchId",
49 'page_id=rev_page',
50 'page_namespace' => $title->getNamespace(),
51 'page_title' => $title->getDbkey() ) );
52 }
53
54 /**
55 * Load either the current, or a specified, revision
56 * that's attached to a given page. If not attached
57 * to that page, will return null.
58 *
59 * @param Database $db
60 * @param int $pageid
61 * @param int $id
62 * @return Revision
63 * @access public
64 */
65 function &loadFromPageId( &$db, $pageid, $id = 0 ) {
66 if( $id ) {
67 $matchId = IntVal( $id );
68 } else {
69 $matchId = 'page_latest';
70 }
71 return Revision::loadFromConds(
72 $db,
73 array( "rev_id=$matchId",
74 'rev_page' => IntVal( $pageid ),
75 'page_id=rev_page' ) );
76 }
77
78 /**
79 * Load the revision for the given title with the given timestamp.
80 * WARNING: Timestamps may in some circumstances not be unique,
81 * so this isn't the best key to use.
82 *
83 * @param Database $db
84 * @param Title $title
85 * @param string $timestamp
86 * @return Revision
87 * @access public
88 * @static
89 */
90 function &loadFromTimestamp( &$db, &$title, $timestamp ) {
91 return Revision::loadFromConds(
92 $db,
93 array( 'rev_timestamp' => $db->timestamp( $timestamp ),
94 'page_id=rev_page',
95 'page_namespace' => $title->getNamespace(),
96 'page_title' => $title->getDbkey() ) );
97 }
98
99 /**
100 * Given a set of conditions, fetch a revision.
101 *
102 * @param array $conditions
103 * @return Revision
104 * @static
105 * @access private
106 */
107 function &newFromConds( $conditions ) {
108 $db =& wfGetDB( DB_SLAVE );
109 return Revision::loadFromConds( $db, $conditions );
110 }
111
112 /**
113 * Given a set of conditions, fetch a revision from
114 * the given database connection.
115 *
116 * @param Database $db
117 * @param array $conditions
118 * @return Revision
119 * @static
120 * @access private
121 */
122 function &loadFromConds( &$db, $conditions ) {
123 $res =& Revision::fetchFromConds( $db, $conditions );
124 if( $res ) {
125 $row = $res->fetchObject();
126 $res->free();
127 if( $row ) {
128 return new Revision( $row );
129 }
130 }
131 return null;
132 }
133
134 /**
135 * Return a wrapper for a series of database rows to
136 * fetch all of a given page's revisions in turn.
137 * Each row can be fed to the constructor to get objects.
138 *
139 * @param Title $title
140 * @return ResultWrapper
141 * @static
142 * @access public
143 */
144 function &fetchAllRevisions( &$title ) {
145 return Revision::fetchFromConds(
146 wfGetDB( DB_SLAVE ),
147 array( 'page_namespace' => $title->getNamespace(),
148 'page_title' => $title->getDbkey(),
149 'page_id=rev_page' ) );
150 }
151
152 /**
153 * Return a wrapper for a series of database rows to
154 * fetch all of a given page's revisions in turn.
155 * Each row can be fed to the constructor to get objects.
156 *
157 * @param Title $title
158 * @return ResultWrapper
159 * @static
160 * @access public
161 */
162 function &fetchRevision( &$title ) {
163 return Revision::fetchFromConds(
164 wfGetDB( DB_SLAVE ),
165 array( 'rev_id=page_latest',
166 'page_namespace' => $title->getNamespace(),
167 'page_title' => $title->getDbkey(),
168 'page_id=rev_page' ) );
169 }
170
171 /**
172 * Given a set of conditions, return a ResultWrapper
173 * which will return matching database rows with the
174 * fields necessary to build Revision objects.
175 *
176 * @param Database $db
177 * @param array $conditions
178 * @return ResultWrapper
179 * @static
180 * @access private
181 */
182 function &fetchFromConds( &$db, $conditions ) {
183 $res = $db->select(
184 array( 'page', 'revision' ),
185 array( 'page_namespace',
186 'page_title',
187 'page_latest',
188 'rev_id',
189 'rev_page',
190 'rev_text_id',
191 'rev_comment',
192 'rev_user_text',
193 'rev_user',
194 'rev_minor_edit',
195 'rev_timestamp',
196 'rev_deleted' ),
197 $conditions,
198 'Revision::fetchRow',
199 array( 'LIMIT' => 1 ) );
200 return $db->resultObject( $res );
201 }
202
203 /**
204 * @param object $row
205 * @access private
206 */
207 function Revision( $row ) {
208 if( is_object( $row ) ) {
209 $this->mId = IntVal( $row->rev_id );
210 $this->mPage = IntVal( $row->rev_page );
211 $this->mTextId = IntVal( $row->rev_text_id );
212 $this->mComment = $row->rev_comment;
213 $this->mUserText = $row->rev_user_text;
214 $this->mUser = IntVal( $row->rev_user );
215 $this->mMinorEdit = IntVal( $row->rev_minor_edit );
216 $this->mTimestamp = $row->rev_timestamp;
217 $this->mDeleted = IntVal( $row->rev_deleted );
218
219 $this->mCurrent = ( $row->rev_id == $row->page_latest );
220 $this->mTitle = Title::makeTitle( $row->page_namespace,
221 $row->page_title );
222
223 if( isset( $row->old_text ) ) {
224 $this->mText = $this->getRevisionText( $row );
225 } else {
226 $this->mText = null;
227 }
228 } elseif( is_array( $row ) ) {
229 // Build a new revision to be saved...
230 global $wgUser;
231
232 $this->mId = isset( $row['id'] ) ? IntVal( $row['id'] ) : null;
233 $this->mPage = isset( $row['page'] ) ? IntVal( $row['page'] ) : null;
234 $this->mTextId = isset( $row['text_id'] ) ? IntVal( $row['text_id'] ) : null;
235 $this->mUserText = isset( $row['user_text'] ) ? StrVal( $row['user_text'] ) : $wgUser->getName();
236 $this->mUser = isset( $row['user'] ) ? IntVal( $row['user'] ) : $wgUser->getId();
237 $this->mMinorEdit = isset( $row['minor_edit'] ) ? IntVal( $row['minor_edit'] ) : 0;
238 $this->mTimestamp = isset( $row['timestamp'] ) ? StrVal( $row['timestamp'] ) : wfTimestamp( TS_MW );
239 $this->mDeleted = isset( $row['deleted'] ) ? IntVal( $row['deleted'] ) : 0;
240
241 // Enforce spacing trimming on supplied text
242 $this->mComment = isset( $row['comment'] ) ? trim( StrVal( $row['comment'] ) ) : null;
243 $this->mText = isset( $row['text'] ) ? rtrim( StrVal( $row['text'] ) ) : null;
244
245 $this->mTitle = null; # Load on demand if needed
246 $this->mCurrent = false;
247 } else {
248 wfDebugDieBacktrace( 'Revision constructor passed invalid row format.' );
249 }
250 }
251
252 /**#@+
253 * @access public
254 */
255
256 /**
257 * @return int
258 */
259 function getId() {
260 return $this->mId;
261 }
262
263 /**
264 * @return int
265 */
266 function getTextId() {
267 return $this->mTextId;
268 }
269
270 /**
271 * Returns the title of the page associated with this entry.
272 * @return Title
273 */
274 function &getTitle() {
275 if( isset( $this->mTitle ) ) {
276 return $this->mTitle;
277 }
278 $dbr =& wfGetDB( DB_SLAVE );
279 $row = $dbr->selectRow(
280 array( 'page', 'revision' ),
281 array( 'page_namespace', 'page_title' ),
282 array( 'page_id=rev_page',
283 'rev_id' => $this->mId ),
284 'Revision::getTItle' );
285 if( $row ) {
286 $this->mTitle =& Title::makeTitle( $row->page_namespace,
287 $row->page_title );
288 }
289 return $this->mTitle;
290 }
291
292 /**
293 * @return int
294 */
295 function getPage() {
296 return $this->mPage;
297 }
298
299 /**
300 * @return int
301 */
302 function getUser() {
303 return $this->mUser;
304 }
305
306 /**
307 * @return string
308 */
309 function getUserText() {
310 return $this->mUserText;
311 }
312
313 /**
314 * @return string
315 */
316 function getComment() {
317 return $this->mComment;
318 }
319
320 /**
321 * @return bool
322 */
323 function isMinor() {
324 return (bool)$this->mMinorEdit;
325 }
326
327 /**
328 * @return bool
329 */
330 function isDeleted() {
331 return (bool)$this->mDeleted;
332 }
333
334 /**
335 * @return string
336 */
337 function getText() {
338 if( is_null( $this->mText ) ) {
339 // Revision text is immutable. Load on demand:
340 $this->mText = $this->loadText();
341 }
342 return $this->mText;
343 }
344
345 /**
346 * @return string
347 */
348 function getTimestamp() {
349 return $this->mTimestamp;
350 }
351
352 /**
353 * @return bool
354 */
355 function isCurrent() {
356 return $this->mCurrent;
357 }
358
359 /**
360 * @return Revision
361 */
362 function &getPrevious() {
363 $prev = $this->mTitle->getPreviousRevisionID( $this->mId );
364 return Revision::newFromTitle( $this->mTitle, $prev );
365 }
366
367 /**
368 * @return Revision
369 */
370 function &getNext() {
371 $next = $this->mTitle->getNextRevisionID( $this->mId );
372 return Revision::newFromTitle( $this->mTitle, $next );
373 }
374 /**#@-*/
375
376 /**
377 * Get revision text associated with an old or archive row
378 * $row is usually an object from wfFetchRow(), both the flags and the text
379 * field must be included
380 * @static
381 * @param integer $row Id of a row
382 * @param string $prefix table prefix (default 'old_')
383 * @return string $text|false the text requested
384 */
385 function getRevisionText( $row, $prefix = 'old_' ) {
386 $fname = 'Revision::getRevisionText';
387 wfProfileIn( $fname );
388
389 # Get data
390 $textField = $prefix . 'text';
391 $flagsField = $prefix . 'flags';
392
393 if( isset( $row->$flagsField ) ) {
394 $flags = explode( ',', $row->$flagsField );
395 } else {
396 $flags = array();
397 }
398
399 if( isset( $row->$textField ) ) {
400 $text = $row->$textField;
401 } else {
402 wfProfileOut( $fname );
403 return false;
404 }
405
406 # Use external methods for external objects, text in table is URL-only then
407 if ( in_array( 'external', $flags ) ) {
408 $url=$text;
409 @list($proto,$path)=explode('://',$url,2);
410 if ($path=="") {
411 wfProfileOut( $fname );
412 return false;
413 }
414 require_once('ExternalStore.php');
415 $text=ExternalStore::fetchFromURL($url);
416 }
417
418 if( in_array( 'gzip', $flags ) ) {
419 # Deal with optional compression of archived pages.
420 # This can be done periodically via maintenance/compressOld.php, and
421 # as pages are saved if $wgCompressRevisions is set.
422 $text = gzinflate( $text );
423 }
424
425 if( in_array( 'object', $flags ) ) {
426 # Generic compressed storage
427 $obj = unserialize( $text );
428
429 # Bugger, corrupted my test database by double-serializing
430 if ( !is_object( $obj ) ) {
431 $obj = unserialize( $obj );
432 }
433
434 $text = $obj->getText();
435 }
436
437 global $wgLegacyEncoding;
438 if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) {
439 # Old revisions kept around in a legacy encoding?
440 # Upconvert on demand.
441 global $wgInputEncoding, $wgContLang;
442 $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text );
443 }
444 wfProfileOut( $fname );
445 return $text;
446 }
447
448 /**
449 * If $wgCompressRevisions is enabled, we will compress data.
450 * The input string is modified in place.
451 * Return value is the flags field: contains 'gzip' if the
452 * data is compressed, and 'utf-8' if we're saving in UTF-8
453 * mode.
454 *
455 * @static
456 * @param mixed $text reference to a text
457 * @return string
458 */
459 function compressRevisionText( &$text ) {
460 global $wgCompressRevisions;
461 $flags = array();
462
463 # Revisions not marked this way will be converted
464 # on load if $wgLegacyCharset is set in the future.
465 $flags[] = 'utf-8';
466
467 if( $wgCompressRevisions ) {
468 if( function_exists( 'gzdeflate' ) ) {
469 $text = gzdeflate( $text );
470 $flags[] = 'gzip';
471 } else {
472 wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
473 }
474 }
475 return implode( ',', $flags );
476 }
477
478 /**
479 * Insert a new revision into the database, returning the new revision ID
480 * number on success and dies horribly on failure.
481 *
482 * @param Database $dbw
483 * @return int
484 */
485 function insertOn( &$dbw ) {
486 $fname = 'Revision::insertOn';
487 wfProfileIn( $fname );
488
489 $mungedText = $this->mText;
490 $flags = Revision::compressRevisionText( $mungedText );
491
492 # Record the text to the text table
493 if( !isset( $this->mTextId ) ) {
494 $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
495 $dbw->insert( 'text',
496 array(
497 'old_id' => $old_id,
498 'old_text' => $mungedText,
499 'old_flags' => $flags,
500 ), $fname
501 );
502 $this->mTextId = $dbw->insertId();
503 }
504
505 # Record the edit in revisions
506 $rev_id = isset( $this->mId )
507 ? $this->mId
508 : $dbw->nextSequenceValue( 'rev_rev_id_val' );
509 $dbw->insert( 'revision',
510 array(
511 'rev_id' => $rev_id,
512 'rev_page' => $this->mPage,
513 'rev_text_id' => $this->mTextId,
514 'rev_comment' => $this->mComment,
515 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
516 'rev_user' => $this->mUser,
517 'rev_user_text' => $this->mUserText,
518 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
519 'rev_deleted' => $this->mDeleted,
520 ), $fname
521 );
522
523 $this->mId = $dbw->insertId();
524
525 wfProfileOut( $fname );
526 return $this->mId;
527 }
528
529 /**
530 * Lazy-load the revision's text.
531 * Currently hardcoded to the 'text' table storage engine.
532 *
533 * @return string
534 * @access private
535 */
536 function loadText() {
537 $fname = 'Revision::loadText';
538 wfProfileIn( $fname );
539
540 $dbr =& wfGetDB( DB_SLAVE );
541 $row = $dbr->selectRow( 'text',
542 array( 'old_text', 'old_flags' ),
543 array( 'old_id' => $this->getTextId() ),
544 $fname);
545
546 $text = Revision::getRevisionText( $row );
547 wfProfileOut( $fname );
548
549 return $text;
550 }
551
552 /**
553 * Create a new null-revision for insertion into a page's
554 * history. This will not re-save the text, but simply refer
555 * to the text from the previous version.
556 *
557 * Such revisions can for instance identify page rename
558 * operations and other such meta-modifications.
559 *
560 * @param Database $dbw
561 * @param int $pageId ID number of the page to read from
562 * @param string $summary
563 * @param bool $minor
564 * @return Revision
565 */
566 function &newNullRevision( &$dbw, $pageId, $summary, $minor ) {
567 $fname = 'Revision::newNullRevision';
568 wfProfileIn( $fname );
569
570 $current = $dbw->selectRow(
571 array( 'page', 'revision' ),
572 array( 'page_latest', 'rev_text_id' ),
573 array(
574 'page_id' => $pageId,
575 'page_latest=rev_id',
576 ),
577 $fname );
578
579 if( $current ) {
580 $revision = new Revision( array(
581 'page' => $pageId,
582 'comment' => $summary,
583 'minor_edit' => $minor,
584 'text_id' => $current->rev_text_id,
585 ) );
586 } else {
587 $revision = null;
588 }
589
590 wfProfileOut( $fname );
591 return $revision;
592 }
593
594 }
595 ?>