MediaWiki  master
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(
66  SpecialPage::getTitleFor( 'Log' ),
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 
213  $dbr = wfGetDB( DB_REPLICA );
214 
215  # show deletion/move log if there is an entry
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->getContext()->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(),
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 
340  $dbr = wfGetDB( DB_REPLICA );
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  // T270033 Index renaming
358  $revIndex = $dbr->indexExists( 'revision', 'page_timestamp', __METHOD__ )
359  ? 'page_timestamp'
360  : 'rev_page_timestamp';
361  return $dbr->select(
362  $revQuery['tables'],
363  $revQuery['fields'],
364  array_merge( [ 'rev_page' => $page_id ], $offsets ),
365  __METHOD__,
366  [
367  'ORDER BY' => "rev_timestamp $dirs",
368  'USE INDEX' => [ 'revision' => $revIndex ],
369  'LIMIT' => $limit
370  ],
371  $revQuery['joins']
372  );
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;
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 }
getAuthority()
WatchlistManager $watchlistManager
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:198
getHookRunner()
Definition: Action.php:250
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Action.php:445
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:219
getContext()
Get the IContextSource in use here.
Definition: Action.php:135
getOutput()
Get the OutputPage being used for this instance.
Definition: Action.php:159
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:169
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition: Action.php:123
getArticle()
Get a Article object.
Definition: Action.php:209
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: Action.php:231
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:188
getRequest()
Get the WebRequest being used for this instance.
Definition: Action.php:149
A base class for outputting syndication feeds (e.g.
Definition: FeedItem.php:36
static stripComment( $text)
Quickie hack...
Definition: FeedItem.php:224
static formatDiffRow2( $title, $oldid, $newid, $timestamp, $formattedComment, $actiontext='')
Really really format a diff for the newsfeed.
Definition: FeedUtils.php:132
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.
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:338
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,...
feedItem( $row, $formattedComment)
Generate a FeedItem object from a given revision table row Borrows Recent Changes' feed generation fu...
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.
getTimestampFromRequest(WebRequest $request)
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)
A class containing constants representing the names of configuration variables.
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...
Definition: WebRequest.php:43
getVal( $name, $default=null)
Fetch a text string and partially normalized it.
Definition: WebRequest.php:514
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Definition: WebRequest.php:618
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
$revQuery