MediaWiki  master
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(
71  SpecialPage::getTitleFor( 'Log' ),
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 
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  $send404Code = $config->get( MainConfigNames::Send404Code );
207  if ( $send404Code ) {
208  $out->setStatusCode( 404 );
209  }
210  $out->addWikiMsg( 'nohistory' );
211 
212  $dbr = wfGetDB( DB_REPLICA );
213 
214  # show deletion/move log if there is an entry
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' => 'action',
246  'type' => 'hidden',
247  'default' => 'history',
248  ],
249  [
250  'type' => 'date',
251  'default' => $ts,
252  'label' => $this->msg( 'date-range-to' )->text(),
253  'name' => 'date-range-to',
254  ],
255  [
256  'label-message' => 'tag-filter',
257  'type' => 'tagfilter',
258  'id' => 'tagfilter',
259  'name' => 'tagfilter',
260  'value' => $tagFilter,
261  ]
262  ];
263  if ( $this->getAuthority()->isAllowed( '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  ->setSubmitTextMsg( 'historyaction-submit' )
280  ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
281  ->setWrapperLegendMsg( 'history-fieldset-title' )
282  ->prepareForm();
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 = (int)$dateComponents[0];
295  $m = (int)$dateComponents[1];
296  $d = (int)$dateComponents[2];
297  } else {
298  $y = 0;
299  $m = 0;
300  $d = 0;
301  }
302  $pager = new HistoryPager(
303  $this,
304  $y,
305  $m,
306  $tagFilter,
307  $conds,
308  $d,
309  $services->getLinkBatchFactory(),
310  $watchlistManager,
311  $services->getCommentFormatter(),
312  $services->getHookContainer()
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 
340  $dbr = wfGetDB( DB_REPLICA );
341 
342  if ( $direction === self::DIR_PREV ) {
343  [ $dirs, $oper ] = [ "ASC", ">=" ];
344  } else { /* $direction === self::DIR_NEXT */
345  [ $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;
442  $text = FeedUtils::formatDiffRow2(
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: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.
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:354
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 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: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...
Definition: WebRequest.php:50
getVal( $name, $default=null)
Fetch a text string and partially normalize it.
Definition: WebRequest.php:514
getBool( $name, $default=false)
Fetch a boolean value from the input or return $default if not set.
Definition: WebRequest.php:662
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Definition: WebRequest.php:618
getRawVal( $name, $default=null)
Fetch a string WITHOUT any Unicode or line break normalization.
Definition: WebRequest.php:487
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
$revQuery