MediaWiki  master
HistoryAction.php
Go to the documentation of this file.
1 <?php
32 
44  private const DIR_PREV = 0;
45  private const DIR_NEXT = 1;
46 
48  public $message;
49 
50  public function getName() {
51  return 'history';
52  }
53 
54  public function requiresWrite() {
55  return false;
56  }
57 
58  public function requiresUnblock() {
59  return false;
60  }
61 
62  protected function getPageTitle() {
63  return $this->msg( 'history-title', $this->getTitle()->getPrefixedText() )->text();
64  }
65 
66  protected function getDescription() {
67  // Creation of a subtitle link pointing to [[Special:Log]]
68  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
69  $subtitle = $linkRenderer->makeKnownLink(
70  SpecialPage::getTitleFor( 'Log' ),
71  $this->msg( 'viewpagelogs' )->text(),
72  [],
73  [ 'page' => $this->getTitle()->getPrefixedText() ]
74  );
75 
76  $links = [];
77  // Allow extensions to add more links
78  $this->getHookRunner()->onHistoryPageToolLinks( $this->getContext(), $linkRenderer, $links );
79  if ( $links ) {
80  $subtitle .= ''
81  . $this->msg( 'word-separator' )->escaped()
82  . $this->msg( 'parentheses' )
83  ->rawParams( $this->getLanguage()->pipeList( $links ) )
84  ->escaped();
85  }
86  return Html::rawElement( 'div', [ 'class' => 'mw-history-subtitle' ], $subtitle );
87  }
88 
93  private function preCacheMessages() {
94  // Precache various messages
95  if ( !isset( $this->message ) ) {
96  $this->message = [];
97  $msgs = [ 'cur', 'tooltip-cur', 'last', 'tooltip-last', 'pipe-separator' ];
98  foreach ( $msgs as $msg ) {
99  $this->message[$msg] = $this->msg( $msg )->escaped();
100  }
101  }
102  }
103 
108  private function getTimestampFromRequest( WebRequest $request ) {
109  // Backwards compatibility checks for URIs with only year and/or month.
110  $year = $request->getInt( 'year' );
111  $month = $request->getInt( 'month' );
112  $day = null;
113  if ( $year !== 0 || $month !== 0 ) {
114  if ( $year === 0 ) {
115  $year = MWTimestamp::getLocalInstance()->format( 'Y' );
116  }
117  if ( $month < 1 || $month > 12 ) {
118  // month is invalid so treat as December (all months)
119  $month = 12;
120  }
121  // month is valid so check day
122  $day = cal_days_in_month( CAL_GREGORIAN, $month, $year );
123 
124  // Left pad the months and days
125  $month = str_pad( (string)$month, 2, "0", STR_PAD_LEFT );
126  $day = str_pad( (string)$day, 2, "0", STR_PAD_LEFT );
127  }
128 
129  $before = $request->getVal( 'date-range-to' );
130  if ( $before ) {
131  $parts = explode( '-', $before );
132  $year = $parts[0];
133  // check date input is valid
134  if ( count( $parts ) === 3 ) {
135  $month = $parts[1];
136  $day = $parts[2];
137  }
138  }
139  return $year && $month && $day ? $year . '-' . $month . '-' . $day : '';
140  }
141 
146  public function onView() {
147  $out = $this->getOutput();
148  $request = $this->getRequest();
149  $config = $this->context->getConfig();
150  $services = MediaWikiServices::getInstance();
151 
152  // Allow client-side HTTP caching of the history page.
153  // But, always ignore this cache if the (logged-in) user has this page on their watchlist
154  // and has one or more unseen revisions. Otherwise, we might be showing stale update markers.
155  // The Last-Modified for the history page does not change when user's markers are cleared,
156  // so going from "some unseen" to "all seen" would not clear the cache.
157  // But, when all of the revisions are marked as seen, then only way for new unseen revision
158  // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified.
159  $watchlistManager = $services->getWatchlistManager();
160  $hasUnseenRevisionMarkers = $config->get( MainConfigNames::ShowUpdatedMarker ) &&
161  $watchlistManager->getTitleNotificationTimestamp(
162  $this->getUser(),
163  $this->getTitle()
164  );
165  if (
166  !$hasUnseenRevisionMarkers &&
167  $out->checkLastModified( $this->getWikiPage()->getTouched() )
168  ) {
169  return null; // Client cache fresh and headers sent, nothing more to do.
170  }
171 
172  $this->preCacheMessages();
173 
174  # Fill in the file cache if not set already
175  if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
176  $cache = new HTMLFileCache( $this->getTitle(), 'history' );
177  if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
178  ob_start( [ &$cache, 'saveToFileCache' ] );
179  }
180  }
181 
182  // Setup page variables.
183  $out->setFeedAppendQuery( 'action=history' );
184  $out->addModules( 'mediawiki.action.history' );
185  $out->addModuleStyles( [
186  'mediawiki.interface.helpers.styles',
187  'mediawiki.action.history.styles',
188  'mediawiki.special.changeslist',
189  ] );
190  if ( $config->get( MainConfigNames::UseMediaWikiUIEverywhere ) ) {
191  $out->addModuleStyles( [
192  'mediawiki.ui.input',
193  'mediawiki.ui.checkbox',
194  ] );
195  }
196 
197  // Handle atom/RSS feeds.
198  $feedType = $request->getRawVal( 'feed' );
199  if ( $feedType !== null ) {
200  $this->feed( $feedType );
201  return null;
202  }
203 
204  $this->addHelpLink(
205  'https://meta.wikimedia.org/wiki/Special:MyLanguage/Help:Page_history',
206  true
207  );
208 
209  // Fail nicely if article doesn't exist.
210  if ( !$this->getWikiPage()->exists() ) {
211  $send404Code = $config->get( MainConfigNames::Send404Code );
212  if ( $send404Code ) {
213  $out->setStatusCode( 404 );
214  }
215  $out->addWikiMsg( 'nohistory' );
216 
217  $dbr = wfGetDB( DB_REPLICA );
218 
219  # show deletion/move log if there is an entry
221  $out,
222  [ 'delete', 'move', 'protect' ],
223  $this->getTitle(),
224  '',
225  [ 'lim' => 10,
226  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
227  'showIfEmpty' => false,
228  'msgKey' => [ 'moveddeleted-notice' ]
229  ]
230  );
231 
232  return null;
233  }
234 
235  $ts = $this->getTimestampFromRequest( $request );
236  $tagFilter = $request->getVal( 'tagfilter' );
237 
241  if ( $request->getBool( 'deleted' ) ) {
242  $conds = [ 'rev_deleted != 0' ];
243  } else {
244  $conds = [];
245  }
246 
247  // Add the general form.
248  $fields = [
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->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  ->prepareForm();
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 = (int)$dateComponents[0];
300  $m = (int)$dateComponents[1];
301  $d = (int)$dateComponents[2];
302  } else {
303  $y = 0;
304  $m = 0;
305  $d = 0;
306  }
307  $pager = new HistoryPager(
308  $this,
309  $y,
310  $m,
311  $tagFilter,
312  $conds,
313  $d,
314  $services->getLinkBatchFactory(),
315  $watchlistManager,
316  $services->getCommentFormatter()
317  );
318  $out->addHTML(
319  $pager->getNavigationBar() .
320  $pager->getBody() .
321  $pager->getNavigationBar()
322  );
323  $out->setPreventClickjacking( $pager->getPreventClickjacking() );
324 
325  return null;
326  }
327 
338  private function fetchRevisions( $limit, $offset, $direction ) {
339  // Fail if article doesn't exist.
340  if ( !$this->getTitle()->exists() ) {
341  return new FakeResultWrapper( [] );
342  }
343 
344  $dbr = wfGetDB( DB_REPLICA );
345 
346  if ( $direction === self::DIR_PREV ) {
347  [ $dirs, $oper ] = [ "ASC", ">=" ];
348  } else { /* $direction === self::DIR_NEXT */
349  [ $dirs, $oper ] = [ "DESC", "<=" ];
350  }
351 
352  if ( $offset ) {
353  $offsets = [ "rev_timestamp $oper " . $dbr->addQuotes( $dbr->timestamp( $offset ) ) ];
354  } else {
355  $offsets = [];
356  }
357 
358  $page_id = $this->getWikiPage()->getId();
359 
360  $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo();
361 
362  $res = $dbr->newSelectQueryBuilder()
363  ->queryInfo( $revQuery )
364  ->where( [ 'rev_page' => $page_id ] )
365  ->andWhere( $offsets )
366  ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
367  ->orderBy( [ 'rev_timestamp' ], $dirs )
368  ->limit( $limit )
369  ->caller( __METHOD__ )
370  ->fetchResultSet();
371 
372  return $res;
373  }
374 
380  private function feed( $type ) {
381  if ( !FeedUtils::checkFeedOutput( $type, $this->getOutput() ) ) {
382  return;
383  }
384  $request = $this->getRequest();
385 
386  $feedClasses = $this->context->getConfig()->get( MainConfigNames::FeedClasses );
388  $feed = new $feedClasses[$type](
389  $this->getTitle()->getPrefixedText() . ' - ' .
390  $this->msg( 'history-feed-title' )->inContentLanguage()->text(),
391  $this->msg( 'history-feed-description' )->inContentLanguage()->text(),
392  $this->getTitle()->getFullURL( 'action=history' )
393  );
394 
395  // Get a limit on number of feed entries. Provide a sensible default
396  // of 10 if none is defined (but limit to $wgFeedLimit max)
397  $limit = $request->getInt( 'limit', 10 );
398  $limit = min(
399  max( $limit, 1 ),
400  $this->context->getConfig()->get( MainConfigNames::FeedLimit )
401  );
402 
403  $items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT );
404 
405  // Preload comments
406  $formattedComments = MediaWikiServices::getInstance()->getRowCommentFormatter()
407  ->formatRows( $items, 'rev_comment' );
408 
409  // Generate feed elements enclosed between header and footer.
410  $feed->outHeader();
411  if ( $items->numRows() ) {
412  foreach ( $items as $i => $row ) {
413  $feed->outItem( $this->feedItem( $row, $formattedComments[$i] ) );
414  }
415  } else {
416  $feed->outItem( $this->feedEmpty() );
417  }
418  $feed->outFooter();
419  }
420 
421  private function feedEmpty() {
422  return new FeedItem(
423  $this->msg( 'nohistory' )->inContentLanguage()->text(),
424  $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
425  $this->getTitle()->getFullURL(),
426  wfTimestamp( TS_MW ),
427  '',
428  $this->getTitle()->getTalkPage()->getFullURL()
429  );
430  }
431 
441  private function feedItem( $row, $formattedComment ) {
442  $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
443  $rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() );
444  $prevRev = $revisionStore->getPreviousRevision( $rev );
445  $revComment = $rev->getComment() === null ? null : $rev->getComment()->text;
446  $text = FeedUtils::formatDiffRow2(
447  $this->getTitle(),
448  $prevRev ? $prevRev->getId() : false,
449  $rev->getId(),
450  $rev->getTimestamp(),
451  $formattedComment
452  );
453  $revUserText = $rev->getUser() ? $rev->getUser()->getName() : '';
454  if ( $revComment == '' ) {
455  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
456  $title = $this->msg( 'history-feed-item-nocomment',
457  $revUserText,
458  $contLang->timeanddate( $rev->getTimestamp() ),
459  $contLang->date( $rev->getTimestamp() ),
460  $contLang->time( $rev->getTimestamp() )
461  )->inContentLanguage()->text();
462  } else {
463  $title = $revUserText .
464  $this->msg( 'colon-separator' )->inContentLanguage()->text() .
465  FeedItem::stripComment( $revComment );
466  }
467 
468  return new FeedItem(
469  $title,
470  $text,
471  $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
472  $rev->getTimestamp(),
473  $revUserText,
474  $this->getTitle()->getTalkPage()->getFullURL()
475  );
476  }
477 }
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:203
getHookRunner()
Definition: Action.php:268
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Action.php:431
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:224
getContext()
Get the IContextSource in use here.
Definition: Action.php:130
getOutput()
Get the OutputPage being used for this instance.
Definition: Action.php:154
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:164
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition: Action.php:118
getArticle()
Get a Article object.
Definition: Action.php:214
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: Action.php:236
array $fields
The fields used to create the HTMLForm.
Definition: Action.php:64
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition: Action.php:193
getAuthority()
Shortcut to get the Authority executing this instance.
Definition: Action.php:174
getRequest()
Get the WebRequest being used for this instance.
Definition: Action.php:144
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.
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:349
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 rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
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:47
Generate an RSS feed.
Definition: RSSFeed.php:31
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...
Definition: WebRequest.php:47
getVal( $name, $default=null)
Fetch a text string and partially normalized it.
Definition: WebRequest.php:516
getBool( $name, $default=false)
Fetch a boolean value from the input or return $default if not set.
Definition: WebRequest.php:664
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Definition: WebRequest.php:620
getRawVal( $name, $default=null)
Fetch a string WITHOUT any Unicode or line break normalization.
Definition: WebRequest.php:489
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
$revQuery