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