MediaWiki  master
ApiFeedWatchlist.php
Go to the documentation of this file.
1 <?php
30 
38 class ApiFeedWatchlist extends ApiBase {
39 
40  private $watchlistModule = null;
41  private $linkToSections = false;
42 
43  private ParserFactory $parserFactory;
44 
50  public function __construct(
51  ApiMain $main,
52  $action,
53  ParserFactory $parserFactory
54  ) {
55  parent::__construct( $main, $action );
56  $this->parserFactory = $parserFactory;
57  }
58 
64  public function getCustomPrinter() {
65  return new ApiFormatFeedWrapper( $this->getMain() );
66  }
67 
72  public function execute() {
73  $config = $this->getConfig();
74  $feedClasses = $config->get( MainConfigNames::FeedClasses );
75  $params = [];
76  $feedItems = [];
77  try {
78  $params = $this->extractRequestParams();
79 
80  if ( !$config->get( MainConfigNames::Feed ) ) {
81  $this->dieWithError( 'feed-unavailable' );
82  }
83 
84  if ( !isset( $feedClasses[$params['feedformat']] ) ) {
85  $this->dieWithError( 'feed-invalid' );
86  }
87 
88  // limit to the number of hours going from now back
89  $endTime = wfTimestamp( TS_MW, time() - (int)$params['hours'] * 60 * 60 );
90 
91  // Prepare parameters for nested request
92  $fauxReqArr = [
93  'action' => 'query',
94  'meta' => 'siteinfo',
95  'siprop' => 'general',
96  'list' => 'watchlist',
97  'wlprop' => 'title|user|comment|timestamp|ids|loginfo',
98  'wldir' => 'older', // reverse order - from newest to oldest
99  'wlend' => $endTime, // stop at this time
100  'wllimit' => min( 50, $this->getConfig()->get( MainConfigNames::FeedLimit ) )
101  ];
102 
103  if ( $params['wlowner'] !== null ) {
104  $fauxReqArr['wlowner'] = $params['wlowner'];
105  }
106  if ( $params['wltoken'] !== null ) {
107  $fauxReqArr['wltoken'] = $params['wltoken'];
108  }
109  if ( $params['wlexcludeuser'] !== null ) {
110  $fauxReqArr['wlexcludeuser'] = $params['wlexcludeuser'];
111  }
112  if ( $params['wlshow'] !== null ) {
113  $fauxReqArr['wlshow'] = $params['wlshow'];
114  }
115  if ( $params['wltype'] !== null ) {
116  $fauxReqArr['wltype'] = $params['wltype'];
117  }
118 
119  // Support linking directly to sections when possible
120  // (possible only if section name is present in comment)
121  if ( $params['linktosections'] ) {
122  $this->linkToSections = true;
123  }
124 
125  // Check for 'allrev' parameter, and if found, show all revisions to each page on wl.
126  if ( $params['allrev'] ) {
127  $fauxReqArr['wlallrev'] = '';
128  }
129 
130  $fauxReq = new FauxRequest( $fauxReqArr );
131 
132  $module = new ApiMain( $fauxReq );
133  $module->execute();
134 
135  $data = $module->getResult()->getResultData( [ 'query', 'watchlist' ] );
136  foreach ( (array)$data as $key => $info ) {
137  if ( ApiResult::isMetadataKey( $key ) ) {
138  continue;
139  }
140  $feedItem = $this->createFeedItem( $info );
141  if ( $feedItem ) {
142  $feedItems[] = $feedItem;
143  }
144  }
145 
146  $msg = $this->msg( 'watchlist' )->inContentLanguage()->text();
147 
148  $feedTitle = $this->getConfig()->get( MainConfigNames::Sitename ) . ' - ' . $msg .
149  ' [' . $this->getConfig()->get( MainConfigNames::LanguageCode ) . ']';
150  $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL();
151 
152  $feed = new $feedClasses[$params['feedformat']] (
153  $feedTitle,
154  htmlspecialchars( $msg ),
155  $feedUrl
156  );
157 
158  ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
159  } catch ( Exception $e ) {
160  // Error results should not be cached
161  $this->getMain()->setCacheMaxAge( 0 );
162 
163  // @todo FIXME: Localise brackets
164  $feedTitle = $this->getConfig()->get( MainConfigNames::Sitename ) . ' - Error - ' .
165  $this->msg( 'watchlist' )->inContentLanguage()->text() .
166  ' [' . $this->getConfig()->get( MainConfigNames::LanguageCode ) . ']';
167  $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL();
168 
169  $feedFormat = $params['feedformat'] ?? 'rss';
170  $msg = $this->msg( 'watchlist' )->inContentLanguage()->escaped();
171  $feed = new $feedClasses[$feedFormat] ( $feedTitle, $msg, $feedUrl );
172 
173  if ( $e instanceof ApiUsageException ) {
174  foreach ( $e->getStatusValue()->getErrors() as $error ) {
175  // @phan-suppress-next-line PhanUndeclaredMethod
176  $msg = ApiMessage::create( $error )
177  ->inLanguage( $this->getLanguage() );
178  $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() );
179  $errorText = $msg->text();
180  $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
181  }
182  } else {
183  // Something is seriously wrong
184  $errorCode = 'internal_api_error';
185  $errorTitle = $this->msg( 'api-feed-error-title', $errorCode );
186  $errorText = $e->getMessage();
187  $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
188  }
189 
190  ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
191  }
192  }
193 
198  private function createFeedItem( $info ) {
199  if ( !isset( $info['title'] ) ) {
200  // Probably a revdeled log entry, skip it.
201  return null;
202  }
203 
204  $titleStr = $info['title'];
205  $title = Title::newFromText( $titleStr );
206  $curidParam = [];
207  if ( !$title || $title->isExternal() ) {
208  // Probably a formerly-valid title that's now conflicting with an
209  // interwiki prefix or the like.
210  if ( isset( $info['pageid'] ) ) {
211  $title = Title::newFromID( $info['pageid'] );
212  $curidParam = [ 'curid' => $info['pageid'] ];
213  }
214  if ( !$title || $title->isExternal() ) {
215  return null;
216  }
217  }
218  if ( isset( $info['revid'] ) ) {
219  if ( $info['revid'] === 0 && isset( $info['logid'] ) ) {
220  $logTitle = Title::makeTitle( NS_SPECIAL, 'Log' );
221  $titleUrl = $logTitle->getFullURL( [ 'logid' => $info['logid'] ] );
222  } else {
223  $titleUrl = $title->getFullURL( [ 'diff' => $info['revid'] ] );
224  }
225  } else {
226  $titleUrl = $title->getFullURL( $curidParam );
227  }
228  $comment = $info['comment'] ?? null;
229 
230  // Create an anchor to section.
231  // The anchor won't work for sections that have dupes on page
232  // as there's no way to strip that info from ApiWatchlist (apparently?).
233  // RegExp in the line below is equal to MediaWiki\CommentFormatter\CommentParser::doSectionLinks().
234  if ( $this->linkToSections && $comment !== null &&
235  preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches )
236  ) {
237  $titleUrl .= $this->parserFactory->getMainInstance()->guessSectionNameFromWikiText( $matches[ 2 ] );
238  }
239 
240  $timestamp = $info['timestamp'];
241 
242  if ( isset( $info['user'] ) ) {
243  $user = $info['user'];
244  $completeText = "$comment ($user)";
245  } else {
246  $user = '';
247  $completeText = (string)$comment;
248  }
249 
250  return new FeedItem( $titleStr, $completeText, $titleUrl, $timestamp, $user );
251  }
252 
253  private function getWatchlistModule() {
254  $this->watchlistModule ??= $this->getMain()->getModuleManager()->getModule( 'query' )
255  ->getModuleManager()->getModule( 'watchlist' );
256 
257  return $this->watchlistModule;
258  }
259 
260  public function getAllowedParams( $flags = 0 ) {
261  $feedFormatNames = array_keys( $this->getConfig()->get( MainConfigNames::FeedClasses ) );
262  $ret = [
263  'feedformat' => [
264  ParamValidator::PARAM_DEFAULT => 'rss',
265  ParamValidator::PARAM_TYPE => $feedFormatNames
266  ],
267  'hours' => [
268  ParamValidator::PARAM_DEFAULT => 24,
269  ParamValidator::PARAM_TYPE => 'integer',
270  IntegerDef::PARAM_MIN => 1,
271  IntegerDef::PARAM_MAX => 72,
272  ],
273  'linktosections' => false,
274  ];
275 
276  $copyParams = [
277  'allrev' => 'allrev',
278  'owner' => 'wlowner',
279  'token' => 'wltoken',
280  'show' => 'wlshow',
281  'type' => 'wltype',
282  'excludeuser' => 'wlexcludeuser',
283  ];
284  // @phan-suppress-next-line PhanParamTooMany
285  $wlparams = $this->getWatchlistModule()->getAllowedParams( $flags );
286  foreach ( $copyParams as $from => $to ) {
287  $p = $wlparams[$from];
288  if ( !is_array( $p ) ) {
289  $p = [ ParamValidator::PARAM_DEFAULT => $p ];
290  }
291  if ( !isset( $p[ApiBase::PARAM_HELP_MSG] ) ) {
292  $p[ApiBase::PARAM_HELP_MSG] = "apihelp-query+watchlist-param-$from";
293  }
294  if ( isset( $p[ParamValidator::PARAM_TYPE] ) && is_array( $p[ParamValidator::PARAM_TYPE] ) &&
296  ) {
297  foreach ( $p[ParamValidator::PARAM_TYPE] as $v ) {
298  if ( !isset( $p[ApiBase::PARAM_HELP_MSG_PER_VALUE][$v] ) ) {
299  $p[ApiBase::PARAM_HELP_MSG_PER_VALUE][$v] = "apihelp-query+watchlist-paramvalue-$from-$v";
300  }
301  }
302  }
303  $ret[$to] = $p;
304  }
305 
306  return $ret;
307  }
308 
309  protected function getExamplesMessages() {
310  return [
311  'action=feedwatchlist'
312  => 'apihelp-feedwatchlist-example-default',
313  'action=feedwatchlist&allrev=&hours=6'
314  => 'apihelp-feedwatchlist-example-all6hrs',
315  ];
316  }
317 
318  public function getHelpUrls() {
319  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist_feed';
320  }
321 }
const NS_SPECIAL
Definition: Defines.php:53
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
$matches
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:63
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1516
getMain()
Get the main module.
Definition: ApiBase.php:547
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition: ApiBase.php:210
getResult()
Get the result object.
Definition: ApiBase.php:668
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:808
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:170
This action allows users to get their watchlist items in RSS/Atom formats.
getAllowedParams( $flags=0)
getExamplesMessages()
Returns usage examples for this module.
getHelpUrls()
Return links to more detailed help pages about the module.
getCustomPrinter()
This module uses a custom feed wrapper printer.
__construct(ApiMain $main, $action, ParserFactory $parserFactory)
execute()
Make a nested call to the API to request watchlist items in the last $hours.
This printer is used to wrap an instance of the Feed class.
static setResult( $result, $feed, $feedItems)
Call this method to initialize output data.
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:64
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:45
static isMetadataKey( $key)
Test whether a key should be considered metadata.
Definition: ApiResult.php:780
Exception used to abort API execution with an error.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
A base class for outputting syndication feeds (e.g.
Definition: FeedItem.php:40
A class containing constants representing the names of configuration variables.
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:42
Parent class for all special pages.
Definition: SpecialPage.php:66
Represents a title within MediaWiki.
Definition: Title.php:76
Service for formatting and validating API parameters.
Type definition for integer types.
Definition: IntegerDef.php:23