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