MediaWiki 1.40.4
HistoryAction.php
Go to the documentation of this file.
1<?php
33
45 private const DIR_PREV = 0;
46 private const DIR_NEXT = 1;
47
49 public $message;
50
51 public function getName() {
52 return 'history';
53 }
54
55 public function requiresWrite() {
56 return false;
57 }
58
59 public function requiresUnblock() {
60 return false;
61 }
62
63 protected function getPageTitle() {
64 return $this->msg( 'history-title', $this->getTitle()->getPrefixedText() )->text();
65 }
66
67 protected function getDescription() {
68 // Creation of a subtitle link pointing to [[Special:Log]]
69 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
70 $subtitle = $linkRenderer->makeKnownLink(
72 $this->msg( 'viewpagelogs' )->text(),
73 [],
74 [ 'page' => $this->getTitle()->getPrefixedText() ]
75 );
76
77 $links = [];
78 // Allow extensions to add more links
79 $this->getHookRunner()->onHistoryPageToolLinks( $this->getContext(), $linkRenderer, $links );
80 if ( $links ) {
81 $subtitle .= ''
82 . $this->msg( 'word-separator' )->escaped()
83 . $this->msg( 'parentheses' )
84 ->rawParams( $this->getLanguage()->pipeList( $links ) )
85 ->escaped();
86 }
87 return Html::rawElement( 'div', [ 'class' => 'mw-history-subtitle' ], $subtitle );
88 }
89
94 private function preCacheMessages() {
95 // Precache various messages
96 if ( !isset( $this->message ) ) {
97 $this->message = [];
98 $msgs = [ 'cur', 'tooltip-cur', 'last', 'tooltip-last', 'pipe-separator' ];
99 foreach ( $msgs as $msg ) {
100 $this->message[$msg] = $this->msg( $msg )->escaped();
101 }
102 }
103 }
104
109 private function getTimestampFromRequest( WebRequest $request ) {
110 // Backwards compatibility checks for URIs with only year and/or month.
111 $year = $request->getInt( 'year' );
112 $month = $request->getInt( 'month' );
113 $day = null;
114 if ( $year !== 0 || $month !== 0 ) {
115 if ( $year === 0 ) {
116 $year = MWTimestamp::getLocalInstance()->format( 'Y' );
117 }
118 if ( $month < 1 || $month > 12 ) {
119 // month is invalid so treat as December (all months)
120 $month = 12;
121 }
122 // month is valid so check day
123 $day = cal_days_in_month( CAL_GREGORIAN, $month, $year );
124
125 // Left pad the months and days
126 $month = str_pad( (string)$month, 2, "0", STR_PAD_LEFT );
127 $day = str_pad( (string)$day, 2, "0", STR_PAD_LEFT );
128 }
129
130 $before = $request->getVal( 'date-range-to' );
131 if ( $before ) {
132 $parts = explode( '-', $before );
133 $year = $parts[0];
134 // check date input is valid
135 if ( count( $parts ) === 3 ) {
136 $month = $parts[1];
137 $day = $parts[2];
138 }
139 }
140 return $year && $month && $day ? $year . '-' . $month . '-' . $day : '';
141 }
142
147 public function onView() {
148 $out = $this->getOutput();
149 $request = $this->getRequest();
150 $config = $this->context->getConfig();
151 $services = MediaWikiServices::getInstance();
152
153 // Allow client-side HTTP caching of the history page.
154 // But, always ignore this cache if the (logged-in) user has this page on their watchlist
155 // and has one or more unseen revisions. Otherwise, we might be showing stale update markers.
156 // The Last-Modified for the history page does not change when user's markers are cleared,
157 // so going from "some unseen" to "all seen" would not clear the cache.
158 // But, when all of the revisions are marked as seen, then only way for new unseen revision
159 // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified.
160 $watchlistManager = $services->getWatchlistManager();
161 $hasUnseenRevisionMarkers = $config->get( MainConfigNames::ShowUpdatedMarker ) &&
162 $watchlistManager->getTitleNotificationTimestamp(
163 $this->getUser(),
164 $this->getTitle()
165 );
166 if (
167 !$hasUnseenRevisionMarkers &&
168 $out->checkLastModified( $this->getWikiPage()->getTouched() )
169 ) {
170 return null; // Client cache fresh and headers sent, nothing more to do.
171 }
172
173 $this->preCacheMessages();
174
175 # Fill in the file cache if not set already
176 if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
177 $cache = new HTMLFileCache( $this->getTitle(), 'history' );
178 if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
179 ob_start( [ &$cache, 'saveToFileCache' ] );
180 }
181 }
182
183 // Setup page variables.
184 $out->setFeedAppendQuery( 'action=history' );
185 $out->addModules( 'mediawiki.action.history' );
186 $out->addModuleStyles( [
187 'mediawiki.interface.helpers.styles',
188 'mediawiki.action.history.styles',
189 'mediawiki.special.changeslist',
190 ] );
191 if ( $config->get( MainConfigNames::UseMediaWikiUIEverywhere ) ) {
192 $out->addModuleStyles( [
193 'mediawiki.ui.input',
194 'mediawiki.ui.checkbox',
195 ] );
196 }
197
198 // Handle atom/RSS feeds.
199 $feedType = $request->getRawVal( 'feed' );
200 if ( $feedType !== null ) {
201 $this->feed( $feedType );
202 return null;
203 }
204
205 $this->addHelpLink(
206 'https://meta.wikimedia.org/wiki/Special:MyLanguage/Help:Page_history',
207 true
208 );
209
210 // Fail nicely if article doesn't exist.
211 if ( !$this->getWikiPage()->exists() ) {
212 $send404Code = $config->get( MainConfigNames::Send404Code );
213 if ( $send404Code ) {
214 $out->setStatusCode( 404 );
215 }
216 $out->addWikiMsg( 'nohistory' );
217
219
220 # show deletion/move log if there is an entry
221 LogEventsList::showLogExtract(
222 $out,
223 [ 'delete', 'move', 'protect' ],
224 $this->getTitle(),
225 '',
226 [ 'lim' => 10,
227 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
228 'showIfEmpty' => false,
229 'msgKey' => [ 'moveddeleted-notice' ]
230 ]
231 );
232
233 return null;
234 }
235
236 $ts = $this->getTimestampFromRequest( $request );
237 $tagFilter = $request->getVal( 'tagfilter' );
238
242 if ( $request->getBool( 'deleted' ) ) {
243 $conds = [ 'rev_deleted != 0' ];
244 } else {
245 $conds = [];
246 }
247
248 // Add the general form.
249 $fields = [
250 [
251 'name' => 'action',
252 'type' => 'hidden',
253 'default' => 'history',
254 ],
255 [
256 'type' => 'date',
257 'default' => $ts,
258 'label' => $this->msg( 'date-range-to' )->text(),
259 'name' => 'date-range-to',
260 ],
261 [
262 'label-message' => 'tag-filter',
263 'type' => 'tagfilter',
264 'id' => 'tagfilter',
265 'name' => 'tagfilter',
266 'value' => $tagFilter,
267 ]
268 ];
269 if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
270 $fields[] = [
271 'type' => 'check',
272 'label' => $this->msg( 'history-show-deleted' )->text(),
273 'default' => $request->getBool( 'deleted' ),
274 'name' => 'deleted',
275 ];
276 }
277
278 $out->enableOOUI();
279 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
280 $htmlForm
281 ->setMethod( 'get' )
282 ->setAction( wfScript() )
283 ->setCollapsibleOptions( true )
284 ->setId( 'mw-history-searchform' )
285 ->setSubmitTextMsg( 'historyaction-submit' )
286 ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
287 ->setWrapperLegendMsg( 'history-fieldset-title' )
288 ->prepareForm();
289
290 $out->addHTML( $htmlForm->getHTML( false ) );
291
292 $this->getHookRunner()->onPageHistoryBeforeList(
293 $this->getArticle(),
294 $this->getContext()
295 );
296
297 // Create and output the list.
298 $dateComponents = explode( '-', $ts );
299 if ( count( $dateComponents ) > 1 ) {
300 $y = (int)$dateComponents[0];
301 $m = (int)$dateComponents[1];
302 $d = (int)$dateComponents[2];
303 } else {
304 $y = 0;
305 $m = 0;
306 $d = 0;
307 }
308 $pager = new HistoryPager(
309 $this,
310 $y,
311 $m,
312 $tagFilter,
313 $conds,
314 $d,
315 $services->getLinkBatchFactory(),
316 $watchlistManager,
317 $services->getCommentFormatter(),
318 $services->getHookContainer()
319 );
320 $out->addHTML(
321 $pager->getNavigationBar() .
322 $pager->getBody() .
323 $pager->getNavigationBar()
324 );
325 $out->setPreventClickjacking( $pager->getPreventClickjacking() );
326
327 return null;
328 }
329
340 private function fetchRevisions( $limit, $offset, $direction ) {
341 // Fail if article doesn't exist.
342 if ( !$this->getTitle()->exists() ) {
343 return new FakeResultWrapper( [] );
344 }
345
347
348 if ( $direction === self::DIR_PREV ) {
349 [ $dirs, $oper ] = [ "ASC", ">=" ];
350 } else { /* $direction === self::DIR_NEXT */
351 [ $dirs, $oper ] = [ "DESC", "<=" ];
352 }
353
354 if ( $offset ) {
355 $offsets = [ "rev_timestamp $oper " . $dbr->addQuotes( $dbr->timestamp( $offset ) ) ];
356 } else {
357 $offsets = [];
358 }
359
360 $page_id = $this->getWikiPage()->getId();
361
362 $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo();
363
364 $res = $dbr->newSelectQueryBuilder()
365 ->queryInfo( $revQuery )
366 ->where( [ 'rev_page' => $page_id ] )
367 ->andWhere( $offsets )
368 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
369 ->orderBy( [ 'rev_timestamp' ], $dirs )
370 ->limit( $limit )
371 ->caller( __METHOD__ )
372 ->fetchResultSet();
373
374 return $res;
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}
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
getWikiPage()
Get a WikiPage object.
Definition Action.php:200
getHookRunner()
Definition Action.php:265
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Action.php:428
getTitle()
Shortcut to get the Title object from the page.
Definition Action.php:221
getContext()
Get the IContextSource in use here.
Definition Action.php:127
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:151
getUser()
Shortcut to get the User being used for this instance.
Definition Action.php:161
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition Action.php:115
getArticle()
Get a Article object.
Definition Action.php:211
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition Action.php:233
array $fields
The fields used to create the HTMLForm.
Definition Action.php:65
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition Action.php:190
getAuthority()
Shortcut to get the Authority executing this instance.
Definition Action.php:171
getRequest()
Get the WebRequest being used for this instance.
Definition Action.php:141
An action which just does something, without showing a form first.
Page view caching in the file system.
static useFileCache(IContextSource $context, $mode=self::MODE_NORMAL)
Check if pages can be cached for this request/user.
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.
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
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:45
Generate an RSS feed.
Definition RSSFeed.php:31
This class is a collection of static functions that serve two purposes:
Definition Html.php:55
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
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 normalized it.
getBool( $name, $default=false)
Fetch a boolean value from the input or return $default if not set.
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
getRawVal( $name, $default=null)
Fetch a string WITHOUT any Unicode or line break normalization.
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.
const DB_REPLICA
Definition defines.php:26