Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 143
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 / 142
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 / 62
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'] && $config->get( MainConfigNames::MiserMode ) ) {
84            $this->dieWithError( 'apierror-sizediffdisabled' );
85        }
86
87        $msg = $this->msg( 'Contributions' )->inContentLanguage()->escaped();
88        $feedTitle = $config->get( MainConfigNames::Sitename )
89            . " - $msg "
90            . $this->msg( 'brackets', $config->get( MainConfigNames::LanguageCode ) )->inContentLanguage()->text();
91
92        $target = $params['user'];
93        if ( ExternalUserNames::isExternal( $target ) ) {
94            // Interwiki names make invalid titles, so put the target in the query instead.
95            $feedUrl = SpecialPage::getTitleFor( 'Contributions' )->getFullURL( [ 'target' => $target ] );
96        } else {
97            $feedUrl = SpecialPage::getTitleFor( 'Contributions', $target )->getFullURL();
98        }
99
100        $feed = new $feedClasses[$params['feedformat']](
101            $feedTitle,
102            $msg,
103            $feedUrl
104        );
105
106        // Convert year/month parameters to end parameter
107        $params['start'] = '';
108        $params['end'] = '';
109        $params = ContribsPager::processDateFilter( $params );
110
111        $targetUser = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
112
113        $pager = new ContribsPager(
114            $this->getContext(), [
115                'target' => $target,
116                'namespace' => $params['namespace'],
117                'start' => $params['start'],
118                'end' => $params['end'],
119                'tagFilter' => $params['tagfilter'],
120                'deletedOnly' => $params['deletedonly'],
121                'topOnly' => $params['toponly'],
122                'newOnly' => $params['newonly'],
123                'hideMinor' => $params['hideminor'],
124                'showSizeDiff' => $params['showsizediff'],
125            ],
126            $this->linkRenderer,
127            $this->linkBatchFactory,
128            $this->hookContainer,
129            $this->dbProvider,
130            $this->revisionStore,
131            $this->namespaceInfo,
132            $targetUser,
133            $this->commentFormatter
134        );
135
136        $feedLimit = $config->get( MainConfigNames::FeedLimit );
137        if ( $pager->getLimit() > $feedLimit ) {
138            $pager->setLimit( $feedLimit );
139        }
140
141        $feedItems = [];
142        if ( $pager->getNumRows() > 0 ) {
143            $count = 0;
144            $limit = $pager->getLimit();
145            foreach ( $pager->mResult as $row ) {
146                // ContribsPager selects one more row for navigation, skip that row
147                if ( ++$count > $limit ) {
148                    break;
149                }
150                $item = $this->feedItem( $row );
151                if ( $item !== null ) {
152                    $feedItems[] = $item;
153                }
154            }
155        }
156
157        ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
158    }
159
160    /**
161     * TODO: use stdClass type hint without T398925
162     * @param stdClass $row
163     */
164    protected function feedItem( $row ): ?FeedItem {
165        // This hook is the api contributions equivalent to the
166        // ContributionsLineEnding hook. Hook implementers may cancel
167        // the hook to signal the user is not allowed to read this item.
168        $feedItem = null;
169        $hookResult = $this->hookRunner->onApiFeedContributions__feedItem(
170            $row, $this->getContext(), $feedItem );
171        // Hook returned a valid feed item
172        if ( $feedItem instanceof FeedItem ) {
173            return $feedItem;
174        // Hook was canceled and did not return a valid feed item
175        } elseif ( !$hookResult ) {
176            return null;
177        }
178
179        // Hook completed and did not return a valid feed item
180        $title = Title::makeTitle( (int)$row->page_namespace, $row->page_title );
181
182        if ( $this->getAuthority()->authorizeRead( 'read', $title ) ) {
183            $date = $row->rev_timestamp;
184            $comments = $title->getTalkPage()->getFullURL();
185            $revision = $this->revisionStore->newRevisionFromRow( $row, 0, $title );
186
187            return new FeedItem(
188                $title->getPrefixedText(),
189                $this->feedItemDesc( $revision ),
190                $title->getFullURL( [ 'diff' => $revision->getId() ] ),
191                $date,
192                $this->feedItemAuthor( $revision ),
193                $comments
194            );
195        }
196
197        return null;
198    }
199
200    /**
201     * @since 1.32, takes a RevisionRecord instead of a Revision
202     * @param RevisionRecord $revision
203     * @return string
204     */
205    protected function feedItemAuthor( RevisionRecord $revision ) {
206        $user = $revision->getUser();
207        return $user ? $user->getName() : '';
208    }
209
210    /**
211     * @since 1.32, takes a RevisionRecord instead of a Revision
212     * @param RevisionRecord $revision
213     * @return string
214     */
215    protected function feedItemDesc( RevisionRecord $revision ) {
216        $msg = $this->msg( 'colon-separator' )->inContentLanguage()->escaped();
217        try {
218            $content = $revision->getContent( SlotRecord::MAIN );
219        } catch ( RevisionAccessException ) {
220            $content = null;
221        }
222
223        if ( $content instanceof TextContent ) {
224            // only textual content has a "source view".
225            $html = nl2br( htmlspecialchars( $content->getText(), ENT_COMPAT ) );
226        } else {
227            // XXX: we could get an HTML representation of the content via getParserOutput, but that may
228            //     contain JS magic and generally may not be suitable for inclusion in a feed.
229            //     Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method.
230            // Compare also MediaWiki\Feed\FeedUtils::formatDiffRow.
231            $html = '';
232        }
233
234        $comment = $revision->getComment();
235
236        return '<p>' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg .
237            htmlspecialchars( FeedItem::stripComment( $comment->text ?? '' ) ) .
238            "</p>\n<hr />\n<div>" . $html . '</div>';
239    }
240
241    /** @inheritDoc */
242    public function getAllowedParams() {
243        $feedFormatNames = array_keys( $this->getConfig()->get( MainConfigNames::FeedClasses ) );
244
245        $ret = [
246            'feedformat' => [
247                ParamValidator::PARAM_DEFAULT => 'rss',
248                ParamValidator::PARAM_TYPE => $feedFormatNames
249            ],
250            'user' => [
251                ParamValidator::PARAM_TYPE => 'user',
252                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'cidr', 'id', 'interwiki' ],
253                ParamValidator::PARAM_REQUIRED => true,
254            ],
255            'namespace' => [
256                ParamValidator::PARAM_TYPE => 'namespace'
257            ],
258            'year' => [
259                ParamValidator::PARAM_TYPE => 'integer'
260            ],
261            'month' => [
262                ParamValidator::PARAM_TYPE => 'integer'
263            ],
264            'tagfilter' => [
265                ParamValidator::PARAM_ISMULTI => true,
266                ParamValidator::PARAM_TYPE => array_values( MediaWikiServices::getInstance()
267                    ->getChangeTagsStore()->listDefinedTags()
268                ),
269                ParamValidator::PARAM_DEFAULT => '',
270            ],
271            'deletedonly' => false,
272            'toponly' => false,
273            'newonly' => false,
274            'hideminor' => false,
275            'showsizediff' => [
276                ParamValidator::PARAM_DEFAULT => false,
277            ],
278        ];
279
280        if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
281            $ret['showsizediff'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
282        }
283
284        return $ret;
285    }
286
287    /** @inheritDoc */
288    protected function getExamplesMessages() {
289        return [
290            'action=feedcontributions&user=Example'
291                => 'apihelp-feedcontributions-example-simple',
292        ];
293    }
294
295    /** @inheritDoc */
296    public function getHelpUrls() {
297        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Feedcontributions';
298    }
299}
300
301/** @deprecated class alias since 1.43 */
302class_alias( ApiFeedContributions::class, 'ApiFeedContributions' );