Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 148 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
ApiFeedContributions | |
0.00% |
0 / 148 |
|
0.00% |
0 / 9 |
756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getCustomPrinter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 61 |
|
0.00% |
0 / 1 |
132 | |||
feedItem | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
feedItemAuthor | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
feedItemDesc | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getAllowedParams | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
6 | |||
getExamplesMessages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Copyright © 2011 Sam Reed |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | use MediaWiki\Api\ApiHookRunner; |
24 | use MediaWiki\Cache\LinkBatchFactory; |
25 | use MediaWiki\CommentFormatter\CommentFormatter; |
26 | use MediaWiki\Feed\FeedItem; |
27 | use MediaWiki\HookContainer\HookContainer; |
28 | use MediaWiki\Linker\LinkRenderer; |
29 | use MediaWiki\MainConfigNames; |
30 | use MediaWiki\Pager\ContribsPager; |
31 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
32 | use MediaWiki\Revision\RevisionAccessException; |
33 | use MediaWiki\Revision\RevisionRecord; |
34 | use MediaWiki\Revision\RevisionStore; |
35 | use MediaWiki\Revision\SlotRecord; |
36 | use MediaWiki\SpecialPage\SpecialPage; |
37 | use MediaWiki\Title\NamespaceInfo; |
38 | use MediaWiki\Title\Title; |
39 | use MediaWiki\Title\TitleParser; |
40 | use MediaWiki\User\ExternalUserNames; |
41 | use MediaWiki\User\UserFactory; |
42 | use MediaWiki\User\UserRigorOptions; |
43 | use Wikimedia\ParamValidator\ParamValidator; |
44 | use Wikimedia\Rdbms\IConnectionProvider; |
45 | |
46 | /** |
47 | * @ingroup API |
48 | */ |
49 | class ApiFeedContributions extends ApiBase { |
50 | |
51 | private RevisionStore $revisionStore; |
52 | private TitleParser $titleParser; |
53 | private LinkRenderer $linkRenderer; |
54 | private LinkBatchFactory $linkBatchFactory; |
55 | private HookContainer $hookContainer; |
56 | private IConnectionProvider $dbProvider; |
57 | private NamespaceInfo $namespaceInfo; |
58 | private UserFactory $userFactory; |
59 | private CommentFormatter $commentFormatter; |
60 | private ApiHookRunner $hookRunner; |
61 | |
62 | /** |
63 | * @param ApiMain $main |
64 | * @param string $action |
65 | * @param RevisionStore $revisionStore |
66 | * @param TitleParser $titleParser |
67 | * @param LinkRenderer $linkRenderer |
68 | * @param LinkBatchFactory $linkBatchFactory |
69 | * @param HookContainer $hookContainer |
70 | * @param IConnectionProvider $dbProvider |
71 | * @param NamespaceInfo $namespaceInfo |
72 | * @param UserFactory $userFactory |
73 | * @param CommentFormatter $commentFormatter |
74 | */ |
75 | public function __construct( |
76 | ApiMain $main, |
77 | $action, |
78 | RevisionStore $revisionStore, |
79 | TitleParser $titleParser, |
80 | LinkRenderer $linkRenderer, |
81 | LinkBatchFactory $linkBatchFactory, |
82 | HookContainer $hookContainer, |
83 | IConnectionProvider $dbProvider, |
84 | NamespaceInfo $namespaceInfo, |
85 | UserFactory $userFactory, |
86 | CommentFormatter $commentFormatter |
87 | ) { |
88 | parent::__construct( $main, $action ); |
89 | $this->revisionStore = $revisionStore; |
90 | $this->titleParser = $titleParser; |
91 | $this->linkRenderer = $linkRenderer; |
92 | $this->linkBatchFactory = $linkBatchFactory; |
93 | $this->hookContainer = $hookContainer; |
94 | $this->dbProvider = $dbProvider; |
95 | $this->namespaceInfo = $namespaceInfo; |
96 | $this->userFactory = $userFactory; |
97 | $this->commentFormatter = $commentFormatter; |
98 | |
99 | $this->hookRunner = new ApiHookRunner( $hookContainer ); |
100 | } |
101 | |
102 | /** |
103 | * This module uses a custom feed wrapper printer. |
104 | * |
105 | * @return ApiFormatFeedWrapper |
106 | */ |
107 | public function getCustomPrinter() { |
108 | return new ApiFormatFeedWrapper( $this->getMain() ); |
109 | } |
110 | |
111 | public function execute() { |
112 | $params = $this->extractRequestParams(); |
113 | |
114 | $config = $this->getConfig(); |
115 | if ( !$config->get( MainConfigNames::Feed ) ) { |
116 | $this->dieWithError( 'feed-unavailable' ); |
117 | } |
118 | |
119 | $feedClasses = $config->get( MainConfigNames::FeedClasses ); |
120 | if ( !isset( $feedClasses[$params['feedformat']] ) ) { |
121 | $this->dieWithError( 'feed-invalid' ); |
122 | } |
123 | |
124 | if ( $params['showsizediff'] && $this->getConfig()->get( MainConfigNames::MiserMode ) ) { |
125 | $this->dieWithError( 'apierror-sizediffdisabled' ); |
126 | } |
127 | |
128 | $msg = $this->msg( 'Contributions' )->inContentLanguage()->text(); |
129 | $feedTitle = $config->get( MainConfigNames::Sitename ) . ' - ' . $msg . |
130 | ' [' . $config->get( MainConfigNames::LanguageCode ) . ']'; |
131 | |
132 | $target = $params['user']; |
133 | if ( ExternalUserNames::isExternal( $target ) ) { |
134 | // Interwiki names make invalid titles, so put the target in the query instead. |
135 | $feedUrl = SpecialPage::getTitleFor( 'Contributions' )->getFullURL( [ 'target' => $target ] ); |
136 | } else { |
137 | $feedUrl = SpecialPage::getTitleFor( 'Contributions', $target )->getFullURL(); |
138 | } |
139 | |
140 | $feed = new $feedClasses[$params['feedformat']] ( |
141 | $feedTitle, |
142 | htmlspecialchars( $msg ), |
143 | $feedUrl |
144 | ); |
145 | |
146 | // Convert year/month parameters to end parameter |
147 | $params['start'] = ''; |
148 | $params['end'] = ''; |
149 | $params = ContribsPager::processDateFilter( $params ); |
150 | |
151 | $targetUser = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE ); |
152 | |
153 | $pager = new ContribsPager( |
154 | $this->getContext(), [ |
155 | 'target' => $target, |
156 | 'namespace' => $params['namespace'], |
157 | 'start' => $params['start'], |
158 | 'end' => $params['end'], |
159 | 'tagFilter' => $params['tagfilter'], |
160 | 'deletedOnly' => $params['deletedonly'], |
161 | 'topOnly' => $params['toponly'], |
162 | 'newOnly' => $params['newonly'], |
163 | 'hideMinor' => $params['hideminor'], |
164 | 'showSizeDiff' => $params['showsizediff'], |
165 | ], |
166 | $this->linkRenderer, |
167 | $this->linkBatchFactory, |
168 | $this->hookContainer, |
169 | $this->dbProvider, |
170 | $this->revisionStore, |
171 | $this->namespaceInfo, |
172 | $targetUser, |
173 | $this->commentFormatter |
174 | ); |
175 | |
176 | $feedLimit = $this->getConfig()->get( MainConfigNames::FeedLimit ); |
177 | if ( $pager->getLimit() > $feedLimit ) { |
178 | $pager->setLimit( $feedLimit ); |
179 | } |
180 | |
181 | $feedItems = []; |
182 | if ( $pager->getNumRows() > 0 ) { |
183 | $count = 0; |
184 | $limit = $pager->getLimit(); |
185 | foreach ( $pager->mResult as $row ) { |
186 | // ContribsPager selects one more row for navigation, skip that row |
187 | if ( ++$count > $limit ) { |
188 | break; |
189 | } |
190 | $item = $this->feedItem( $row ); |
191 | if ( $item !== null ) { |
192 | $feedItems[] = $item; |
193 | } |
194 | } |
195 | } |
196 | |
197 | ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems ); |
198 | } |
199 | |
200 | protected function feedItem( $row ) { |
201 | // This hook is the api contributions equivalent to the |
202 | // ContributionsLineEnding hook. Hook implementers may cancel |
203 | // the hook to signal the user is not allowed to read this item. |
204 | $feedItem = null; |
205 | $hookResult = $this->hookRunner->onApiFeedContributions__feedItem( |
206 | $row, $this->getContext(), $feedItem ); |
207 | // Hook returned a valid feed item |
208 | if ( $feedItem instanceof FeedItem ) { |
209 | return $feedItem; |
210 | // Hook was canceled and did not return a valid feed item |
211 | } elseif ( !$hookResult ) { |
212 | return null; |
213 | } |
214 | |
215 | // Hook completed and did not return a valid feed item |
216 | $title = Title::makeTitle( (int)$row->page_namespace, $row->page_title ); |
217 | |
218 | if ( $title && $this->getAuthority()->authorizeRead( 'read', $title ) ) { |
219 | $date = $row->rev_timestamp; |
220 | $comments = $title->getTalkPage()->getFullURL(); |
221 | $revision = $this->revisionStore->newRevisionFromRow( $row, 0, $title ); |
222 | |
223 | return new FeedItem( |
224 | $title->getPrefixedText(), |
225 | $this->feedItemDesc( $revision ), |
226 | $title->getFullURL( [ 'diff' => $revision->getId() ] ), |
227 | $date, |
228 | $this->feedItemAuthor( $revision ), |
229 | $comments |
230 | ); |
231 | } |
232 | |
233 | return null; |
234 | } |
235 | |
236 | /** |
237 | * @since 1.32, takes a RevisionRecord instead of a Revision |
238 | * @param RevisionRecord $revision |
239 | * @return string |
240 | */ |
241 | protected function feedItemAuthor( RevisionRecord $revision ) { |
242 | $user = $revision->getUser(); |
243 | return $user ? $user->getName() : ''; |
244 | } |
245 | |
246 | /** |
247 | * @since 1.32, takes a RevisionRecord instead of a Revision |
248 | * @param RevisionRecord $revision |
249 | * @return string |
250 | */ |
251 | protected function feedItemDesc( RevisionRecord $revision ) { |
252 | $msg = $this->msg( 'colon-separator' )->inContentLanguage()->text(); |
253 | try { |
254 | $content = $revision->getContent( SlotRecord::MAIN ); |
255 | } catch ( RevisionAccessException $e ) { |
256 | $content = null; |
257 | } |
258 | |
259 | if ( $content instanceof TextContent ) { |
260 | // only textual content has a "source view". |
261 | $html = nl2br( htmlspecialchars( $content->getText(), ENT_COMPAT ) ); |
262 | } else { |
263 | // XXX: we could get an HTML representation of the content via getParserOutput, but that may |
264 | // contain JS magic and generally may not be suitable for inclusion in a feed. |
265 | // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. |
266 | // Compare also MediaWiki\Feed\FeedUtils::formatDiffRow. |
267 | $html = ''; |
268 | } |
269 | |
270 | $comment = $revision->getComment(); |
271 | |
272 | return '<p>' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg . |
273 | htmlspecialchars( FeedItem::stripComment( $comment->text ?? '' ) ) . |
274 | "</p>\n<hr />\n<div>" . $html . '</div>'; |
275 | } |
276 | |
277 | public function getAllowedParams() { |
278 | $feedFormatNames = array_keys( $this->getConfig()->get( MainConfigNames::FeedClasses ) ); |
279 | |
280 | $ret = [ |
281 | 'feedformat' => [ |
282 | ParamValidator::PARAM_DEFAULT => 'rss', |
283 | ParamValidator::PARAM_TYPE => $feedFormatNames |
284 | ], |
285 | 'user' => [ |
286 | ParamValidator::PARAM_TYPE => 'user', |
287 | UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'cidr', 'id', 'interwiki' ], |
288 | ParamValidator::PARAM_REQUIRED => true, |
289 | ], |
290 | 'namespace' => [ |
291 | ParamValidator::PARAM_TYPE => 'namespace' |
292 | ], |
293 | 'year' => [ |
294 | ParamValidator::PARAM_TYPE => 'integer' |
295 | ], |
296 | 'month' => [ |
297 | ParamValidator::PARAM_TYPE => 'integer' |
298 | ], |
299 | 'tagfilter' => [ |
300 | ParamValidator::PARAM_ISMULTI => true, |
301 | ParamValidator::PARAM_TYPE => array_values( ChangeTags::listDefinedTags() ), |
302 | ParamValidator::PARAM_DEFAULT => '', |
303 | ], |
304 | 'deletedonly' => false, |
305 | 'toponly' => false, |
306 | 'newonly' => false, |
307 | 'hideminor' => false, |
308 | 'showsizediff' => [ |
309 | ParamValidator::PARAM_DEFAULT => false, |
310 | ], |
311 | ]; |
312 | |
313 | if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) { |
314 | $ret['showsizediff'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode'; |
315 | } |
316 | |
317 | return $ret; |
318 | } |
319 | |
320 | protected function getExamplesMessages() { |
321 | return [ |
322 | 'action=feedcontributions&user=Example' |
323 | => 'apihelp-feedcontributions-example-simple', |
324 | ]; |
325 | } |
326 | |
327 | public function getHelpUrls() { |
328 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Feedcontributions'; |
329 | } |
330 | } |