MediaWiki  master
HistoryAction.php
Go to the documentation of this file.
1 <?php
27 
39  private const DIR_PREV = 0;
40  private 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  $this->getHookRunner()->onHistoryPageToolLinks( $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 
88  private function preCacheMessages() {
89  // Precache various messages
90  if ( !isset( $this->message ) ) {
91  $this->message = [];
92  $msgs = [ 'cur', 'last', 'pipe-separator' ];
93  foreach ( $msgs as $msg ) {
94  $this->message[$msg] = $this->msg( $msg )->escaped();
95  }
96  }
97  }
98 
103  private function getTimestampFromRequest( WebRequest $request ) {
104  // Backwards compatibility checks for URIs with only year and/or month.
105  $year = $request->getInt( 'year' );
106  $month = $request->getInt( 'month' );
107  $day = null;
108  if ( $year !== 0 || $month !== 0 ) {
109  if ( $year === 0 ) {
110  $year = MWTimestamp::getLocalInstance()->format( 'Y' );
111  }
112  if ( $month < 1 || $month > 12 ) {
113  // month is invalid so treat as December (all months)
114  $month = 12;
115  }
116  // month is valid so check day
117  $day = cal_days_in_month( CAL_GREGORIAN, $month, $year );
118 
119  // Left pad the months and days
120  $month = str_pad( $month, 2, "0", STR_PAD_LEFT );
121  $day = str_pad( $day, 2, "0", STR_PAD_LEFT );
122  }
123 
124  $before = $request->getVal( 'date-range-to' );
125  if ( $before ) {
126  $parts = explode( '-', $before );
127  $year = $parts[0];
128  // check date input is valid
129  if ( count( $parts ) === 3 ) {
130  $month = $parts[1];
131  $day = $parts[2];
132  }
133  }
134  return $year && $month && $day ? $year . '-' . $month . '-' . $day : '';
135  }
136 
141  public function onView() {
142  $out = $this->getOutput();
143  $request = $this->getRequest();
144  $config = $this->context->getConfig();
145  $services = MediaWikiServices::getInstance();
146 
147  // Allow client-side HTTP caching of the history page.
148  // But, always ignore this cache if the (logged-in) user has this page on their watchlist
149  // and has one or more unseen revisions. Otherwise, we might be showing stale update markers.
150  // The Last-Modified for the history page does not change when user's markers are cleared,
151  // so going from "some unseen" to "all seen" would not clear the cache.
152  // But, when all of the revisions are marked as seen, then only way for new unseen revision
153  // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified.
154  $watchlistManager = $services->getWatchlistManager();
155  $hasUnseenRevisionMarkers = $config->get( 'ShowUpdatedMarker' ) &&
156  $watchlistManager->getTitleNotificationTimestamp(
157  $this->getUser(),
158  $this->getTitle()
159  );
160  if (
161  !$hasUnseenRevisionMarkers &&
162  $out->checkLastModified( $this->getWikiPage()->getTouched() )
163  ) {
164  return null; // Client cache fresh and headers sent, nothing more to do.
165  }
166 
167  $this->preCacheMessages();
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->addModuleStyles( [
187  'mediawiki.ui.input',
188  'mediawiki.ui.checkbox',
189  ] );
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  global $wgSend404Code;
207  if ( $wgSend404Code ) {
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' => 'title',
246  'type' => 'hidden',
247  'default' => $this->getTitle()->getPrefixedDBkey(),
248  ],
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->getContext()->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  $htmlForm->loadData();
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 = $dateComponents[0];
300  $m = $dateComponents[1];
301  $d = $dateComponents[2];
302  } else {
303  $y = '';
304  $m = '';
305  $d = '';
306  }
307  $pager = new HistoryPager(
308  $this,
309  $y,
310  $m,
311  $tagFilter,
312  $conds,
313  $d,
314  $services->getLinkBatchFactory(),
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  list( $dirs, $oper ) = [ "ASC", ">=" ];
348  } else { /* $direction === self::DIR_NEXT */
349  list( $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  // T270033 Index renaming
362  $revIndex = $dbr->indexExists( 'revision', 'page_timestamp', __METHOD__ )
363  ? 'page_timestamp'
364  : 'rev_page_timestamp';
365  return $dbr->select(
366  $revQuery['tables'],
367  $revQuery['fields'],
368  array_merge( [ 'rev_page' => $page_id ], $offsets ),
369  __METHOD__,
370  [
371  'ORDER BY' => "rev_timestamp $dirs",
372  'USE INDEX' => [ 'revision' => $revIndex ],
373  'LIMIT' => $limit
374  ],
375  $revQuery['joins']
376  );
377  }
378 
384  private function feed( $type ) {
385  if ( !FeedUtils::checkFeedOutput( $type, $this->getOutput() ) ) {
386  return;
387  }
388  $request = $this->getRequest();
389 
390  $feedClasses = $this->context->getConfig()->get( 'FeedClasses' );
392  $feed = new $feedClasses[$type](
393  $this->getTitle()->getPrefixedText() . ' - ' .
394  $this->msg( 'history-feed-title' )->inContentLanguage()->text(),
395  $this->msg( 'history-feed-description' )->inContentLanguage()->text(),
396  $this->getTitle()->getFullURL( 'action=history' )
397  );
398 
399  // Get a limit on number of feed entries. Provide a sane default
400  // of 10 if none is defined (but limit to $wgFeedLimit max)
401  $limit = $request->getInt( 'limit', 10 );
402  $limit = min(
403  max( $limit, 1 ),
404  $this->context->getConfig()->get( 'FeedLimit' )
405  );
406 
407  $items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT );
408 
409  // Preload comments
410  $formattedComments = MediaWikiServices::getInstance()->getRowCommentFormatter()
411  ->formatRows( $items, 'rev_comment' );
412 
413  // Generate feed elements enclosed between header and footer.
414  $feed->outHeader();
415  if ( $items->numRows() ) {
416  foreach ( $items as $i => $row ) {
417  $feed->outItem( $this->feedItem( $row, $formattedComments[$i] ) );
418  }
419  } else {
420  $feed->outItem( $this->feedEmpty() );
421  }
422  $feed->outFooter();
423  }
424 
425  private function feedEmpty() {
426  return new FeedItem(
427  $this->msg( 'nohistory' )->inContentLanguage()->text(),
428  $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
429  $this->getTitle()->getFullURL(),
430  wfTimestamp( TS_MW ),
431  '',
432  $this->getTitle()->getTalkPage()->getFullURL()
433  );
434  }
435 
445  private function feedItem( $row, $formattedComment ) {
446  $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
447  $rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() );
448  $prevRev = $revisionStore->getPreviousRevision( $rev );
449  $revComment = $rev->getComment() === null ? null : $rev->getComment()->text;
451  $this->getTitle(),
452  $prevRev ? $prevRev->getId() : false,
453  $rev->getId(),
454  $rev->getTimestamp(),
455  $formattedComment
456  );
457  $revUserText = $rev->getUser() ? $rev->getUser()->getName() : '';
458  if ( $revComment == '' ) {
459  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
460  $title = $this->msg( 'history-feed-item-nocomment',
461  $revUserText,
462  $contLang->timeanddate( $rev->getTimestamp() ),
463  $contLang->date( $rev->getTimestamp() ),
464  $contLang->time( $rev->getTimestamp() )
465  )->inContentLanguage()->text();
466  } else {
467  $title = $revUserText .
468  $this->msg( 'colon-separator' )->inContentLanguage()->text() .
469  FeedItem::stripComment( $revComment );
470  }
471 
472  return new FeedItem(
473  $title,
474  $text,
475  $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
476  $rev->getTimestamp(),
477  $revUserText,
478  $this->getTitle()->getTalkPage()->getFullURL()
479  );
480  }
481 }
$wgSend404Code
$wgSend404Code
Some web hosts attempt to rewrite all responses with a 404 (not found) status code,...
Definition: DefaultSettings.php:3955
HistoryAction\$message
array $message
Array of message keys and strings.
Definition: HistoryAction.php:43
HTMLFileCache\useFileCache
static useFileCache(IContextSource $context, $mode=self::MODE_NORMAL)
Check if pages can be cached for this request/user.
Definition: HTMLFileCache.php:94
FeedItem
A base class for outputting syndication feeds (e.g.
Definition: FeedItem.php:33
FormlessAction
An action which just does something, without showing a form first.
Definition: FormlessAction.php:30
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:200
Action\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: Action.php:146
HistoryAction\onView
onView()
Print the history page for an article.
Definition: HistoryAction.php:141
HTMLFileCache
Page view caching in the file system.
Definition: HTMLFileCache.php:35
HistoryAction\requiresWrite
requiresWrite()
Whether this action requires the wiki not to be locked.
Definition: HistoryAction.php:49
HistoryAction\getPageTitle
getPageTitle()
Returns the name that goes in the <h1> page title.
Definition: HistoryAction.php:57
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1665
HistoryAction\DIR_NEXT
const DIR_NEXT
Definition: HistoryAction.php:40
getAuthority
getAuthority()
Action\exists
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition: Action.php:121
FeedItem\stripComment
static stripComment( $text)
Quickie hack...
Definition: FeedItem.php:220
HistoryAction\fetchRevisions
fetchRevisions( $limit, $offset, $direction)
Fetch an array of revisions, specified by a given limit, offset and direction.
Definition: HistoryAction.php:338
SpecialPage\getTitleFor
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,...
Definition: SpecialPage.php:107
HistoryAction\getDescription
getDescription()
Returns the description that goes below the <h1> tag.
Definition: HistoryAction.php:61
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Definition: FakeResultWrapper.php:12
$revQuery
$revQuery
Definition: testCompression.php:56
$dbr
$dbr
Definition: testCompression.php:54
Action\getContext
getContext()
Get the IContextSource in use here.
Definition: Action.php:132
Action\getArticle
getArticle()
Get a Article object.
Definition: Action.php:206
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2282
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:26
HistoryAction
This class handles printing the history page for an article.
Definition: HistoryAction.php:38
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2200
HistoryAction\preCacheMessages
preCacheMessages()
As we use the same small set of messages in various methods and that they are called often,...
Definition: HistoryAction.php:88
HistoryAction\feedEmpty
feedEmpty()
Definition: HistoryAction.php:425
HistoryPager
Definition: HistoryPager.php:35
$title
$title
Definition: testCompression.php:38
$dirs
$dirs
Definition: mergeMessageFileList.php:213
LogEventsList\showLogExtract
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Definition: LogEventsList.php:597
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
HistoryAction\getTimestampFromRequest
getTimestampFromRequest(WebRequest $request)
Definition: HistoryAction.php:103
Action\getWikiPage
getWikiPage()
Get a WikiPage object.
Definition: Action.php:195
Action\getUser
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:166
Action\addHelpLink
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Action.php:431
Action\getTitle
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:216
FeedUtils\checkFeedOutput
static checkFeedOutput( $type, $output=null)
Check whether feeds can be used and that $type is a valid feed type.
Definition: FeedUtils.php:44
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:43
$watchlistManager
WatchlistManager $watchlistManager
Definition: ApiWatchlistTrait.php:30
HistoryAction\getName
getName()
Return the name of the action this object responds to.
Definition: HistoryAction.php:45
Action\getHookRunner
getHookRunner()
Definition: Action.php:247
$cache
$cache
Definition: mcc.php:33
WebRequest\getVal
getVal( $name, $default=null)
Fetch a text string and partially normalized it.
Definition: WebRequest.php:505
WebRequest\getInt
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Definition: WebRequest.php:609
Action\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: Action.php:228
HistoryAction\feedItem
feedItem( $row, $formattedComment)
Generate a FeedItem object from a given revision table row Borrows Recent Changes' feed generation fu...
Definition: HistoryAction.php:445
HistoryAction\requiresUnblock
requiresUnblock()
Whether this action can still be executed by a blocked user.
Definition: HistoryAction.php:53
HistoryAction\feed
feed( $type)
Output a subscription feed listing recent edits to this page.
Definition: HistoryAction.php:384
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
Action\getLanguage
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition: Action.php:185
Action\$fields
array $fields
The fields used to create the HTMLForm.
Definition: Action.php:73
Action\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: Action.php:156
FeedUtils\formatDiffRow2
static formatDiffRow2( $title, $oldid, $newid, $timestamp, $formattedComment, $actiontext='')
Really really format a diff for the newsfeed.
Definition: FeedUtils.php:131
HistoryAction\DIR_PREV
const DIR_PREV
Definition: HistoryAction.php:39
MWTimestamp\getLocalInstance
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
Definition: MWTimestamp.php:173
HTMLForm\factory
static factory( $displayFormat,... $arguments)
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:326
$type
$type
Definition: testCompression.php:52