MediaWiki REL1_37
HistoryAction.php
Go to the documentation of this file.
1<?php
27
39 private const DIR_PREV = 0;
40 private const DIR_NEXT = 1;
41
43 public $message;
44
45 public function getName() {
46 return 'history';
47 }
48
49 public function requiresWrite() {
50 return false;
51 }
52
53 public function requiresUnblock() {
54 return false;
55 }
56
57 protected function getPageTitle() {
58 return $this->msg( 'history-title', $this->getTitle()->getPrefixedText() )->text();
59 }
60
61 protected function getDescription() {
62 // Creation of a subtitle link pointing to [[Special:Log]]
63 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
64 $subtitle = $linkRenderer->makeKnownLink(
66 $this->msg( 'viewpagelogs' )->text(),
67 [],
68 [ 'page' => $this->getTitle()->getPrefixedText() ]
69 );
70
71 $links = [];
72 // Allow extensions to add more links
73 $this->getHookRunner()->onHistoryPageToolLinks( $this->getContext(), $linkRenderer, $links );
74 if ( $links ) {
75 $subtitle .= ''
76 . $this->msg( 'word-separator' )->escaped()
77 . $this->msg( 'parentheses' )
78 ->rawParams( $this->getLanguage()->pipeList( $links ) )
79 ->escaped();
80 }
81 return Html::rawElement( 'div', [ 'class' => 'mw-history-subtitle' ], $subtitle );
82 }
83
88 private function preCacheMessages() {
89 // Precache various messages
90 if ( !isset( $this->message ) ) {
91 $this->message = [];
92 $msgs = [ 'cur', 'last', 'pipe-separator' ];
93 foreach ( $msgs as $msg ) {
94 $this->message[$msg] = $this->msg( $msg )->escaped();
95 }
96 }
97 }
98
103 private function getTimestampFromRequest( WebRequest $request ) {
104 // Backwards compatibility checks for URIs with only year and/or month.
105 $year = $request->getInt( 'year' );
106 $month = $request->getInt( 'month' );
107 $day = null;
108 if ( $year !== 0 || $month !== 0 ) {
109 if ( $year === 0 ) {
110 $year = MWTimestamp::getLocalInstance()->format( 'Y' );
111 }
112 if ( $month < 1 || $month > 12 ) {
113 // month is invalid so treat as December (all months)
114 $month = 12;
115 }
116 // month is valid so check day
117 $day = cal_days_in_month( CAL_GREGORIAN, $month, $year );
118
119 // Left pad the months and days
120 $month = str_pad( $month, 2, "0", STR_PAD_LEFT );
121 $day = str_pad( $day, 2, "0", STR_PAD_LEFT );
122 }
123
124 $before = $request->getVal( 'date-range-to' );
125 if ( $before ) {
126 $parts = explode( '-', $before );
127 $year = $parts[0];
128 // check date input is valid
129 if ( count( $parts ) === 3 ) {
130 $month = $parts[1];
131 $day = $parts[2];
132 }
133 }
134 return $year && $month && $day ? $year . '-' . $month . '-' . $day : '';
135 }
136
141 public function onView() {
142 $out = $this->getOutput();
143 $request = $this->getRequest();
144 $config = $this->context->getConfig();
145 $services = MediaWikiServices::getInstance();
146
147 // Allow client-side HTTP caching of the history page.
148 // But, always ignore this cache if the (logged-in) user has this page on their watchlist
149 // and has one or more unseen revisions. Otherwise, we might be showing stale update markers.
150 // The Last-Modified for the history page does not change when user's markers are cleared,
151 // so going from "some unseen" to "all seen" would not clear the cache.
152 // But, when all of the revisions are marked as seen, then only way for new unseen revision
153 // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified.
154 $watchlistManager = $services->getWatchlistManager();
155 $hasUnseenRevisionMarkers = $config->get( 'ShowUpdatedMarker' ) &&
156 $watchlistManager->getTitleNotificationTimestamp(
157 $this->getUser(),
158 $this->getTitle()
159 );
160 if (
161 !$hasUnseenRevisionMarkers &&
162 $out->checkLastModified( $this->getWikiPage()->getTouched() )
163 ) {
164 return null; // Client cache fresh and headers sent, nothing more to do.
165 }
166
167 $this->preCacheMessages();
168
169 # Fill in the file cache if not set already
170 if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
171 $cache = new HTMLFileCache( $this->getTitle(), 'history' );
172 if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
173 ob_start( [ &$cache, 'saveToFileCache' ] );
174 }
175 }
176
177 // Setup page variables.
178 $out->setFeedAppendQuery( 'action=history' );
179 $out->addModules( 'mediawiki.action.history' );
180 $out->addModuleStyles( [
181 'mediawiki.interface.helpers.styles',
182 'mediawiki.action.history.styles',
183 'mediawiki.special.changeslist',
184 ] );
185 if ( $config->get( 'UseMediaWikiUIEverywhere' ) ) {
186 $out->addModuleStyles( [
187 'mediawiki.ui.input',
188 'mediawiki.ui.checkbox',
189 ] );
190 }
191
192 // Handle atom/RSS feeds.
193 $feedType = $request->getRawVal( 'feed' );
194 if ( $feedType !== null ) {
195 $this->feed( $feedType );
196 return null;
197 }
198
199 $this->addHelpLink(
200 'https://meta.wikimedia.org/wiki/Special:MyLanguage/Help:Page_history',
201 true
202 );
203
204 // Fail nicely if article doesn't exist.
205 if ( !$this->getWikiPage()->exists() ) {
206 global $wgSend404Code;
207 if ( $wgSend404Code ) {
208 $out->setStatusCode( 404 );
209 }
210 $out->addWikiMsg( 'nohistory' );
211
213
214 # show deletion/move log if there is an entry
215 LogEventsList::showLogExtract(
216 $out,
217 [ 'delete', 'move', 'protect' ],
218 $this->getTitle(),
219 '',
220 [ 'lim' => 10,
221 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
222 'showIfEmpty' => false,
223 'msgKey' => [ 'moveddeleted-notice' ]
224 ]
225 );
226
227 return null;
228 }
229
230 $ts = $this->getTimestampFromRequest( $request );
231 $tagFilter = $request->getVal( 'tagfilter' );
232
236 if ( $request->getBool( 'deleted' ) ) {
237 $conds = [ 'rev_deleted != 0' ];
238 } else {
239 $conds = [];
240 }
241
242 // Add the general form.
243 $fields = [
244 [
245 'name' => 'title',
246 'type' => 'hidden',
247 'default' => $this->getTitle()->getPrefixedDBkey(),
248 ],
249 [
250 'name' => 'action',
251 'type' => 'hidden',
252 'default' => 'history',
253 ],
254 [
255 'type' => 'date',
256 'default' => $ts,
257 'label' => $this->msg( 'date-range-to' )->text(),
258 'name' => 'date-range-to',
259 ],
260 [
261 'label-message' => 'tag-filter',
262 'type' => 'tagfilter',
263 'id' => 'tagfilter',
264 'name' => 'tagfilter',
265 'value' => $tagFilter,
266 ]
267 ];
268 if ( $this->getContext()->getAuthority()->isAllowed( 'deletedhistory' ) ) {
269 $fields[] = [
270 'type' => 'check',
271 'label' => $this->msg( 'history-show-deleted' )->text(),
272 'default' => $request->getBool( 'deleted' ),
273 'name' => 'deleted',
274 ];
275 }
276
277 $out->enableOOUI();
278 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
279 $htmlForm
280 ->setMethod( 'get' )
281 ->setAction( wfScript() )
282 ->setCollapsibleOptions( true )
283 ->setId( 'mw-history-searchform' )
284 ->setSubmitTextMsg( 'historyaction-submit' )
285 ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
286 ->setWrapperLegendMsg( 'history-fieldset-title' );
287 $htmlForm->loadData();
288
289 $out->addHTML( $htmlForm->getHTML( false ) );
290
291 $this->getHookRunner()->onPageHistoryBeforeList(
292 $this->getArticle(),
293 $this->getContext()
294 );
295
296 // Create and output the list.
297 $dateComponents = explode( '-', $ts );
298 if ( count( $dateComponents ) > 1 ) {
299 $y = $dateComponents[0];
300 $m = $dateComponents[1];
301 $d = $dateComponents[2];
302 } else {
303 $y = '';
304 $m = '';
305 $d = '';
306 }
307 $pager = new HistoryPager(
308 $this,
309 $y,
310 $m,
311 $tagFilter,
312 $conds,
313 $d,
314 $services->getLinkBatchFactory(),
316 );
317 $out->addHTML(
318 $pager->getNavigationBar() .
319 $pager->getBody() .
320 $pager->getNavigationBar()
321 );
322 $out->preventClickjacking( $pager->getPreventClickjacking() );
323
324 return null;
325 }
326
337 private function fetchRevisions( $limit, $offset, $direction ) {
338 // Fail if article doesn't exist.
339 if ( !$this->getTitle()->exists() ) {
340 return new FakeResultWrapper( [] );
341 }
342
344
345 if ( $direction === self::DIR_PREV ) {
346 list( $dirs, $oper ) = [ "ASC", ">=" ];
347 } else { /* $direction === self::DIR_NEXT */
348 list( $dirs, $oper ) = [ "DESC", "<=" ];
349 }
350
351 if ( $offset ) {
352 $offsets = [ "rev_timestamp $oper " . $dbr->addQuotes( $dbr->timestamp( $offset ) ) ];
353 } else {
354 $offsets = [];
355 }
356
357 $page_id = $this->getWikiPage()->getId();
358
359 $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo();
360 // T270033 Index renaming
361 $revIndex = $dbr->indexExists( 'revision', 'page_timestamp', __METHOD__ )
362 ? 'page_timestamp'
363 : 'rev_page_timestamp';
364 return $dbr->select(
365 $revQuery['tables'],
366 $revQuery['fields'],
367 array_merge( [ 'rev_page' => $page_id ], $offsets ),
368 __METHOD__,
369 [
370 'ORDER BY' => "rev_timestamp $dirs",
371 'USE INDEX' => [ 'revision' => $revIndex ],
372 'LIMIT' => $limit
373 ],
374 $revQuery['joins']
375 );
376 }
377
383 private function feed( $type ) {
384 if ( !FeedUtils::checkFeedOutput( $type, $this->getOutput() ) ) {
385 return;
386 }
387 $request = $this->getRequest();
388
389 $feedClasses = $this->context->getConfig()->get( 'FeedClasses' );
391 $feed = new $feedClasses[$type](
392 $this->getTitle()->getPrefixedText() . ' - ' .
393 $this->msg( 'history-feed-title' )->inContentLanguage()->text(),
394 $this->msg( 'history-feed-description' )->inContentLanguage()->text(),
395 $this->getTitle()->getFullURL( 'action=history' )
396 );
397
398 // Get a limit on number of feed entries. Provide a sane default
399 // of 10 if none is defined (but limit to $wgFeedLimit max)
400 $limit = $request->getInt( 'limit', 10 );
401 $limit = min(
402 max( $limit, 1 ),
403 $this->context->getConfig()->get( 'FeedLimit' )
404 );
405
406 $items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT );
407
408 // Generate feed elements enclosed between header and footer.
409 $feed->outHeader();
410 if ( $items->numRows() ) {
411 foreach ( $items as $row ) {
412 $feed->outItem( $this->feedItem( $row ) );
413 }
414 } else {
415 $feed->outItem( $this->feedEmpty() );
416 }
417 $feed->outFooter();
418 }
419
420 private function feedEmpty() {
421 return new FeedItem(
422 $this->msg( 'nohistory' )->inContentLanguage()->text(),
423 $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
424 $this->getTitle()->getFullURL(),
425 wfTimestamp( TS_MW ),
426 '',
427 $this->getTitle()->getTalkPage()->getFullURL()
428 );
429 }
430
439 private function feedItem( $row ) {
440 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
441 $rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() );
442 $prevRev = $revisionStore->getPreviousRevision( $rev );
443 $revComment = $rev->getComment() === null ? null : $rev->getComment()->text;
445 $this->getTitle(),
446 $prevRev ? $prevRev->getId() : false,
447 $rev->getId(),
448 $rev->getTimestamp(),
449 $revComment
450 );
451 $revUserText = $rev->getUser() ? $rev->getUser()->getName() : '';
452 if ( $revComment == '' ) {
453 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
454 $title = $this->msg( 'history-feed-item-nocomment',
455 $revUserText,
456 $contLang->timeanddate( $rev->getTimestamp() ),
457 $contLang->date( $rev->getTimestamp() ),
458 $contLang->time( $rev->getTimestamp() )
459 )->inContentLanguage()->text();
460 } else {
461 $title = $revUserText .
462 $this->msg( 'colon-separator' )->inContentLanguage()->text() .
463 FeedItem::stripComment( $revComment );
464 }
465
466 return new FeedItem(
467 $title,
468 $text,
469 $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
470 $rev->getTimestamp(),
471 $revUserText,
472 $this->getTitle()->getTalkPage()->getFullURL()
473 );
474 }
475}
getAuthority()
WatchlistManager $watchlistManager
$wgSend404Code
Some web hosts attempt to rewrite all responses with a 404 (not found) status code,...
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:195
getHookRunner()
Definition Action.php:247
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Action.php:441
getTitle()
Shortcut to get the Title object from the page.
Definition Action.php:216
getContext()
Get the IContextSource in use here.
Definition Action.php:132
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:156
getUser()
Shortcut to get the User being used for this instance.
Definition Action.php:166
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition Action.php:121
getArticle()
Get a Article object.
Definition Action.php:206
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:73
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition Action.php:185
getRequest()
Get the WebRequest being used for this instance.
Definition Action.php:146
A base class for outputting syndication feeds (e.g.
Definition FeedItem.php:33
static formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='')
Really format a diff for the newsfeed.
static checkFeedOutput( $type, $output=null)
Check whether feeds can be used and that $type is a valid feed type.
Definition FeedUtils.php:44
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.
feed( $type)
Output a subscription feed listing recent edits to this page.
preCacheMessages()
As we use the same small set of messages in various methods and that they are called often,...
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.
fetchRevisions( $limit, $offset, $direction)
Fetch an array of revisions, specified by a given limit, offset and direction.
feedItem( $row)
Generate a FeedItem object from a given revision table row Borrows Recent Changes' feed generation fu...
getTimestampFromRequest(WebRequest $request)
getDescription()
Returns the description that goes below the <h1> tag.
requiresUnblock()
Whether this action can still be executed by a blocked user.
MediaWikiServices is the service locator for the application scope of MediaWiki.
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.
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
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.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25