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