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  $watchlistNotificationManager = $services->getWatchlistNotificationManager();
155  $hasUnseenRevisionMarkers = $config->get( 'ShowUpdatedMarker' ) &&
156  $watchlistNotificationManager->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-raw' => $this->msg( 'tag-filter' )->parse(),
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  ->setSubmitText( $this->msg( 'historyaction-submit' )->text() )
285  ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
286  ->setWrapperLegend( $this->msg( 'history-fieldset-title' )->text() );
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(),
315  $watchlistNotificationManager
316  );
317  $out->addHTML(
318  $pager->getNavigationBar() .
319  $pager->getBody() .
320  $pager->getNavigationBar()
321  );
322  $out->preventClickjacking( $pager->getPreventClickjacking() );
323 
324  return null;
325  }
326 
337  private 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->getWikiPage()->getId();
358 
359  $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo();
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  private function feed( $type ) {
380  if ( !FeedUtils::checkFeedOutput( $type, $this->getOutput() ) ) {
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  private 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  private 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 }
$wgSend404Code
$wgSend404Code
Some web hosts attempt to rewrite all responses with a 404 (not found) status code,...
Definition: DefaultSettings.php:3753
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:93
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:173
Action\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: Action.php:229
HistoryAction\onView
onView()
Print the history page for an article.
Definition: HistoryAction.php:141
HistoryAction\feedItem
feedItem( $row)
Generate a FeedItem object from a given revision table row Borrows Recent Changes' feed generation fu...
Definition: HistoryAction.php:435
HTMLFileCache
Page view caching in the file system.
Definition: HTMLFileCache.php:33
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:1832
HistoryAction\DIR_NEXT
const DIR_NEXT
Definition: HistoryAction.php:40
FeedUtils\formatDiffRow
static formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='')
Really format a diff for the newsfeed.
Definition: FeedUtils.php:102
getAuthority
getAuthority()
Action\exists
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition: Action.php:206
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:337
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 ResultsWrapper so it doesn't go anywhere near an actual da...
Definition: FakeResultWrapper.php:11
$revQuery
$revQuery
Definition: testCompression.php:56
$dbr
$dbr
Definition: testCompression.php:54
Action\getContext
getContext()
Get the IContextSource in use here.
Definition: Action.php:215
Action\getArticle
getArticle()
Get a Article object.
Definition: Action.php:289
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2534
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
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:2467
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:416
HistoryPager
Definition: HistoryPager.php:34
$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:602
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:278
Action\getUser
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:249
Action\addHelpLink
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Action.php:509
Action\getTitle
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:299
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:42
HistoryAction\getName
getName()
Return the name of the action this object responds to.
Definition: HistoryAction.php:45
Action\getHookRunner
getHookRunner()
Definition: Action.php:329
$cache
$cache
Definition: mcc.php:33
WebRequest\getVal
getVal( $name, $default=null)
Fetch a scalar from the input or return $default if it's not set.
Definition: WebRequest.php:504
WebRequest\getInt
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Definition: WebRequest.php:590
Action\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: Action.php:311
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:379
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
Action\getLanguage
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition: Action.php:268
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:239
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:213
HTMLForm\factory
static factory( $displayFormat,... $arguments)
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:322
$type
$type
Definition: testCompression.php:52