Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.76% covered (warning)
81.76%
139 / 170
55.56% covered (warning)
55.56%
5 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiEditMassMessageList
81.76% covered (warning)
81.76%
139 / 170
55.56% covered (warning)
55.56%
5 / 9
65.16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
98.00% covered (success)
98.00%
98 / 100
0.00% covered (danger)
0.00%
0 / 1
31
 getEditSummary
42.86% covered (danger)
42.86%
15 / 35
0.00% covered (danger)
0.00%
0 / 1
38.87
 getAllowedParams
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
1
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 8
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
3namespace MediaWiki\MassMessage\Api;
4
5use ApiBase;
6use ApiMain;
7use ApiWatchlistTrait;
8use MediaWiki\MassMessage\Content\MassMessageListContent;
9use MediaWiki\MassMessage\Content\MassMessageListContentHandler;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\Revision\SlotRecord;
12use MediaWiki\Title\Title;
13use Wikimedia\ParamValidator\ParamValidator;
14
15/**
16 * API module to edit a MassMessage delivery list.
17 *
18 * @ingroup API
19 */
20
21class ApiEditMassMessageList extends ApiBase {
22
23    use ApiWatchlistTrait;
24
25    /** @inheritDoc */
26    public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
27        parent::__construct( $mainModule, $moduleName, $modulePrefix );
28
29        // Needed for ApiWatchlistTrait.
30        $this->watchlistExpiryEnabled = false;
31    }
32
33    public function execute() {
34        $data = $this->extractRequestParams();
35
36        $this->requireAtLeastOneParameter( $data, 'add', 'remove', 'description' );
37
38        $spamlist = Title::newFromText( $data['spamlist'] );
39        if ( $spamlist === null
40            || !$spamlist->exists()
41            || !$spamlist->hasContentModel( 'MassMessageListContent' )
42        ) {
43            $this->dieWithError( 'apierror-massmessage-invalidspamlist', 'invalidspamlist' );
44        }
45        '@phan-var Title $spamlist';
46
47        /**
48         * @var MassMessageListContent $content
49         */
50        $content = MediaWikiServices::getInstance()
51            ->getRevisionLookup()
52            ->getRevisionByTitle( $spamlist )
53            ->getContent( SlotRecord::MAIN );
54        '@phan-var MassMessageListContent $content';
55        $description = $content->getDescription();
56        $targets = $content->getTargets();
57
58        // Create a copy.
59        $newTargets = $targets;
60
61        if ( isset( $data['add'] ) ) {
62            $invalidAdd = [];
63
64            foreach ( $data['add'] as $page ) {
65                $target = MassMessageListContentHandler::extractTarget( $page );
66                if ( isset( $target['errors'] ) ) {
67                    $item = [ '*' => $page ];
68                    foreach ( $target['errors'] as $error ) {
69                        $item[$error] = '';
70                    }
71                    $invalidAdd[] = $item;
72                } else {
73                    $newTargets[] = $target;
74                }
75            }
76
77            // Remove duplicates
78            $newTargets = MassMessageListContentHandler::normalizeTargetArray( $newTargets );
79            $invalidAdd = array_unique( $invalidAdd, SORT_REGULAR );
80        }
81
82        if ( isset( $data['remove'] ) ) {
83            $toRemove = [];
84            $invalidRemove = [];
85
86            foreach ( $data['remove'] as $page ) {
87                $target = MassMessageListContentHandler::extractTarget( $page );
88                if ( isset( $target['errors'] ) || !in_array( $target, $newTargets ) ) {
89                    $invalidRemove[] = $page;
90                } else {
91                    $toRemove[] = $target;
92                }
93            }
94
95            // In case there are duplicates within the provided list
96            $toRemove = MassMessageListContentHandler::normalizeTargetArray( $toRemove );
97            $invalidRemove = array_unique( $invalidRemove );
98
99            $newTargets = array_values( array_udiff( $newTargets, $toRemove,
100                [ MassMessageListContentHandler::class, 'compareTargets' ] ) );
101        }
102
103        if ( isset( $data['add'] ) ) {
104            $added = array_values( array_udiff( $newTargets, $targets,
105                [ MassMessageListContentHandler::class, 'compareTargets' ] ) );
106        } else {
107            $added = [];
108        }
109
110        if ( isset( $data['remove'] ) ) {
111            $removed = array_values( array_udiff( $targets, $newTargets,
112                [ MassMessageListContentHandler::class, 'compareTargets' ] ) );
113        } else {
114            $removed = [];
115        }
116
117        $description = $oldDescription = $content->getDescription();
118        $descriptionChanged = false;
119        if ( isset( $data['description'] ) ) {
120            $description = $data['description'];
121            $descriptionChanged = ( $description !== $oldDescription );
122        }
123
124        // Make an edit only if there are added or removed pages, or the description changed
125        if ( $added || $removed || $descriptionChanged ) {
126            $summary = $this->getEditSummary( $added, $removed, $descriptionChanged );
127            $editResult = MassMessageListContentHandler::edit(
128                $spamlist,
129                $description,
130                $newTargets,
131                $summary,
132                $this->getPermissionManager()->userHasRight( $this->getUser(), 'minoredit' ) &&
133                    $data['minor'],
134                $data['watchlist'],
135                // We can pass $this because APIs implement IContextSource
136                $this
137            );
138            if ( !$editResult->isGood() ) {
139                $this->dieStatus( $editResult );
140            }
141        }
142
143        $result = $this->getResult();
144        $resultArray = [ 'result' => 'Success' ];
145
146        if ( isset( $data['add'] ) ) {
147            $resultArray['added'] = $added;
148
149            // Use a LinkBatch to look up and cache existence for all local targets
150            $lb = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch();
151            foreach ( $resultArray['added'] as $target ) {
152                if ( !isset( $target['site'] ) ) {
153                    $lb->addObj( Title::newFromText( $target['title'] ) );
154                }
155            }
156            $lb->execute();
157
158            // Add an empty "missing" attribute to new local targets that do not exist
159            foreach ( $resultArray['added'] as &$target ) {
160                if ( !isset( $target['site'] )
161                    && !Title::newFromText( $target['title'] )->exists()
162                ) {
163                    $target['missing'] = '';
164                }
165            }
166
167            $result->setIndexedTagName( $resultArray['added'], 'page' );
168
169            if ( !empty( $invalidAdd ) ) {
170                $resultArray['result'] = 'Done';
171                $resultArray['invalidadd'] = $invalidAdd;
172                $result->setIndexedTagName( $resultArray['invalidadd'], 'item' );
173            }
174        }
175
176        if ( isset( $data['remove'] ) ) {
177            $resultArray['removed'] = $removed;
178            $result->setIndexedTagName( $resultArray['removed'], 'page' );
179
180            if ( !empty( $invalidRemove ) ) {
181                $resultArray['result'] = 'Done';
182                $resultArray['invalidremove'] = $invalidRemove;
183                $result->setIndexedTagName( $resultArray['invalidremove'], 'item' );
184            }
185        }
186
187        if ( isset( $data['description'] ) ) {
188            if ( $descriptionChanged ) {
189                $resultArray['description'] = $description;
190            } else {
191                $resultArray['result'] = 'Done';
192                $resultArray['invaliddescription'] = $description;
193            }
194        }
195
196        $result->addValue(
197            null,
198            $this->getModuleName(),
199            $resultArray
200        );
201    }
202
203    /**
204     * Get the edit summary.
205     * @todo add the actual new description to the summary, rather than noting that it changed
206     *
207     * @param array $added
208     * @param array $removed
209     * @param bool $descriptionChanged
210     * @return string
211     */
212    protected function getEditSummary( $added, $removed, $descriptionChanged ) {
213        $msgChange = ( $descriptionChanged ? 'change' : '' );
214        if ( $added && $removed ) {
215            // * massmessage-summary-addremove
216            // * massmessage-summary-addremovechange
217            $summaryMsg = $this->msg( 'massmessage-summary-addremove' . $msgChange )
218                ->numParams( count( $added ) )
219                ->numParams( count( $removed ) );
220        } elseif ( $added && !$removed ) {
221            if ( count( $added ) === 1 ) {
222                if ( isset( $added[0]['site'] ) ) {
223                    // * massmessage-summary-addonsite
224                    // * massmessage-summary-addonsitechange
225                    $summaryMsg = $this->msg(
226                        'massmessage-summary-addonsite' . $msgChange,
227                        $added[0]['title'],
228                        $added[0]['site']
229                    );
230                } else {
231                    // * massmessage-summary-add
232                    // * massmessage-summary-addchange
233                    $summaryMsg = $this->msg(
234                        'massmessage-summary-add' . $msgChange,
235                        $added[0]['title']
236                    );
237                }
238            } else {
239                // * massmessage-summary-addmulti
240                // * massmessage-summary-addmultichange
241                $summaryMsg = $this->msg( 'massmessage-summary-addmulti' . $msgChange )
242                    ->numParams( count( $added ) );
243            }
244        } elseif ( !$added && $removed ) {
245            if ( count( $removed ) === 1 ) {
246                if ( isset( $removed[0]['site'] ) ) {
247                    // * massmessage-summary-removeonsite
248                    // * massmessage-summary-removeonsitechange
249                    $summaryMsg = $this->msg(
250                        'massmessage-summary-removeonsite' . $msgChange,
251                        $removed[0]['title'],
252                        $removed[0]['site']
253                    );
254                } else {
255                    // * massmessage-summary-remove
256                    // * massmessage-summary-removechange
257                    $summaryMsg = $this->msg(
258                        'massmessage-summary-remove' . $msgChange,
259                        $removed[0]['title']
260                    );
261                }
262            } else {
263                // * massmessage-summary-removemulti
264                // * massmessage-summary-removemultichange
265                $summaryMsg = $this->msg( 'massmessage-summary-removemulti' . $msgChange )
266                    ->numParams( count( $removed ) );
267            }
268        } else {
269            $summaryMsg = $this->msg( 'massmessage-summary-change' );
270        }
271        return $summaryMsg->inContentLanguage()->text();
272    }
273
274    /** @inheritDoc */
275    public function getAllowedParams() {
276        return [
277            'spamlist' => [
278                ParamValidator::PARAM_TYPE => 'string',
279                ParamValidator::PARAM_REQUIRED => true
280            ],
281            'description' => [
282                ParamValidator::PARAM_TYPE => 'string'
283            ],
284            'add' => [
285                ParamValidator::PARAM_TYPE => 'string',
286                ParamValidator::PARAM_ISMULTI => true
287            ],
288            'remove' => [
289                ParamValidator::PARAM_TYPE => 'string',
290                ParamValidator::PARAM_ISMULTI => true
291            ],
292            'minor' => [
293                ParamValidator::PARAM_TYPE => 'boolean',
294                ParamValidator::PARAM_DEFAULT => false
295            ],
296        ] + $this->getWatchlistParams() + [ 'token' => null ];
297    }
298
299    /** @inheritDoc */
300    public function mustBePosted() {
301        return true;
302    }
303
304    /** @inheritDoc */
305    public function needsToken() {
306        return 'csrf';
307    }
308
309    /** @inheritDoc */
310    public function isWriteMode() {
311        return true;
312    }
313
314    /** @inheritDoc */
315    protected function getExamplesMessages() {
316        return [
317            'action=editmassmessagelist&spamlist=Example&add=User%20talk%3AFoo%7CTalk%3ABar' .
318            '&remove=Talk%3ABaz&token=TOKEN'
319                => 'apihelp-editmassmessagelist-example-1',
320            'action=editmassmessagelist&spamlist=Example' .
321            '&description=FooBor%20delivery%20services&token=TOKEN'
322                => 'apihelp-editmassmessagelist-example-2'
323        ];
324    }
325
326    /** @inheritDoc */
327    public function getHelpUrls() {
328        return [ 'https://www.mediawiki.org/wiki/Extension:MassMessage/API' ];
329    }
330
331}