MediaWiki REL1_35
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
145 // Allow client-side HTTP caching of the history page.
146 // But, always ignore this cache if the (logged-in) user has this page on their watchlist
147 // and has one or more unseen revisions. Otherwise, we might be showing stale update markers.
148 // The Last-Modified for the history page does not change when user's markers are cleared,
149 // so going from "some unseen" to "all seen" would not clear the cache.
150 // But, when all of the revisions are marked as seen, then only way for new unseen revision
151 // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified.
152 if (
153 !$this->hasUnseenRevisionMarkers() &&
154 $out->checkLastModified( $this->getWikiPage()->getTouched() )
155 ) {
156 return null; // Client cache fresh and headers sent, nothing more to do.
157 }
158
159 $this->preCacheMessages();
160 $config = $this->context->getConfig();
161
162 # Fill in the file cache if not set already
163 if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
164 $cache = new HTMLFileCache( $this->getTitle(), 'history' );
165 if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
166 ob_start( [ &$cache, 'saveToFileCache' ] );
167 }
168 }
169
170 // Setup page variables.
171 $out->setFeedAppendQuery( 'action=history' );
172 $out->addModules( 'mediawiki.action.history' );
173 $out->addModuleStyles( [
174 'mediawiki.interface.helpers.styles',
175 'mediawiki.action.history.styles',
176 'mediawiki.special.changeslist',
177 ] );
178 if ( $config->get( 'UseMediaWikiUIEverywhere' ) ) {
179 $out = $this->getOutput();
180 $out->addModuleStyles( [
181 'mediawiki.ui.input',
182 'mediawiki.ui.checkbox',
183 ] );
184 }
185
186 // Handle atom/RSS feeds.
187 $feedType = $request->getRawVal( 'feed' );
188 if ( $feedType !== null ) {
189 $this->feed( $feedType );
190 return null;
191 }
192
193 $this->addHelpLink(
194 'https://meta.wikimedia.org/wiki/Special:MyLanguage/Help:Page_history',
195 true
196 );
197
198 // Fail nicely if article doesn't exist.
199 if ( !$this->getWikiPage()->exists() ) {
200 global $wgSend404Code;
201 if ( $wgSend404Code ) {
202 $out->setStatusCode( 404 );
203 }
204 $out->addWikiMsg( 'nohistory' );
205
207
208 # show deletion/move log if there is an entry
209 LogEventsList::showLogExtract(
210 $out,
211 [ 'delete', 'move', 'protect' ],
212 $this->getTitle(),
213 '',
214 [ 'lim' => 10,
215 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
216 'showIfEmpty' => false,
217 'msgKey' => [ 'moveddeleted-notice' ]
218 ]
219 );
220
221 return null;
222 }
223
224 $ts = $this->getTimestampFromRequest( $request );
225 $tagFilter = $request->getVal( 'tagfilter' );
226
230 if ( $request->getBool( 'deleted' ) ) {
231 $conds = [ 'rev_deleted != 0' ];
232 } else {
233 $conds = [];
234 }
235
236 // Add the general form.
237 $fields = [
238 [
239 'name' => 'title',
240 'type' => 'hidden',
241 'default' => $this->getTitle()->getPrefixedDBkey(),
242 ],
243 [
244 'name' => 'action',
245 'type' => 'hidden',
246 'default' => 'history',
247 ],
248 [
249 'type' => 'date',
250 'default' => $ts,
251 'label' => $this->msg( 'date-range-to' )->text(),
252 'name' => 'date-range-to',
253 ],
254 [
255 'label-raw' => $this->msg( 'tag-filter' )->parse(),
256 'type' => 'tagfilter',
257 'id' => 'tagfilter',
258 'name' => 'tagfilter',
259 'value' => $tagFilter,
260 ]
261 ];
262 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
263 if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
264 $fields[] = [
265 'type' => 'check',
266 'label' => $this->msg( 'history-show-deleted' )->text(),
267 'default' => $request->getBool( 'deleted' ),
268 'name' => 'deleted',
269 ];
270 }
271
272 $out->enableOOUI();
273 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
274 $htmlForm
275 ->setMethod( 'get' )
276 ->setAction( wfScript() )
277 ->setCollapsibleOptions( true )
278 ->setId( 'mw-history-searchform' )
279 ->setSubmitText( $this->msg( 'historyaction-submit' )->text() )
280 ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
281 ->setWrapperLegend( $this->msg( 'history-fieldset-title' )->text() );
282 $htmlForm->loadData();
283
284 $out->addHTML( $htmlForm->getHTML( false ) );
285
286 $this->getHookRunner()->onPageHistoryBeforeList(
287 $this->getArticle(),
288 $this->getContext()
289 );
290
291 // Create and output the list.
292 $dateComponents = explode( '-', $ts );
293 if ( count( $dateComponents ) > 1 ) {
294 $y = $dateComponents[0];
295 $m = $dateComponents[1];
296 $d = $dateComponents[2];
297 } else {
298 $y = '';
299 $m = '';
300 $d = '';
301 }
302 $pager = new HistoryPager( $this, $y, $m, $tagFilter, $conds, $d );
303 $out->addHTML(
304 $pager->getNavigationBar() .
305 $pager->getBody() .
306 $pager->getNavigationBar()
307 );
308 $out->preventClickjacking( $pager->getPreventClickjacking() );
309
310 return null;
311 }
312
316 private function hasUnseenRevisionMarkers() {
317 return (
318 $this->getContext()->getConfig()->get( 'ShowUpdatedMarker' ) &&
319 $this->getTitle()->getNotificationTimestamp( $this->getUser() )
320 );
321 }
322
333 private function fetchRevisions( $limit, $offset, $direction ) {
334 // Fail if article doesn't exist.
335 if ( !$this->getTitle()->exists() ) {
336 return new FakeResultWrapper( [] );
337 }
338
340
341 if ( $direction === self::DIR_PREV ) {
342 list( $dirs, $oper ) = [ "ASC", ">=" ];
343 } else { /* $direction === self::DIR_NEXT */
344 list( $dirs, $oper ) = [ "DESC", "<=" ];
345 }
346
347 if ( $offset ) {
348 $offsets = [ "rev_timestamp $oper " . $dbr->addQuotes( $dbr->timestamp( $offset ) ) ];
349 } else {
350 $offsets = [];
351 }
352
353 $page_id = $this->getWikiPage()->getId();
354
355 $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo();
356 return $dbr->select(
357 $revQuery['tables'],
358 $revQuery['fields'],
359 array_merge( [ 'rev_page' => $page_id ], $offsets ),
360 __METHOD__,
361 [
362 'ORDER BY' => "rev_timestamp $dirs",
363 'USE INDEX' => [ 'revision' => 'page_timestamp' ],
364 'LIMIT' => $limit
365 ],
366 $revQuery['joins']
367 );
368 }
369
375 private function feed( $type ) {
377 return;
378 }
379 $request = $this->getRequest();
380
381 $feedClasses = $this->context->getConfig()->get( 'FeedClasses' );
383 $feed = new $feedClasses[$type](
384 $this->getTitle()->getPrefixedText() . ' - ' .
385 $this->msg( 'history-feed-title' )->inContentLanguage()->text(),
386 $this->msg( 'history-feed-description' )->inContentLanguage()->text(),
387 $this->getTitle()->getFullURL( 'action=history' )
388 );
389
390 // Get a limit on number of feed entries. Provide a sane default
391 // of 10 if none is defined (but limit to $wgFeedLimit max)
392 $limit = $request->getInt( 'limit', 10 );
393 $limit = min(
394 max( $limit, 1 ),
395 $this->context->getConfig()->get( 'FeedLimit' )
396 );
397
398 $items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT );
399
400 // Generate feed elements enclosed between header and footer.
401 $feed->outHeader();
402 if ( $items->numRows() ) {
403 foreach ( $items as $row ) {
404 $feed->outItem( $this->feedItem( $row ) );
405 }
406 } else {
407 $feed->outItem( $this->feedEmpty() );
408 }
409 $feed->outFooter();
410 }
411
412 private function feedEmpty() {
413 return new FeedItem(
414 $this->msg( 'nohistory' )->inContentLanguage()->text(),
415 $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
416 $this->getTitle()->getFullURL(),
417 wfTimestamp( TS_MW ),
418 '',
419 $this->getTitle()->getTalkPage()->getFullURL()
420 );
421 }
422
431 private function feedItem( $row ) {
432 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
433 $rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() );
434 $prevRev = $revisionStore->getPreviousRevision( $rev );
435 $revComment = $rev->getComment() === null ? null : $rev->getComment()->text;
437 $this->getTitle(),
438 $prevRev ? $prevRev->getId() : false,
439 $rev->getId(),
440 $rev->getTimestamp(),
441 $revComment
442 );
443 $revUserText = $rev->getUser() ? $rev->getUser()->getName() : '';
444 if ( $revComment == '' ) {
445 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
446 $title = $this->msg( 'history-feed-item-nocomment',
447 $revUserText,
448 $contLang->timeanddate( $rev->getTimestamp() ),
449 $contLang->date( $rev->getTimestamp() ),
450 $contLang->time( $rev->getTimestamp() )
451 )->inContentLanguage()->text();
452 } else {
453 $title = $revUserText .
454 $this->msg( 'colon-separator' )->inContentLanguage()->text() .
455 FeedItem::stripComment( $revComment );
456 }
457
458 return new FeedItem(
459 $title,
460 $text,
461 $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
462 $rev->getTimestamp(),
463 $revUserText,
464 $this->getTitle()->getTalkPage()->getFullURL()
465 );
466 }
467}
$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:278
getHookRunner()
Definition Action.php:329
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Action.php:519
getTitle()
Shortcut to get the Title object from the page.
Definition Action.php:299
getContext()
Get the IContextSource in use here.
Definition Action.php:215
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:239
getUser()
Shortcut to get the User being used for this instance.
Definition Action.php:249
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition Action.php:206
getArticle()
Get a Article object.
Definition Action.php:289
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition Action.php:311
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:268
getRequest()
Get the WebRequest being used for this instance.
Definition Action.php:229
A base class for outputting syndication feeds (e.g.
Definition FeedItem.php:33
static checkFeedOutput( $type)
Check whether feeds can be used and that $type is a valid feed type.
Definition FeedUtils.php:41
static formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='')
Really format a diff for the newsfeed.
Definition FeedUtils.php:93
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 scalar from the input or return $default if it's not set.
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
Result wrapper for grabbing data queried from an IDatabase object.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25