MediaWiki  master
HistoryAction.php
Go to the documentation of this file.
1 <?php
27 
39  const DIR_PREV = 0;
40  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(
65  SpecialPage::getTitleFor( 'Log' ),
66  $this->msg( 'viewpagelogs' )->text(),
67  [],
68  [ 'page' => $this->getTitle()->getPrefixedText() ]
69  );
70 
71  $links = [];
72  // Allow extensions to add more links
73  Hooks::run( 'HistoryPageToolLinks', [ $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 
87  public function getArticle() {
88  return $this->page;
89  }
90 
95  private function preCacheMessages() {
96  // Precache various messages
97  if ( !isset( $this->message ) ) {
98  $this->message = [];
99  $msgs = [ 'cur', 'last', 'pipe-separator' ];
100  foreach ( $msgs as $msg ) {
101  $this->message[$msg] = $this->msg( $msg )->escaped();
102  }
103  }
104  }
105 
110  private function getTimestampFromRequest( WebRequest $request ) {
111  // Backwards compatibility checks for URIs with only year and/or month.
112  $year = $request->getInt( 'year' );
113  $month = $request->getInt( 'month' );
114  $day = null;
115  if ( $year !== 0 || $month !== 0 ) {
116  if ( $year === 0 ) {
117  $year = MWTimestamp::getLocalInstance()->format( 'Y' );
118  }
119  if ( $month < 1 || $month > 12 ) {
120  // month is invalid so treat as December (all months)
121  $month = 12;
122  }
123  // month is valid so check day
124  $day = cal_days_in_month( CAL_GREGORIAN, $month, $year );
125 
126  // Left pad the months and days
127  $month = str_pad( $month, 2, "0", STR_PAD_LEFT );
128  $day = str_pad( $day, 2, "0", STR_PAD_LEFT );
129  }
130 
131  $before = $request->getVal( 'date-range-to' );
132  if ( $before ) {
133  $parts = explode( '-', $before );
134  $year = $parts[0];
135  // check date input is valid
136  if ( count( $parts ) === 3 ) {
137  $month = $parts[1];
138  $day = $parts[2];
139  }
140  }
141  return $year && $month && $day ? $year . '-' . $month . '-' . $day : '';
142  }
143 
148  function onView() {
149  $out = $this->getOutput();
150  $request = $this->getRequest();
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  if (
160  !$this->hasUnseenRevisionMarkers() &&
161  $out->checkLastModified( $this->page->getTouched() )
162  ) {
163  return null; // Client cache fresh and headers sent, nothing more to do.
164  }
165 
166  $this->preCacheMessages();
167  $config = $this->context->getConfig();
168 
169  # Fill in the file cache if not set already
170  if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
171  $cache = new HTMLFileCache( $this->getTitle(), 'history' );
172  if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
173  ob_start( [ &$cache, 'saveToFileCache' ] );
174  }
175  }
176 
177  // Setup page variables.
178  $out->setFeedAppendQuery( 'action=history' );
179  $out->addModules( 'mediawiki.action.history' );
180  $out->addModuleStyles( [
181  'mediawiki.interface.helpers.styles',
182  'mediawiki.action.history.styles',
183  'mediawiki.special.changeslist',
184  ] );
185  if ( $config->get( 'UseMediaWikiUIEverywhere' ) ) {
186  $out = $this->getOutput();
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->page->exists() ) {
207  global $wgSend404Code;
208  if ( $wgSend404Code ) {
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' => 'title',
247  'type' => 'hidden',
248  'default' => $this->getTitle()->getPrefixedDBkey(),
249  ],
250  [
251  'name' => 'action',
252  'type' => 'hidden',
253  'default' => 'history',
254  ],
255  [
256  'type' => 'date',
257  'default' => $ts,
258  'label' => $this->msg( 'date-range-to' )->text(),
259  'name' => 'date-range-to',
260  ],
261  [
262  'label-raw' => $this->msg( 'tag-filter' )->parse(),
263  'type' => 'tagfilter',
264  'id' => 'tagfilter',
265  'name' => 'tagfilter',
266  'value' => $tagFilter,
267  ]
268  ];
269  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
270  if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
271  $fields[] = [
272  'type' => 'check',
273  'label' => $this->msg( 'history-show-deleted' )->text(),
274  'default' => $request->getBool( 'deleted' ),
275  'name' => 'deleted',
276  ];
277  }
278 
279  $out->enableOOUI();
280  $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
281  $htmlForm
282  ->setMethod( 'get' )
283  ->setAction( wfScript() )
284  ->setCollapsibleOptions( true )
285  ->setId( 'mw-history-searchform' )
286  ->setSubmitText( $this->msg( 'historyaction-submit' )->text() )
287  ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
288  ->setWrapperLegend( $this->msg( 'history-fieldset-title' )->text() );
289  $htmlForm->loadData();
290 
291  $out->addHTML( $htmlForm->getHTML( false ) );
292 
293  Hooks::run( 'PageHistoryBeforeList', [ &$this->page, $this->getContext() ] );
294 
295  // Create and output the list.
296  $dateComponents = explode( '-', $ts );
297  if ( count( $dateComponents ) > 1 ) {
298  $y = $dateComponents[0];
299  $m = $dateComponents[1];
300  $d = $dateComponents[2];
301  } else {
302  $y = '';
303  $m = '';
304  $d = '';
305  }
306  $pager = new HistoryPager( $this, $y, $m, $tagFilter, $conds, $d );
307  $out->addHTML(
308  $pager->getNavigationBar() .
309  $pager->getBody() .
310  $pager->getNavigationBar()
311  );
312  $out->preventClickjacking( $pager->getPreventClickjacking() );
313 
314  return null;
315  }
316 
320  private function hasUnseenRevisionMarkers() {
321  return (
322  $this->getContext()->getConfig()->get( 'ShowUpdatedMarker' ) &&
323  $this->getTitle()->getNotificationTimestamp( $this->getUser() )
324  );
325  }
326 
337  function fetchRevisions( $limit, $offset, $direction ) {
338  // Fail if article doesn't exist.
339  if ( !$this->getTitle()->exists() ) {
340  return new FakeResultWrapper( [] );
341  }
342 
343  $dbr = wfGetDB( DB_REPLICA );
344 
345  if ( $direction === self::DIR_PREV ) {
346  list( $dirs, $oper ) = [ "ASC", ">=" ];
347  } else { /* $direction === self::DIR_NEXT */
348  list( $dirs, $oper ) = [ "DESC", "<=" ];
349  }
350 
351  if ( $offset ) {
352  $offsets = [ "rev_timestamp $oper " . $dbr->addQuotes( $dbr->timestamp( $offset ) ) ];
353  } else {
354  $offsets = [];
355  }
356 
357  $page_id = $this->page->getId();
358 
360  return $dbr->select(
361  $revQuery['tables'],
362  $revQuery['fields'],
363  array_merge( [ 'rev_page' => $page_id ], $offsets ),
364  __METHOD__,
365  [
366  'ORDER BY' => "rev_timestamp $dirs",
367  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
368  'LIMIT' => $limit
369  ],
370  $revQuery['joins']
371  );
372  }
373 
379  function feed( $type ) {
380  if ( !FeedUtils::checkFeedOutput( $type ) ) {
381  return;
382  }
383  $request = $this->getRequest();
384 
385  $feedClasses = $this->context->getConfig()->get( 'FeedClasses' );
387  $feed = new $feedClasses[$type](
388  $this->getTitle()->getPrefixedText() . ' - ' .
389  $this->msg( 'history-feed-title' )->inContentLanguage()->text(),
390  $this->msg( 'history-feed-description' )->inContentLanguage()->text(),
391  $this->getTitle()->getFullURL( 'action=history' )
392  );
393 
394  // Get a limit on number of feed entries. Provide a sane default
395  // of 10 if none is defined (but limit to $wgFeedLimit max)
396  $limit = $request->getInt( 'limit', 10 );
397  $limit = min(
398  max( $limit, 1 ),
399  $this->context->getConfig()->get( 'FeedLimit' )
400  );
401 
402  $items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT );
403 
404  // Generate feed elements enclosed between header and footer.
405  $feed->outHeader();
406  if ( $items->numRows() ) {
407  foreach ( $items as $row ) {
408  $feed->outItem( $this->feedItem( $row ) );
409  }
410  } else {
411  $feed->outItem( $this->feedEmpty() );
412  }
413  $feed->outFooter();
414  }
415 
416  function feedEmpty() {
417  return new FeedItem(
418  $this->msg( 'nohistory' )->inContentLanguage()->text(),
419  $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
420  $this->getTitle()->getFullURL(),
421  wfTimestamp( TS_MW ),
422  '',
423  $this->getTitle()->getTalkPage()->getFullURL()
424  );
425  }
426 
435  function feedItem( $row ) {
436  $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
437  $rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() );
438  $prevRev = $revisionStore->getPreviousRevision( $rev );
439  $revComment = $rev->getComment() === null ? null : $rev->getComment()->text;
440  $text = FeedUtils::formatDiffRow(
441  $this->getTitle(),
442  $prevRev ? $prevRev->getId() : false,
443  $rev->getId(),
444  $rev->getTimestamp(),
445  $revComment
446  );
447  $revUserText = $rev->getUser() ? $rev->getUser()->getName() : '';
448  if ( $revComment == '' ) {
449  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
450  $title = $this->msg( 'history-feed-item-nocomment',
451  $revUserText,
452  $contLang->timeanddate( $rev->getTimestamp() ),
453  $contLang->date( $rev->getTimestamp() ),
454  $contLang->time( $rev->getTimestamp() )
455  )->inContentLanguage()->text();
456  } else {
457  $title = $revUserText .
458  $this->msg( 'colon-separator' )->inContentLanguage()->text() .
459  FeedItem::stripComment( $revComment );
460  }
461 
462  return new FeedItem(
463  $title,
464  $text,
465  $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
466  $rev->getTimestamp(),
467  $revUserText,
468  $this->getTitle()->getTalkPage()->getFullURL()
469  );
470  }
471 }
static stripComment( $text)
Quickie hack...
Definition: FeedItem.php:220
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Definition: WebRequest.php:575
getOutput()
Get the OutputPage being used for this instance.
Definition: Action.php:208
feed( $type)
Output a subscription feed listing recent edits to this page.
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Action.php:400
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:247
getVal( $name, $default=null)
Fetch a scalar from the input or return $default if it&#39;s not set.
Definition: WebRequest.php:489
static useFileCache(IContextSource $context, $mode=self::MODE_NORMAL)
Check if pages can be cached for this request/user.
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:218
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Page view caching in the file system.
static formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='')
Really format a diff for the newsfeed.
Definition: FeedUtils.php:91
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition: Action.php:237
static exists( $name)
Check if a given action is recognised, even if it&#39;s disabled.
Definition: Action.php:170
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: Action.php:259
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
feedItem( $row)
Generate a FeedItem object from a given revision table row Borrows Recent Changes&#39; feed generation fu...
onView()
Print the history page for an article.
static factory( $displayFormat,... $arguments)
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:307
preCacheMessages()
As we use the same small set of messages in various methods and that they are called often...
$cache
Definition: mcc.php:33
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object...
Definition: Revision.php:316
getContext()
Get the IContextSource in use here.
Definition: Action.php:179
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
A base class for outputting syndication feeds (e.g.
Definition: FeedItem.php:33
WikiPage Article ImagePage CategoryPage Page $page
Page on which we&#39;re performing the action.
Definition: Action.php:46
array $fields
The fields used to create the HTMLForm.
Definition: Action.php:60
An action which just does something, without showing a form first.
This class handles printing the history page for an article.
fetchRevisions( $limit, $offset, $direction)
Fetch an array of revisions, specified by a given limit, offset and direction.
Overloads the relevant methods of the real ResultsWrapper so it doesn&#39;t go anywhere near an actual da...
$revQuery
array $message
Array of message keys and strings.
getTimestampFromRequest(WebRequest $request)
static checkFeedOutput( $type)
Check whether feeds can be used and that $type is a valid feed type.
Definition: FeedUtils.php:39
const DB_REPLICA
Definition: defines.php:25
$wgSend404Code
Some web hosts attempt to rewrite all responses with a 404 (not found) status code, mangling or hiding MediaWiki&#39;s output.
getRequest()
Get the WebRequest being used for this instance.
Definition: Action.php:198
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.