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