MediaWiki master
HistoryAction.php
Go to the documentation of this file.
1<?php
37
49 private const DIR_PREV = 0;
50 private const DIR_NEXT = 1;
51
53 public $message;
54
55 public function getName() {
56 return 'history';
57 }
58
59 public function requiresWrite() {
60 return false;
61 }
62
63 public function requiresUnblock() {
64 return false;
65 }
66
67 protected function getPageTitle() {
68 return $this->msg( 'history-title' )->plaintextParams( $this->getTitle()->getPrefixedText() );
69 }
70
71 protected function getDescription() {
72 // Creation of a subtitle link pointing to [[Special:Log]]
73 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
74 $subtitle = $linkRenderer->makeKnownLink(
75 SpecialPage::getTitleFor( 'Log' ),
76 $this->msg( 'viewpagelogs' )->text(),
77 [],
78 [ 'page' => $this->getTitle()->getPrefixedText() ]
79 );
80
81 $links = [];
82 // Allow extensions to add more links
83 $this->getHookRunner()->onHistoryPageToolLinks( $this->getContext(), $linkRenderer, $links );
84 if ( $links ) {
85 $subtitle .= ''
86 . $this->msg( 'word-separator' )->escaped()
87 . $this->msg( 'parentheses' )
88 ->rawParams( $this->getLanguage()->pipeList( $links ) )
89 ->escaped();
90 }
91 return Html::rawElement( 'div', [ 'class' => 'mw-history-subtitle' ], $subtitle );
92 }
93
98 private function preCacheMessages() {
99 // Precache various messages
100 if ( !isset( $this->message ) ) {
101 $this->message = [];
102 $msgs = [
103 'cur', 'tooltip-cur', 'last', 'tooltip-last', 'pipe-separator',
104 'changeslist-nocomment', 'updatedmarker',
105 ];
106 foreach ( $msgs as $msg ) {
107 $this->message[$msg] = $this->msg( $msg )->escaped();
108 }
109 }
110 }
111
116 private function getTimestampFromRequest( WebRequest $request ) {
117 // Backwards compatibility checks for URIs with only year and/or month.
118 $year = $request->getInt( 'year' );
119 $month = $request->getInt( 'month' );
120 $day = null;
121 if ( $year !== 0 || $month !== 0 ) {
122 if ( $year === 0 ) {
123 $year = MWTimestamp::getLocalInstance()->format( 'Y' );
124 }
125 if ( $month < 1 || $month > 12 ) {
126 // month is invalid so treat as December (all months)
127 $month = 12;
128 }
129 // month is valid so check day
130 $day = cal_days_in_month( CAL_GREGORIAN, $month, $year );
131
132 // Left pad the months and days
133 $month = str_pad( (string)$month, 2, "0", STR_PAD_LEFT );
134 $day = str_pad( (string)$day, 2, "0", STR_PAD_LEFT );
135 }
136
137 $before = $request->getVal( 'date-range-to' );
138 if ( $before ) {
139 $parts = explode( '-', $before );
140 $year = $parts[0];
141 // check date input is valid
142 if ( count( $parts ) === 3 ) {
143 $month = $parts[1];
144 $day = $parts[2];
145 }
146 }
147 return $year && $month && $day ? $year . '-' . $month . '-' . $day : '';
148 }
149
154 public function onView() {
155 $out = $this->getOutput();
156 $request = $this->getRequest();
157 $config = $this->context->getConfig();
158 $services = MediaWikiServices::getInstance();
159
160 // Allow client-side HTTP caching of the history page.
161 // But, always ignore this cache if the (logged-in) user has this page on their watchlist
162 // and has one or more unseen revisions. Otherwise, we might be showing stale update markers.
163 // The Last-Modified for the history page does not change when user's markers are cleared,
164 // so going from "some unseen" to "all seen" would not clear the cache.
165 // But, when all of the revisions are marked as seen, then only way for new unseen revision
166 // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified.
167 $watchlistManager = $services->getWatchlistManager();
168 $hasUnseenRevisionMarkers = $config->get( MainConfigNames::ShowUpdatedMarker ) &&
169 $watchlistManager->getTitleNotificationTimestamp(
170 $this->getUser(),
171 $this->getTitle()
172 );
173 if (
174 !$hasUnseenRevisionMarkers &&
175 $out->checkLastModified( $this->getWikiPage()->getTouched() )
176 ) {
177 return null; // Client cache fresh and headers sent, nothing more to do.
178 }
179
180 $this->preCacheMessages();
181
182 # Fill in the file cache if not set already
183 if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
184 $cache = new HTMLFileCache( $this->getTitle(), 'history' );
185 if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
186 ob_start( [ &$cache, 'saveToFileCache' ] );
187 }
188 }
189
190 // Setup page variables.
191 $out->setFeedAppendQuery( 'action=history' );
192 $out->addModules( 'mediawiki.action.history' );
193 $out->addModuleStyles( [
194 'mediawiki.interface.helpers.styles',
195 'codex-search-styles',
196 'mediawiki.action.history.styles',
197 'mediawiki.special.changeslist',
198 ] );
199
200 // Handle atom/RSS feeds.
201 $feedType = $request->getRawVal( 'feed' );
202 if ( $feedType !== null ) {
203 $this->feed( $feedType );
204 return null;
205 }
206
207 $this->addHelpLink(
208 'https://meta.wikimedia.org/wiki/Special:MyLanguage/Help:Page_history',
209 true
210 );
211
212 // Fail nicely if article doesn't exist.
213 if ( !$this->getWikiPage()->exists() ) {
214 $send404Code = $config->get( MainConfigNames::Send404Code );
215 if ( $send404Code ) {
216 $out->setStatusCode( 404 );
217 }
218 $out->addWikiMsg( 'nohistory' );
219
220 $dbr = $services->getConnectionProvider()->getReplicaDatabase();
221
222 # show deletion/move log if there is an entry
223 LogEventsList::showLogExtract(
224 $out,
225 [ 'delete', 'move', 'protect' ],
226 $this->getTitle(),
227 '',
228 [ 'lim' => 10,
229 'conds' => [ $dbr->expr( 'log_action', '!=', 'revision' ) ],
230 'showIfEmpty' => false,
231 'msgKey' => [ 'moveddeleted-notice' ]
232 ]
233 );
234
235 return null;
236 }
237
238 $ts = $this->getTimestampFromRequest( $request );
239 $tagFilter = $request->getVal( 'tagfilter' );
240
244 if ( $request->getBool( 'deleted' ) ) {
245 $conds = [ 'rev_deleted != 0' ];
246 } else {
247 $conds = [];
248 }
249
250 // Add the general form.
251 $fields = [
252 'action' => [
253 'name' => 'action',
254 'type' => 'hidden',
255 'default' => 'history',
256 ],
257 'date-range-to' => [
258 'type' => 'date',
259 'default' => $ts,
260 'label' => $this->msg( 'date-range-to' )->text(),
261 'name' => 'date-range-to',
262 ],
263 'tagfilter' => [
264 'label-message' => 'tag-filter',
265 'type' => 'tagfilter',
266 'id' => 'tagfilter',
267 'name' => 'tagfilter',
268 'value' => $tagFilter,
269 ],
270 'tagInvert' => [
271 'type' => 'check',
272 'name' => 'tagInvert',
273 'label-message' => 'invert',
274 'hide-if' => [ '===', 'tagfilter', '' ],
275 ],
276 ];
277 if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
278 $fields['deleted'] = [
279 'type' => 'check',
280 'label' => $this->msg( 'history-show-deleted' )->text(),
281 'default' => $request->getBool( 'deleted' ),
282 'name' => 'deleted',
283 ];
284 }
285
286 $out->enableOOUI();
287 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
288 $htmlForm
289 ->setMethod( 'get' )
290 ->setAction( wfScript() )
291 ->setCollapsibleOptions( true )
292 ->setId( 'mw-history-searchform' )
293 ->setSubmitTextMsg( 'historyaction-submit' )
294 ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
295 ->setWrapperLegendMsg( 'history-fieldset-title' )
296 ->prepareForm();
297
298 $out->addHTML( $htmlForm->getHTML( false ) );
299
300 $this->getHookRunner()->onPageHistoryBeforeList(
301 $this->getArticle(),
302 $this->getContext()
303 );
304
305 // Create and output the list.
306 $dateComponents = explode( '-', $ts );
307 if ( count( $dateComponents ) > 1 ) {
308 $y = (int)$dateComponents[0];
309 $m = (int)$dateComponents[1];
310 $d = (int)$dateComponents[2];
311 } else {
312 $y = 0;
313 $m = 0;
314 $d = 0;
315 }
316 $pager = new HistoryPager(
317 $this,
318 $y,
319 $m,
320 $d,
321 $tagFilter,
322 $request->getCheck( 'tagInvert' ),
323 $conds,
324 $services->getLinkBatchFactory(),
325 $watchlistManager,
326 $services->getCommentFormatter(),
327 $services->getHookContainer(),
328 $services->getChangeTagsStore()
329 );
330 $out->addHTML(
331 $pager->getNavigationBar() .
332 $pager->getBody() .
333 $pager->getNavigationBar()
334 );
335 $out->setPreventClickjacking( $pager->getPreventClickjacking() );
336
337 return null;
338 }
339
350 private function fetchRevisions( $limit, $offset, $direction ) {
351 // Fail if article doesn't exist.
352 if ( !$this->getTitle()->exists() ) {
353 return new FakeResultWrapper( [] );
354 }
355
356 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
357
358 if ( $direction === self::DIR_PREV ) {
359 [ $dirs, $oper ] = [ "ASC", ">=" ];
360 } else { /* $direction === self::DIR_NEXT */
361 [ $dirs, $oper ] = [ "DESC", "<=" ];
362 }
363
364 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr )
365 ->joinComment()
366 ->where( [ 'rev_page' => $this->getWikiPage()->getId() ] )
367 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
368 ->orderBy( [ 'rev_timestamp' ], $dirs )
369 ->limit( $limit );
370 if ( $offset ) {
371 $queryBuilder->andWhere( $dbr->expr( 'rev_timestamp', $oper, $dbr->timestamp( $offset ) ) );
372 }
373
374 return $queryBuilder->caller( __METHOD__ )->fetchResultSet();
375 }
376
382 private function feed( $type ) {
383 if ( !FeedUtils::checkFeedOutput( $type, $this->getOutput() ) ) {
384 return;
385 }
386 $request = $this->getRequest();
387
388 $feedClasses = $this->context->getConfig()->get( MainConfigNames::FeedClasses );
390 $feed = new $feedClasses[$type](
391 $this->getTitle()->getPrefixedText() . ' - ' .
392 $this->msg( 'history-feed-title' )->inContentLanguage()->text(),
393 $this->msg( 'history-feed-description' )->inContentLanguage()->text(),
394 $this->getTitle()->getFullURL( 'action=history' )
395 );
396
397 // Get a limit on number of feed entries. Provide a sensible default
398 // of 10 if none is defined (but limit to $wgFeedLimit max)
399 $limit = $request->getInt( 'limit', 10 );
400 $limit = min(
401 max( $limit, 1 ),
402 $this->context->getConfig()->get( MainConfigNames::FeedLimit )
403 );
404
405 $items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT );
406
407 // Preload comments
408 $formattedComments = MediaWikiServices::getInstance()->getRowCommentFormatter()
409 ->formatRows( $items, 'rev_comment' );
410
411 // Generate feed elements enclosed between header and footer.
412 $feed->outHeader();
413 if ( $items->numRows() ) {
414 foreach ( $items as $i => $row ) {
415 $feed->outItem( $this->feedItem( $row, $formattedComments[$i] ) );
416 }
417 } else {
418 $feed->outItem( $this->feedEmpty() );
419 }
420 $feed->outFooter();
421 }
422
423 private function feedEmpty() {
424 return new FeedItem(
425 $this->msg( 'nohistory' )->inContentLanguage()->text(),
426 $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
427 $this->getTitle()->getFullURL(),
428 wfTimestamp( TS_MW ),
429 '',
430 $this->getTitle()->getTalkPage()->getFullURL()
431 );
432 }
433
443 private function feedItem( $row, $formattedComment ) {
444 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
445 $rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() );
446 $prevRev = $revisionStore->getPreviousRevision( $rev );
447 $revComment = $rev->getComment() === null ? null : $rev->getComment()->text;
448 $text = FeedUtils::formatDiffRow2(
449 $this->getTitle(),
450 $prevRev ? $prevRev->getId() : false,
451 $rev->getId(),
452 $rev->getTimestamp(),
453 $formattedComment
454 );
455 $revUserText = $rev->getUser() ? $rev->getUser()->getName() : '';
456 if ( $revComment == '' ) {
457 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
458 $title = $this->msg( 'history-feed-item-nocomment',
459 $revUserText,
460 $contLang->timeanddate( $rev->getTimestamp() ),
461 $contLang->date( $rev->getTimestamp() ),
462 $contLang->time( $rev->getTimestamp() )
463 )->inContentLanguage()->text();
464 } else {
465 $title = $revUserText .
466 $this->msg( 'colon-separator' )->inContentLanguage()->text() .
467 FeedItem::stripComment( $revComment );
468 }
469
470 return new FeedItem(
471 $title,
472 $text,
473 $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
474 $rev->getTimestamp(),
475 $revUserText,
476 $this->getTitle()->getTalkPage()->getFullURL()
477 );
478 }
479}
getUser()
getRequest()
getAuthority()
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
getContext()
getWikiPage()
Get a WikiPage object.
Definition Action.php:190
getHookRunner()
Definition Action.php:255
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Action.php:442
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:141
getArticle()
Get a Article object.
Definition Action.php:201
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition Action.php:223
array $fields
The fields used to create the HTMLForm.
Definition Action.php:71
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition Action.php:180
An action which just does something, without showing a form first.
Page view caching in the file system.
This class handles printing the history page for an article.
onView()
Print the history page for an article.
array $message
Array of message keys and strings.
getName()
Return the name of the action this object responds to.
requiresWrite()
Whether this action requires the wiki not to be locked.
getPageTitle()
Returns the name that goes in the <h1> page title.
getDescription()
Returns the description that goes below the <h1> element.
requiresUnblock()
Whether this action can still be executed by a blocked user.
Generate an Atom feed.
Definition AtomFeed.php:35
A base class for outputting syndication feeds (e.g.
Definition FeedItem.php:40
Helper functions for feeds.
Definition FeedUtils.php:44
Generate an RSS feed.
Definition RSSFeed.php:31
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
getVal( $name, $default=null)
Fetch a text string and partially normalize it.
getBool( $name, $default=false)
Fetch a boolean value from the input or return $default if not set.
getRawVal( $name, $default=null)
Fetch a string WITHOUT any Unicode or line break normalization.
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
getCheck( $name)
Return true if the named value is set in the input, whatever that value is (even "0").
Parent class for all special pages.
Library for creating and parsing MW-style timestamps.
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Result wrapper for grabbing data queried from an IDatabase object.