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