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