* (bug 4873) Don't overwrite the subtitle navigation when viewing a redirect page...
[lhc/web/wiklou.git] / includes / PageHistory.php
1 <?php
2 /**
3 * Page history
4 *
5 * Split off from Article.php and Skin.php, 2003-12-22
6 * @package MediaWiki
7 */
8
9 define('DIR_PREV', 0);
10 define('DIR_NEXT', 1);
11
12 /**
13 * This class handles printing the history page for an article. In order to
14 * be efficient, it uses timestamps rather than offsets for paging, to avoid
15 * costly LIMIT,offset queries.
16 *
17 * Construct it by passing in an Article, and call $h->history() to print the
18 * history.
19 *
20 * @package MediaWiki
21 */
22
23 class PageHistory {
24 var $mArticle, $mTitle, $mSkin;
25 var $lastdate;
26 var $linesonpage;
27 var $mNotificationTimestamp;
28 var $mLatestId = null;
29
30 /**
31 * Construct a new PageHistory.
32 *
33 * @param Article $article
34 * @returns nothing
35 */
36 function PageHistory($article) {
37 global $wgUser;
38
39 $this->mArticle =& $article;
40 $this->mTitle =& $article->mTitle;
41 $this->mNotificationTimestamp = NULL;
42 $this->mSkin = $wgUser->getSkin();
43
44 $this->defaultLimit = 50;
45 }
46
47 /**
48 * Print the history page for an article.
49 *
50 * @returns nothing
51 */
52 function history() {
53 global $wgOut, $wgRequest, $wgTitle;
54
55 /*
56 * Allow client caching.
57 */
58
59 if( $wgOut->checkLastModified( $this->mArticle->getTimestamp() ) )
60 /* Client cache fresh and headers sent, nothing more to do. */
61 return;
62
63 $fname = 'PageHistory::history';
64 wfProfileIn( $fname );
65
66 /*
67 * Setup page variables.
68 */
69 $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
70 $wgOut->setSubtitle( wfMsg( 'revhistory' ) );
71 $wgOut->setArticleFlag( false );
72 $wgOut->setArticleRelated( true );
73 $wgOut->setRobotpolicy( 'noindex,nofollow' );
74
75 /*
76 * Fail if article doesn't exist.
77 */
78 if( !$this->mTitle->exists() ) {
79 $wgOut->addWikiText( wfMsg( 'nohistory' ) );
80 wfProfileOut( $fname );
81 return;
82 }
83
84 $dbr =& wfGetDB(DB_SLAVE);
85
86 /*
87 * Extract limit, the number of revisions to show, and
88 * offset, the timestamp to begin at, from the URL.
89 */
90 list( $limit, $offset ) = wfCheckLimits( $this->defaultLimit );
91
92 /* Offset must be an integral. */
93 if (!strlen($offset) || !preg_match("/^[0-9]+$/", $offset))
94 $offset = 0;
95 # $offset = $dbr->timestamp($offset);
96 $dboffset = $offset === 0 ? 0 : $dbr->timestamp($offset);
97 /*
98 * "go=last" means to jump to the last history page.
99 */
100 if (($gowhere = $wgRequest->getText("go")) !== NULL) {
101 switch ($gowhere) {
102 case "first":
103 if (($lastid = $this->getLastOffsetForPaging($this->mTitle->getArticleID(), $limit)) === NULL)
104 break;
105 $gourl = $wgTitle->getLocalURL("action=history&limit={$limit}&offset=".
106 wfTimestamp(TS_MW, $lastid));
107 break;
108 default:
109 $gourl = NULL;
110 }
111
112 if (!is_null($gourl)) {
113 $wgOut->redirect($gourl);
114 return;
115 }
116 }
117
118 /*
119 * Fetch revisions.
120 *
121 * If the user clicked "previous", we retrieve the revisions backwards,
122 * then reverse them. This is to avoid needing to know the timestamp of
123 * previous revisions when generating the URL.
124 */
125 $direction = $this->getDirection();
126 $revisions = $this->fetchRevisions($limit, $dboffset, $direction);
127 $navbar = $this->makeNavbar($revisions, $offset, $limit, $direction);
128
129 /*
130 * We fetch one more revision than needed to get the timestamp of the
131 * one after this page (and to know if it exists).
132 *
133 * linesonpage stores the actual number of lines.
134 */
135 if (count($revisions) < $limit + 1)
136 $this->linesonpage = count($revisions);
137 else
138 $this->linesonpage = count($revisions) - 1;
139
140 /* Un-reverse revisions */
141 if ($direction == DIR_PREV)
142 $revisions = array_reverse($revisions);
143
144 /*
145 * Print the top navbar.
146 */
147 $s = $navbar;
148 $s .= $this->beginHistoryList();
149 $counter = 1;
150
151 /*
152 * Print each revision, excluding the one-past-the-end, if any.
153 */
154 foreach (array_slice($revisions, 0, $limit) as $i => $line) {
155 $latest = !$i && $offset == 0;
156 $firstInList = !$i;
157 $next = isset( $revisions[$i + 1] ) ? $revisions[$i + 1 ] : null;
158 $s .= $this->historyLine($line, $next, $counter, $this->getNotificationTimestamp(), $latest, $firstInList);
159 $counter++;
160 }
161
162 /*
163 * End navbar.
164 */
165 $s .= $this->endHistoryList();
166 $s .= $navbar;
167
168 $wgOut->addHTML( $s );
169 wfProfileOut( $fname );
170 }
171
172 /** @todo document */
173 function beginHistoryList() {
174 global $wgTitle;
175 $this->lastdate = '';
176 $s = wfMsgExt( 'histlegend', array( 'parse') );
177 $s .= '<form action="' . $wgTitle->escapeLocalURL( '-' ) . '" method="get">';
178 $prefixedkey = htmlspecialchars($wgTitle->getPrefixedDbKey());
179
180 // The following line is SUPPOSED to have double-quotes around the
181 // $prefixedkey variable, because htmlspecialchars() doesn't escape
182 // single-quotes.
183 //
184 // On at least two occasions people have changed it to single-quotes,
185 // which creates invalid HTML and incorrect display of the resulting
186 // link.
187 //
188 // Please do not break this a third time. Thank you for your kind
189 // consideration and cooperation.
190 //
191 $s .= "<input type='hidden' name='title' value=\"{$prefixedkey}\" />\n";
192
193 $s .= $this->submitButton();
194 $s .= '<ul id="pagehistory">' . "\n";
195 return $s;
196 }
197
198 /** @todo document */
199 function endHistoryList() {
200 $s = '</ul>';
201 $s .= $this->submitButton( array( 'id' => 'historysubmit' ) );
202 $s .= '</form>';
203 return $s;
204 }
205
206 /** @todo document */
207 function submitButton( $bits = array() ) {
208 return ( $this->linesonpage > 0 )
209 ? wfElement( 'input', array_merge( $bits,
210 array(
211 'class' => 'historysubmit',
212 'type' => 'submit',
213 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
214 'title' => wfMsg( 'tooltip-compareselectedversions' ),
215 'value' => wfMsg( 'compareselectedversions' ),
216 ) ) )
217 : '';
218 }
219
220 /** @todo document */
221 function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) {
222 global $wgUser;
223 $rev = new Revision( $row );
224
225 $s = '<li>';
226 $curlink = $this->curLink( $rev, $latest );
227 $lastlink = $this->lastLink( $rev, $next, $counter );
228 $arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
229 $link = $this->revLink( $rev );
230 $user = $this->mSkin->revUserLink( $rev );
231
232 $s .= "($curlink) ($lastlink) $arbitrary";
233
234 if( $wgUser->isAllowed( 'deleterevision' ) ) {
235 $revdel = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' );
236 if( $firstInList ) {
237 // We don't currently handle well changing the top revision's settings
238 $del = wfMsgHtml( 'rev-delundel' );
239 } else {
240 $del = $this->mSkin->makeKnownLinkObj( $revdel,
241 wfMsg( 'rev-delundel' ),
242 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
243 '&oldid=' . urlencode( $rev->getId() ) );
244 }
245 $s .= "(<small>$del</small>) ";
246 }
247
248 $s .= " $link <span class='history-user'>$user</span>";
249
250 if( $row->rev_minor_edit ) {
251 $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
252 }
253
254 $s .= $this->mSkin->revComment( $rev );
255 if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) {
256 $s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>';
257 }
258 if( $row->rev_deleted & MW_REV_DELETED_TEXT ) {
259 $s .= ' ' . wfMsgHtml( 'deletedrev' );
260 }
261 $s .= "</li>\n";
262
263 return $s;
264 }
265
266 /** @todo document */
267 function revLink( $rev ) {
268 global $wgLang;
269 $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true );
270 if( $rev->userCan( MW_REV_DELETED_TEXT ) ) {
271 $link = $this->mSkin->makeKnownLinkObj(
272 $this->mTitle, $date, "oldid=" . $rev->getId() );
273 } else {
274 $link = $date;
275 }
276 if( $rev->isDeleted( MW_REV_DELETED_TEXT ) ) {
277 return '<span class="history-deleted">' . $link . '</span>';
278 }
279 return $link;
280 }
281
282 /** @todo document */
283 function curLink( $rev, $latest ) {
284 $cur = wfMsgExt( 'cur', array( 'escape') );
285 if( $latest || !$rev->userCan( MW_REV_DELETED_TEXT ) ) {
286 return $cur;
287 } else {
288 return $this->mSkin->makeKnownLinkObj(
289 $this->mTitle, $cur,
290 'diff=' . $this->getLatestID() .
291 "&oldid=" . $rev->getId() );
292 }
293 }
294
295 /** @todo document */
296 function lastLink( $rev, $next, $counter ) {
297 $last = wfMsgExt( 'last', array( 'escape' ) );
298 if( is_null( $next ) ) {
299 if( $rev->getTimestamp() == $this->getEarliestOffset() ) {
300 return $last;
301 } else {
302 // Cut off by paging; there are more behind us...
303 return $this->mSkin->makeKnownLinkObj(
304 $this->mTitle,
305 $last,
306 "diff=" . $rev->getId() . "&oldid=prev" );
307 }
308 } elseif( !$rev->userCan( MW_REV_DELETED_TEXT ) ) {
309 return $last;
310 } else {
311 return $this->mSkin->makeKnownLinkObj(
312 $this->mTitle,
313 $last,
314 "diff=" . $rev->getId() . "&oldid={$next->rev_id}"
315 /*,
316 '',
317 '',
318 "tabindex={$counter}"*/ );
319 }
320 }
321
322 /** @todo document */
323 function diffButtons( $rev, $firstInList, $counter ) {
324 if( $this->linesonpage > 1) {
325 $radio = array(
326 'type' => 'radio',
327 'value' => $rev->getId(),
328 # do we really need to flood this on every item?
329 # 'title' => wfMsgHtml( 'selectolderversionfordiff' )
330 );
331
332 if( !$rev->userCan( MW_REV_DELETED_TEXT ) ) {
333 $radio['disabled'] = 'disabled';
334 }
335
336 /** @todo: move title texts to javascript */
337 if ( $firstInList ) {
338 $first = wfElement( 'input', array_merge(
339 $radio,
340 array(
341 'style' => 'visibility:hidden',
342 'name' => 'oldid' ) ) );
343 $checkmark = array( 'checked' => 'checked' );
344 } else {
345 if( $counter == 2 ) {
346 $checkmark = array( 'checked' => 'checked' );
347 } else {
348 $checkmark = array();
349 }
350 $first = wfElement( 'input', array_merge(
351 $radio,
352 $checkmark,
353 array( 'name' => 'oldid' ) ) );
354 $checkmark = array();
355 }
356 $second = wfElement( 'input', array_merge(
357 $radio,
358 $checkmark,
359 array( 'name' => 'diff' ) ) );
360 return $first . $second;
361 } else {
362 return '';
363 }
364 }
365
366 /** @todo document */
367 function getLatestOffset( $id = null ) {
368 if ( $id === null) $id = $this->mTitle->getArticleID();
369 return $this->getExtremeOffset( $id, 'max' );
370 }
371
372 /** @todo document */
373 function getEarliestOffset( $id = null ) {
374 if ( $id === null) $id = $this->mTitle->getArticleID();
375 return $this->getExtremeOffset( $id, 'min' );
376 }
377
378 /** @todo document */
379 function getExtremeOffset( $id, $func ) {
380 $db =& wfGetDB(DB_SLAVE);
381 return $db->selectField( 'revision',
382 "$func(rev_timestamp)",
383 array( 'rev_page' => $id ),
384 'PageHistory::getExtremeOffset' );
385 }
386
387 /** @todo document */
388 function getLatestId() {
389 if( is_null( $this->mLatestId ) ) {
390 $id = $this->mTitle->getArticleID();
391 $db =& wfGetDB(DB_SLAVE);
392 $this->mLatestId = $db->selectField( 'revision',
393 "max(rev_id)",
394 array( 'rev_page' => $id ),
395 'PageHistory::getLatestID' );
396 }
397 return $this->mLatestId;
398 }
399
400 /** @todo document */
401 function getLastOffsetForPaging( $id, $step ) {
402 $fname = 'PageHistory::getLastOffsetForPaging';
403
404 $dbr =& wfGetDB(DB_SLAVE);
405 $res = $dbr->select(
406 'revision',
407 'rev_timestamp',
408 "rev_page=$id",
409 $fname,
410 array('ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $step));
411
412 $n = $dbr->numRows( $res );
413 $last = null;
414 while( $obj = $dbr->fetchObject( $res ) ) {
415 $last = $obj->rev_timestamp;
416 }
417 $dbr->freeResult( $res );
418 return $last;
419 }
420
421 /**
422 * @return returns the direction of browsing watchlist
423 */
424 function getDirection() {
425 global $wgRequest;
426 if ($wgRequest->getText("dir") == "prev")
427 return DIR_PREV;
428 else
429 return DIR_NEXT;
430 }
431
432 /** @todo document */
433 function fetchRevisions($limit, $offset, $direction) {
434 $fname = 'PageHistory::fetchRevisions';
435
436 $dbr =& wfGetDB( DB_SLAVE );
437
438 if ($direction == DIR_PREV)
439 list($dirs, $oper) = array("ASC", ">=");
440 else /* $direction == DIR_NEXT */
441 list($dirs, $oper) = array("DESC", "<=");
442
443 if ($offset)
444 $offsets = array("rev_timestamp $oper '$offset'");
445 else
446 $offsets = array();
447
448 $page_id = $this->mTitle->getArticleID();
449
450 $res = $dbr->select(
451 'revision',
452 array('rev_id', 'rev_page', 'rev_text_id', 'rev_user', 'rev_comment', 'rev_user_text',
453 'rev_timestamp', 'rev_minor_edit', 'rev_deleted'),
454 array_merge(array("rev_page=$page_id"), $offsets),
455 $fname,
456 array('ORDER BY' => "rev_timestamp $dirs",
457 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit)
458 );
459
460 $result = array();
461 while (($obj = $dbr->fetchObject($res)) != NULL)
462 $result[] = $obj;
463
464 return $result;
465 }
466
467 /** @todo document */
468 function getNotificationTimestamp() {
469 global $wgUser, $wgShowUpdatedMarker;
470 $fname = 'PageHistory::getNotficationTimestamp';
471
472 if ($this->mNotificationTimestamp !== NULL)
473 return $this->mNotificationTimestamp;
474
475 if ($wgUser->isAnon() || !$wgShowUpdatedMarker)
476 return $this->mNotificationTimestamp = false;
477
478 $dbr =& wfGetDB(DB_SLAVE);
479
480 $this->mNotificationTimestamp = $dbr->selectField(
481 'watchlist',
482 'wl_notificationtimestamp',
483 array( 'wl_namespace' => $this->mTitle->getNamespace(),
484 'wl_title' => $this->mTitle->getDBkey(),
485 'wl_user' => $wgUser->getID()
486 ),
487 $fname);
488
489 return $this->mNotificationTimestamp;
490 }
491
492 /** @todo document */
493 function makeNavbar($revisions, $offset, $limit, $direction) {
494 global $wgLang;
495
496 $revisions = array_slice($revisions, 0, $limit);
497
498 $latestTimestamp = wfTimestamp(TS_MW, $this->getLatestOffset());
499 $earliestTimestamp = wfTimestamp(TS_MW, $this->getEarliestOffset());
500
501 /*
502 * When we're displaying previous revisions, we need to reverse
503 * the array, because it's queried in reverse order.
504 */
505 if ($direction == DIR_PREV)
506 $revisions = array_reverse($revisions);
507
508 /*
509 * lowts is the timestamp of the first revision on this page.
510 * hights is the timestamp of the last revision.
511 */
512
513 $lowts = $hights = 0;
514
515 if( count( $revisions ) ) {
516 $latestShown = wfTimestamp(TS_MW, $revisions[0]->rev_timestamp);
517 $earliestShown = wfTimestamp(TS_MW, $revisions[count($revisions) - 1]->rev_timestamp);
518 }
519
520 /* Don't announce the limit everywhere if it's the default */
521 $usefulLimit = $limit == $this->defaultLimit ? '' : $limit;
522
523 $urls = array();
524 foreach (array(20, 50, 100, 250, 500) as $num) {
525 $urls[] = $this->MakeLink( $wgLang->formatNum($num),
526 array('offset' => $offset == 0 ? '' : wfTimestamp(TS_MW, $offset), 'limit' => $num, ) );
527 }
528
529 $bits = implode($urls, ' | ');
530
531 wfDebug("latestShown=$latestShown latestTimestamp=$latestTimestamp\n");
532 if( $latestShown < $latestTimestamp ) {
533 $prevtext = $this->MakeLink( wfMsgHtml("prevn", $limit),
534 array( 'dir' => 'prev', 'offset' => $latestShown, 'limit' => $usefulLimit ) );
535 $lasttext = $this->MakeLink( wfMsgHtml('histlast'),
536 array( 'limit' => $usefulLimit ) );
537 } else {
538 $prevtext = wfMsgHtml("prevn", $limit);
539 $lasttext = wfMsgHtml('histlast');
540 }
541
542 wfDebug("earliestShown=$earliestShown earliestTimestamp=$earliestTimestamp\n");
543 if( $earliestShown > $earliestTimestamp ) {
544 $nexttext = $this->MakeLink( wfMsgHtml("nextn", $limit),
545 array( 'offset' => $earliestShown, 'limit' => $usefulLimit ) );
546 $firsttext = $this->MakeLink( wfMsgHtml('histfirst'),
547 array( 'go' => 'first', 'limit' => $usefulLimit ) );
548 } else {
549 $nexttext = wfMsgHtml("nextn", $limit);
550 $firsttext = wfMsgHtml('histfirst');
551 }
552
553 $firstlast = "($lasttext | $firsttext)";
554
555 return "$firstlast " . wfMsgHtml("viewprevnext", $prevtext, $nexttext, $bits);
556 }
557
558 function MakeLink($text, $query = NULL) {
559 if ( $query === null ) return $text;
560 return $this->mSkin->makeKnownLinkObj(
561 $this->mTitle, $text,
562 wfArrayToCGI( $query, array( 'action' => 'history' )));
563 }
564
565
566 }
567
568 ?>