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, $this->getConfig()->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 = $this->getConfig()->get( MainConfigNames::Sitename ) . ' - ' . $msg .
136 ' [' . $this->getConfig()->get( MainConfigNames::LanguageCode ) . ']';
137 $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL();
138
139 $feed = new $feedClasses[$params['feedformat']] (
140 $feedTitle,
141 htmlspecialchars( $msg ),
142 $feedUrl
143 );
144
145 ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
146 } catch ( Exception $e ) {
147 // Error results should not be cached
148 $this->getMain()->setCacheMaxAge( 0 );
149
150 // @todo FIXME: Localise brackets
151 $feedTitle = $this->getConfig()->get( MainConfigNames::Sitename ) . ' - Error - ' .
152 $this->msg( 'watchlist' )->inContentLanguage()->text() .
153 ' [' . $this->getConfig()->get( MainConfigNames::LanguageCode ) . ']';
154 $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL();
155
156 $feedFormat = $params['feedformat'] ?? 'rss';
157 $msg = $this->msg( 'watchlist' )->inContentLanguage()->escaped();
158 $feed = new $feedClasses[$feedFormat]( $feedTitle, $msg, $feedUrl );
159
160 if ( $e instanceof ApiUsageException ) {
161 foreach ( $e->getStatusValue()->getMessages() as $msg ) {
162 // @phan-suppress-next-line PhanUndeclaredMethod
163 $msg = ApiMessage::create( $msg )
164 ->inLanguage( $this->getLanguage() );
165 $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() )->text();
166 $errorText = $msg->text();
167 $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
168 }
169 } else {
170 // Something is seriously wrong
171 $errorCode = 'internal_api_error';
172 $errorTitle = $this->msg( 'api-feed-error-title', $errorCode )->text();
173 $errorText = $e->getMessage();
174 $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
175 }
176
177 ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
178 }
179 }
180
185 private function createFeedItem( $info ) {
186 if ( !isset( $info['title'] ) ) {
187 // Probably a revdeled log entry, skip it.
188 return null;
189 }
190
191 $titleStr = $info['title'];
192 $title = Title::newFromText( $titleStr );
193 $curidParam = [];
194 if ( !$title || $title->isExternal() ) {
195 // Probably a formerly-valid title that's now conflicting with an
196 // interwiki prefix or the like.
197 if ( isset( $info['pageid'] ) ) {
198 $title = Title::newFromID( $info['pageid'] );
199 $curidParam = [ 'curid' => $info['pageid'] ];
200 }
201 if ( !$title || $title->isExternal() ) {
202 return null;
203 }
204 }
205 if ( isset( $info['revid'] ) ) {
206 if ( $info['revid'] === 0 && isset( $info['logid'] ) ) {
207 $logTitle = Title::makeTitle( NS_SPECIAL, 'Log' );
208 $titleUrl = $logTitle->getFullURL( [ 'logid' => $info['logid'] ] );
209 } else {
210 $titleUrl = $title->getFullURL( [ 'diff' => $info['revid'] ] );
211 }
212 } else {
213 $titleUrl = $title->getFullURL( $curidParam );
214 }
215 $comment = $info['comment'] ?? null;
216
217 // Create an anchor to section.
218 // The anchor won't work for sections that have dupes on page
219 // as there's no way to strip that info from ApiWatchlist (apparently?).
220 // RegExp in the line below is equal to MediaWiki\CommentFormatter\CommentParser::doSectionLinks().
221 if ( $this->linkToSections && $comment !== null &&
222 preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches )
223 ) {
224 $titleUrl .= $this->parserFactory->getMainInstance()->guessSectionNameFromWikiText( $matches[ 2 ] );
225 }
226
227 $timestamp = $info['timestamp'];
228
229 if ( isset( $info['user'] ) ) {
230 $user = $info['user'];
231 $completeText = "$comment ($user)";
232 } else {
233 $user = '';
234 $completeText = (string)$comment;
235 }
236
237 return new FeedItem( $titleStr, $completeText, $titleUrl, $timestamp, $user );
238 }
239
241 private function getWatchlistModule() {
242 $this->watchlistModule ??= $this->getMain()->getModuleManager()->getModule( 'query' )
243 ->getModuleManager()->getModule( 'watchlist' );
244
245 return $this->watchlistModule;
246 }
247
249 public function getAllowedParams( $flags = 0 ) {
250 $feedFormatNames = array_keys( $this->getConfig()->get( MainConfigNames::FeedClasses ) );
251 $ret = [
252 'feedformat' => [
253 ParamValidator::PARAM_DEFAULT => 'rss',
254 ParamValidator::PARAM_TYPE => $feedFormatNames
255 ],
256 'hours' => [
257 ParamValidator::PARAM_DEFAULT => 24,
258 ParamValidator::PARAM_TYPE => 'integer',
259 IntegerDef::PARAM_MIN => 1,
260 IntegerDef::PARAM_MAX => 72,
261 ],
262 'linktosections' => false,
263 ];
264
265 $copyParams = [
266 'allrev' => 'allrev',
267 'owner' => 'wlowner',
268 'token' => 'wltoken',
269 'show' => 'wlshow',
270 'type' => 'wltype',
271 'excludeuser' => 'wlexcludeuser',
272 ];
273 // @phan-suppress-next-line PhanParamTooMany
274 $wlparams = $this->getWatchlistModule()->getAllowedParams( $flags );
275 foreach ( $copyParams as $from => $to ) {
276 $p = $wlparams[$from];
277 if ( !is_array( $p ) ) {
278 $p = [ ParamValidator::PARAM_DEFAULT => $p ];
279 }
280 if ( !isset( $p[ApiBase::PARAM_HELP_MSG] ) ) {
281 $p[ApiBase::PARAM_HELP_MSG] = "apihelp-query+watchlist-param-$from";
282 }
283 if ( isset( $p[ParamValidator::PARAM_TYPE] ) && is_array( $p[ParamValidator::PARAM_TYPE] ) &&
285 ) {
286 foreach ( $p[ParamValidator::PARAM_TYPE] as $v ) {
287 if ( !isset( $p[ApiBase::PARAM_HELP_MSG_PER_VALUE][$v] ) ) {
288 $p[ApiBase::PARAM_HELP_MSG_PER_VALUE][$v] = "apihelp-query+watchlist-paramvalue-$from-$v";
289 }
290 }
291 }
292 $ret[$to] = $p;
293 }
294
295 return $ret;
296 }
297
299 protected function getExamplesMessages() {
300 return [
301 'action=feedwatchlist'
302 => 'apihelp-feedwatchlist-example-default',
303 'action=feedwatchlist&allrev=&hours=6'
304 => 'apihelp-feedwatchlist-example-all6hrs',
305 ];
306 }
307
309 public function getHelpUrls() {
310 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist_feed';
311 }
312}
313
315class_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.