Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 175
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialEditMassMessageList
0.00% covered (danger)
0.00%
0 / 175
0.00% covered (danger)
0.00%
0 / 12
2756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setParameter
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
156
 setHeaders
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
72
 getFormFields
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
42
 alterForm
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 preHtml
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 onSubmit
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
90
 onSuccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseInput
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 isListed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\MassMessage\Specials;
4
5use HTMLForm;
6use IDBAccessObject;
7use LogEventsList;
8use MediaWiki\CommentStore\CommentStore;
9use MediaWiki\EditPage\EditPage;
10use MediaWiki\Html\Html;
11use MediaWiki\MassMessage\Content\MassMessageListContent;
12use MediaWiki\MassMessage\Content\MassMessageListContentHandler;
13use MediaWiki\MassMessage\Lookup\DatabaseLookup;
14use MediaWiki\Permissions\PermissionManager;
15use MediaWiki\Permissions\RestrictionStore;
16use MediaWiki\Revision\RevisionLookup;
17use MediaWiki\Revision\RevisionRecord;
18use MediaWiki\Revision\SlotRecord;
19use MediaWiki\SpecialPage\FormSpecialPage;
20use MediaWiki\Status\Status;
21use MediaWiki\Title\Title;
22use MediaWiki\User\Options\UserOptionsLookup;
23use MediaWiki\Watchlist\WatchlistManager;
24
25class SpecialEditMassMessageList extends FormSpecialPage {
26
27    /**
28     * The title of the list to edit
29     * If not null, the title refers to a delivery list.
30     *
31     * @var Title|null
32     */
33    protected $title;
34
35    /**
36     * The revision to edit
37     * If not null, the user can edit the delivery list.
38     *
39     * @var RevisionRecord|null
40     */
41    protected $rev;
42
43    /**
44     * The message key for the error encountered while parsing the title, if any
45     *
46     * @var string|null
47     */
48    protected $errorMsgKey;
49
50    /**
51     * Provides access to user options
52     *
53     * @var UserOptionsLookup
54     */
55    private $userOptionsLookup;
56
57    /** @var RestrictionStore */
58    private $restrictionStore;
59
60    /** @var WatchlistManager */
61    private $watchlistManager;
62
63    /** @var PermissionManager */
64    private $permissionManager;
65
66    /** @var RevisionLookup */
67    private $revisionLookup;
68
69    /**
70     * @param UserOptionsLookup $userOptionsLookup
71     * @param RestrictionStore $restrictionStore
72     * @param WatchlistManager $watchlistManager
73     * @param PermissionManager $permissionManager
74     * @param RevisionLookup $revisionLookup
75     */
76    public function __construct(
77        UserOptionsLookup $userOptionsLookup,
78        RestrictionStore $restrictionStore,
79        WatchlistManager $watchlistManager,
80        PermissionManager $permissionManager,
81        RevisionLookup $revisionLookup
82    ) {
83        parent::__construct( 'EditMassMessageList' );
84
85        $this->userOptionsLookup = $userOptionsLookup;
86        $this->restrictionStore = $restrictionStore;
87        $this->watchlistManager = $watchlistManager;
88        $this->permissionManager = $permissionManager;
89        $this->revisionLookup = $revisionLookup;
90    }
91
92    public function doesWrites() {
93        return true;
94    }
95
96    /**
97     * @param string $par
98     */
99    protected function setParameter( $par ) {
100        if ( $par === null || $par === '' ) {
101            $this->errorMsgKey = 'massmessage-edit-invalidtitle';
102        } else {
103            $title = Title::newFromText( $par );
104
105            if ( !$title
106                || !$title->exists()
107                || !$title->hasContentModel( 'MassMessageListContent' )
108            ) {
109                $this->errorMsgKey = 'massmessage-edit-invalidtitle';
110            } else {
111                $this->title = $title;
112                if ( !$this->permissionManager->userCan( 'edit',
113                    $this->getUser(), $title )
114                ) {
115                    $this->errorMsgKey = 'massmessage-edit-nopermission';
116                } else {
117                    $revId = $this->getRequest()->getInt( 'oldid' );
118                    if ( $revId > 0 ) {
119                        $rev = $this->revisionLookup->getRevisionById( $revId );
120                        if ( $rev
121                            && $title->equals( $rev->getPageAsLinkTarget() )
122                            && $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
123                                ->getModel() === 'MassMessageListContent'
124                            && RevisionRecord::userCanBitfield(
125                                $rev->getVisibility(),
126                                RevisionRecord::DELETED_TEXT,
127                                $this->getUser()
128                            )
129                        ) {
130                            $this->rev = $rev;
131                        } else {
132                            // Use the latest revision for the title if $rev is invalid.
133                            $this->rev = $this->revisionLookup->getRevisionByTitle( $title );
134                        }
135                    } else {
136                        $this->rev = $this->revisionLookup->getRevisionByTitle( $title );
137                    }
138                }
139            }
140        }
141    }
142
143    /**
144     * Override the parent implementation to modify the page title and add a backlink.
145     */
146    public function setHeaders() {
147        parent::setHeaders();
148        if ( $this->title ) {
149            $out = $this->getOutput();
150
151            // Page title
152            $out->setPageTitleMsg(
153                $this->msg( 'massmessage-edit-pagetitle', $this->title->getPrefixedText() )
154            );
155
156            // Backlink
157            if ( $this->rev ) {
158                $revId = $this->rev->getId();
159                $query = ( $revId !== $this->title->getLatestRevId() ) ?
160                    [ 'oldid' => $revId ] : [];
161            } else {
162                $query = [];
163            }
164            $out->addBacklinkSubtitle( $this->title, $query );
165
166            // Edit notices; modified from EditPage::showHeader()
167            if ( $this->rev ) {
168                $out->addHTML(
169                    implode( "\n", $this->title->getEditNotices( $this->rev->getId() ) )
170                );
171            }
172
173            // Protection warnings; modified from EditPage::showHeader()
174            if ( $this->restrictionStore->isProtected( $this->title, 'edit' )
175                && $this->permissionManager
176                    ->getNamespaceRestrictionLevels( $this->title->getNamespace() ) !== [ '' ]
177            ) {
178                if ( $this->restrictionStore->isSemiProtected( $this->title ) ) {
179                    $noticeMsg = 'semiprotectedpagewarning';
180                } else {
181                    // Full protection
182                    $noticeMsg = 'protectedpagewarning';
183                }
184                LogEventsList::showLogExtract( $out, 'protect', $this->title, '',
185                    [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
186            }
187        }
188    }
189
190    /**
191     * @return array
192     */
193    protected function getFormFields() {
194        // Return an empty form if the title is invalid or if the user can't edit the list.
195        if ( !$this->rev ) {
196            return [];
197        }
198
199        $this->getOutput()->addModules( [ 'ext.MassMessage.edit', 'ext.MassMessage.styles' ] );
200
201        /**
202         * @var MassMessageListContent $content
203         */
204        $content = $this->rev->getContent(
205            SlotRecord::MAIN,
206            RevisionRecord::FOR_THIS_USER,
207            $this->getUser()
208        );
209        '@phan-var MassMessageListContent $content';
210        $description = $content->getDescription();
211        $targets = $content->getTargetStrings();
212
213        $fields = [
214            'description' => [
215                'type' => 'textarea',
216                'rows' => 5,
217                'default' => $description ?? '',
218                'useeditfont' => true,
219                'label-message' => 'massmessage-edit-description',
220            ],
221            'content' => [
222                'type' => 'textarea',
223                'default' => ( $targets !== null ) ? implode( "\n", $targets ) : '',
224                'label-message' => 'massmessage-edit-content',
225            ],
226            'summary' => [
227                'type' => 'text',
228                'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
229                'size' => 60,
230                'label-message' => 'summary',
231            ],
232        ];
233
234        if ( $this->permissionManager->userHasRight( $this->getUser(), 'minoredit' ) ) {
235            $fields['minor'] = [
236                'name' => 'minor',
237                'id' => 'wpMinoredit',
238                'type' => 'check',
239                'label-message' => 'minoredit',
240                'default' => false,
241            ];
242        }
243
244        if ( $this->getUser()->isNamed() ) {
245            $fields['watch'] = [
246                'name' => 'watch',
247                'id' => 'wpWatchthis',
248                'type' => 'check',
249                'label-message' => 'watchthis',
250                'default' => $this->watchlistManager->isWatched( $this->getUser(), $this->title ) ||
251                    $this->userOptionsLookup->getOption( $this->getUser(), 'watchdefault' ),
252            ];
253        }
254
255        return $fields + [
256            'copyright' => [
257                'type' => 'info',
258                'default' => EditPage::getCopyrightWarning( $this->title, 'parse', $this ),
259                'raw' => true,
260            ],
261        ];
262    }
263
264    /**
265     * Hide the form if the title is invalid or if the user can't edit the list. If neither
266     * of these are true, then add a cancel button alongside the automatic save button. Also
267     * add an ID to the form for targeting with CSS styles.
268     *
269     * @param HTMLForm $form
270     */
271    protected function alterForm( HTMLForm $form ) {
272        if ( !$this->rev ) {
273            $form->setWrapperLegend( false );
274            $form->suppressDefaultSubmit();
275        } else {
276            $form->showCancel();
277            $form->setCancelTarget( $this->title );
278        }
279        $form->setId( 'mw-massmessage-edit-form' );
280    }
281
282    /**
283     * Return instructions for the form and / or warnings.
284     *
285     * @return string
286     */
287    protected function preHtml() {
288        $allowGlobalMessaging = $this->getConfig()->get( 'AllowGlobalMessaging' );
289
290        if ( $this->rev ) {
291            // Instructions
292            if ( $allowGlobalMessaging && count( DatabaseLookup::getDatabases() ) > 1 ) {
293                $headerKey = 'massmessage-edit-headermulti';
294            } else {
295                $headerKey = 'massmessage-edit-header';
296            }
297            $html = $this->msg( $headerKey )->parseAsBlock();
298
299            // Deleted revision warning
300            if ( $this->rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
301                $html .= Html::openElement( 'div', [ 'class' => 'mw-warning plainlinks' ] );
302                $html .= $this->msg( 'rev-deleted-text-view' )->parseAsBlock();
303                $html .= Html::closeElement( 'div' );
304            }
305
306            // Old revision warning
307            if ( $this->rev->getId() !== $this->title->getLatestRevID( IDBAccessObject::READ_LATEST ) ) {
308                $html .= $this->msg( 'editingold' )->parseAsBlock();
309            }
310        } else {
311            // Error determined in setParameter()
312            $html = $this->msg( $this->errorMsgKey )->parseAsBlock();
313        }
314        return $html;
315    }
316
317    /**
318     * @param array $data
319     * @param HTMLForm|null $form
320     * @return Status
321     */
322    public function onSubmit( array $data, HTMLForm $form = null ) {
323        if ( !$this->title ) {
324            return Status::newFatal( 'massmessage-edit-invalidtitle' );
325        }
326
327        // Parse input into target array.
328        $parseResult = self::parseInput( $data['content'] );
329        if ( !$parseResult->isGood() ) {
330            // Wikitext list of escaped invalid target strings
331            $invalidList = '* ' . implode( "\n* ", array_map( 'wfEscapeWikiText',
332                $parseResult->value ) );
333            return Status::newFatal( $this->msg( 'massmessage-edit-invalidtargets',
334                count( $parseResult->value ), $invalidList ) );
335        }
336
337        // Blank edit summary warning
338        if ( $data['summary'] === ''
339            && $this->userOptionsLookup->getOption( $this->getUser(), 'forceeditsummary' )
340            && !$this->getRequest()->getCheck( 'summarywarned' )
341        ) {
342            $form->addHiddenField( 'summarywarned', 'true' );
343            return Status::newFatal( $this->msg( 'massmessage-edit-missingsummary' ) );
344        }
345
346        $editResult = MassMessageListContentHandler::edit(
347            $this->title,
348            $data['description'],
349            $parseResult->value,
350            $data['summary'],
351            $this->permissionManager->userHasRight( $this->getUser(), 'minoredit' ) && $data['minor'],
352            $data['watch'] ? 'watch' : 'unwatch',
353            $this->getContext()
354        );
355
356        if ( !$editResult->isGood() ) {
357            return $editResult;
358        }
359
360        $this->getOutput()->redirect( $this->title->getFullURL() );
361        return Status::newGood();
362    }
363
364    public function onSuccess() {
365        // No-op: We have already redirected.
366    }
367
368    /**
369     * Parse user input into an array of targets and return it as the value of a Status object.
370     * If input contains invalid data, the value is the array of invalid target strings.
371     *
372     * @param string $input
373     * @return Status
374     */
375    protected static function parseInput( $input ) {
376        // Array of non-empty lines
377        $lines = array_filter( explode( "\n", $input ), 'trim' );
378
379        $targets = [];
380        $invalidTargets = [];
381        foreach ( $lines as $line ) {
382            $target = MassMessageListContentHandler::extractTarget( $line );
383            if ( array_key_exists( 'errors', $target ) ) {
384                $invalidTargets[] = $line;
385            }
386            $targets[] = $target;
387        }
388
389        $result = new Status;
390        if ( !$invalidTargets ) {
391            $result->setResult( true,
392                MassMessageListContentHandler::normalizeTargetArray( $targets ) );
393        } else {
394            $result->setResult( false, $invalidTargets );
395        }
396        return $result;
397    }
398
399    /**
400     * @return bool
401     */
402    public function isListed() {
403        return false;
404    }
405
406    /**
407     * @return string
408     */
409    protected function getDisplayFormat() {
410        return 'ooui';
411    }
412}