Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 251
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewsletterEditPage
0.00% covered (danger)
0.00%
0 / 251
0.00% covered (danger)
0.00%
0 / 9
2450
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 edit
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 getEscapedName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getManageForm
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 1
380
 getForm
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 getFormFields
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 attemptSave
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
72
 submitManageForm
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
110
 getIdsFromUsers
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\Newsletter;
4
5use BadRequestError;
6use HTMLForm;
7use IContextSource;
8use MediaWiki\Extension\Newsletter\Content\NewsletterContentHandler;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Output\OutputPage;
11use MediaWiki\Revision\RevisionRecord;
12use MediaWiki\Revision\SlotRecord;
13use MediaWiki\Status\Status;
14use MediaWiki\Title\Title;
15use MediaWiki\User\User;
16use MediaWiki\User\UserArray;
17use PermissionsError;
18use ReadOnlyError;
19use ThrottledError;
20use UserBlockedError;
21
22/**
23 * @license GPL-2.0-or-later
24 * @author tonythomas
25 */
26
27class NewsletterEditPage {
28
29    /** @var bool */
30    protected $createNew;
31
32    /** @var IContextSource */
33    protected $context;
34
35    /** @var bool */
36    protected $readOnly = false;
37
38    /** @var Newsletter|null */
39    protected $newsletter;
40
41    /** @var User */
42    private $user;
43
44    /** @var Title */
45    private $title;
46
47    /** @var OutputPage */
48    private $out;
49
50    public function __construct( IContextSource $context, Newsletter $newsletter = null ) {
51        $this->context = $context;
52        $this->user = $context->getUser();
53        $this->title = $context->getTitle();
54        $this->out = $context->getOutput();
55        $this->newsletter = $newsletter;
56    }
57
58    public function edit() {
59        $services = MediaWikiServices::getInstance();
60        if ( $services->getReadOnlyMode()->isReadOnly() ) {
61            throw new ReadOnlyError;
62        }
63        $this->createNew = !$this->title->exists();
64        if ( !$this->createNew ) {
65            // A newsletter exists, lets open the edit page
66            $block = $this->user->getBlock();
67            if ( $block ) {
68                throw new UserBlockedError( $block );
69            }
70
71            if ( !$this->newsletter->canManage( $this->user ) ) {
72                throw new PermissionsError( 'newsletter-manage' );
73            }
74
75            $this->out->setPageTitleMsg(
76                $this->context->msg( 'newsletter-manage' )
77                    ->params( $this->newsletter->getName() )
78            );
79
80            $revId = $this->context->getRequest()->getInt( 'undoafter' );
81            $undoId = $this->context->getRequest()->getInt( 'undo' );
82            $oldId = $this->context->getRequest()->getInt( 'oldid' );
83            $this->getManageForm( $revId, $undoId, $oldId )->show();
84        } else {
85            $permManager = $services->getPermissionManager();
86            $permErrors = $permManager->getPermissionErrors( 'edit', $this->user, $this->title );
87            if ( count( $permErrors ) ) {
88                $this->out->showPermissionsErrorPage( $permErrors );
89                return;
90            }
91
92            $this->out->setPageTitle(
93                $this->context->msg( 'newslettercreate', $this->title->getPrefixedText() )->text()
94            );
95            $this->getForm()->show();
96        }
97    }
98
99    /**
100     * We need the escaped newsletter name several times so
101     * extract the method here.
102     *
103     * @return string
104     */
105    protected function getEscapedName() {
106        return htmlspecialchars( $this->newsletter->getName() );
107    }
108
109    /**
110     * Create the manage form. If this is on an undo revision action, $revId would be set, and we
111     * manually load in form data from the reverted revision.
112     *
113     * @param int $revId
114     * @param int $undoId
115     * @param int $oldId
116     * @return HTMLForm
117     * @throws BadRequestError Exception thrown on false revision id on revision undo, etc
118     */
119    protected function getManageForm( $revId, $undoId, $oldId ) {
120        $publishers = UserArray::newFromIDs( $this->newsletter->getPublishers() );
121        $publishersNames = [];
122
123        $mainTitle = Title::newFromID( $this->newsletter->getPageId() );
124        foreach ( $publishers as $publisher ) {
125            $publishersNames[] = $publisher->getName();
126        }
127
128        if ( $mainTitle === null ) {
129            $mainText = null;
130        } else {
131            $mainText = $mainTitle->getPrefixedText();
132        }
133
134        $fields = [
135            'MainPage' => [
136                'type' => 'title',
137                'label-message' => 'newsletter-manage-title',
138                'default' => $mainText,
139                'required' => true,
140            ],
141            'Description' => [
142                'type' => 'textarea',
143                'label-message' => 'newsletter-manage-description',
144                'rows' => 6,
145                'default' => $this->newsletter->getDescription(),
146                'required' => true,
147            ],
148            'Publishers' => [
149                'type' => 'usersmultiselect',
150                'label-message' => 'newsletter-manage-publishers',
151                'exists' => true,
152                'default' => implode( "\n", $publishersNames ),
153            ],
154            'Summary' => [
155                'type' => 'text',
156                'label-message' => 'newsletter-manage-summary',
157                'required' => false,
158            ],
159            'Confirm' => [
160                'type' => 'hidden',
161                'default' => false,
162            ],
163        ];
164
165        // Ensure action is not editing the current revision
166        if ( ( $revId && $undoId ) || $oldId ) {
167            $oldRevRecord = null;
168            $revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
169            // Editing a previous revision
170            if ( $oldId ) {
171                $oldRevRecord = $revLookup->getRevisionById( $oldId );
172                $oldMainSlot = $oldRevRecord->getSlot(
173                    SlotRecord::MAIN,
174                    RevisionRecord::RAW
175                );
176                if ( $oldMainSlot->getModel() === 'NewsletterContent'
177                    && !$oldRevRecord->isDeleted( RevisionRecord::DELETED_TEXT )
178                ) {
179                    $fields['Summary']['default'] = '';
180                }
181            } elseif /* Undoing the latest revision */ ( $revId && $undoId ) {
182                $oldRevRecord = $revLookup->getRevisionById( $revId );
183                $undoRevRecord = $revLookup->getRevisionById( $undoId );
184                $undoMainSlot = $undoRevRecord->getSlot(
185                    SlotRecord::MAIN,
186                    RevisionRecord::RAW
187                );
188                if ( $undoRevRecord->isCurrent()
189                    && $undoMainSlot->getModel() === 'NewsletterContent'
190                    && !$undoRevRecord->isDeleted( RevisionRecord::DELETED_TEXT )
191                ) {
192                    $userText = $undoRevRecord->getUser() ?
193                        $undoRevRecord->getUser()->getName() :
194                        '';
195                    $fields['Summary']['default'] =
196                        $this->context->msg( 'undo-summary' )
197                            ->params( $undoRevRecord->getId(), $userText )
198                            ->inContentLanguage()
199                            ->text();
200                } else /* User attempts to undo prior revision */ {
201                    throw new BadRequestError(
202                        'newsletter-oldrev-undo-error-title',
203                        'newsletter-oldrev-undo-error-body'
204                    );
205                }
206            }
207
208            if ( $oldRevRecord
209                && $oldRevRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
210                    ->getModel() === 'NewsletterContent'
211                && !$oldRevRecord->isDeleted( RevisionRecord::DELETED_TEXT )
212            ) {
213                $content = $oldRevRecord->getContent( SlotRecord::MAIN );
214                '@phan-var \MediaWiki\Extension\Newsletter\Content\NewsletterContent $content';
215                $fields['MainPage']['default'] = $content->getMainPage()->getPrefixedText();
216                $fields['Description']['default'] = $content->getDescription();
217                // HTMLUsersMultiselectField expects a string, so we implode here
218                $publisherNames = $content->getPublishers();
219                $fields['Publishers']['default'] = implode( "\n", $publishersNames );
220            }
221        }
222
223        if ( $this->context->getRequest()->wasPosted() ) {
224            // @todo Make this work properly for double submissions
225            $fields['Confirm']['default'] = true;
226        }
227
228        $form = HTMLForm::factory(
229            'ooui',
230            $fields,
231            $this->context
232        );
233        $form->setSubmitCallback( [ $this, 'submitManageForm' ] );
234        $form->setAction( $this->title->getLocalURL( 'action=submit' ) );
235        $form->addHeaderHtml(
236            $this->context->msg( 'newsletter-manage-text' )
237                ->params( $this->newsletter->getName() )->parse()
238        );
239        $form->setId( 'newsletter-manage-form' );
240        $form->setSubmitID( 'newsletter-manage-button' );
241        $form->setSubmitTextMsg( 'newsletter-managenewsletter-button' );
242        return $form;
243    }
244
245    protected function getForm() {
246        $form = HTMLForm::factory(
247            'ooui',
248            $this->getFormFields(),
249            $this->context
250        );
251        $form->setSubmitCallback( [ $this, 'attemptSave' ] );
252        $form->setAction( $this->title->getLocalURL( 'action=edit' ) );
253        $form->addHeaderHtml( $this->context->msg( 'newslettercreate-text' )->parseAsBlock() );
254        // Retain query parameters (uselang etc)
255        $params = array_diff_key(
256            $this->context->getRequest()->getQueryValues(), [ 'title' => null ] );
257        $form->addHiddenField( 'redirectparams', wfArrayToCgi( $params ) );
258        $form->setSubmitTextMsg( 'newsletter-create-submit' );
259
260        // @todo $form->addPostHtml( save/copyright warnings etc. );
261        return $form;
262    }
263
264    /**
265     * @return array
266     */
267    protected function getFormFields() {
268        return [
269            'name' => [
270                'type' => 'text',
271                'required' => true,
272                'label-message' => 'newsletter-name',
273                'maxlength' => 120,
274                'default' => $this->title->getText(),
275            ],
276            'mainpage' => [
277                'type' => 'title',
278                'required' => true,
279                'label-message' => 'newsletter-title',
280            ],
281            'description' => [
282                'type' => 'textarea',
283                'required' => true,
284                'label-message' => 'newsletter-desc',
285                'rows' => 15,
286                'maxlength' => 600000,
287            ],
288        ];
289    }
290
291    /**
292     * Do input validation, error handling and create a new newletter.
293     *
294     * This is only for saving a new page. Modifying an existing page
295     * is submitManageForm()
296     *
297     * @param array $input The data entered by user in the form
298     * @throws ThrottledError
299     * @return Status
300     */
301    public function attemptSave( array $input ) {
302        $data = [
303            'Name' => trim( $input['name'] ),
304            'Description' => trim( $input['description'] ),
305            'MainPage' => Title::newFromText( $input['mainpage'] ),
306        ];
307
308        $validator = new NewsletterValidator( $data );
309        $validation = $validator->validate( true );
310        if ( !$validation->isGood() ) {
311            // Invalid input was entered
312            return $validation;
313        }
314
315        $mainPageId = $data['MainPage']->getArticleID();
316        $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA );
317        $rows = $dbr->select(
318            'nl_newsletters',
319            [ 'nl_name', 'nl_main_page_id', 'nl_active' ],
320            $dbr->makeList( [
321                'nl_name' => $data['Name'],
322                $dbr->makeList(
323                    [
324                        'nl_main_page_id' => $mainPageId,
325                        'nl_active' => 1
326                    ], LIST_AND )
327            ], LIST_OR ),
328            __METHOD__
329        );
330        // Check whether another existing newsletter has the same name or main page
331        foreach ( $rows as $row ) {
332            if ( $row->nl_name === $data['Name'] ) {
333                return Status::newFatal( 'newsletter-exist-error', $data['Name'] );
334            } elseif ( (int)$row->nl_main_page_id === $mainPageId && (int)$row->nl_active === 1 ) {
335                return Status::newFatal( 'newsletter-mainpage-in-use' );
336            }
337        }
338
339        if ( $this->user->pingLimiter( 'newsletter' ) ) {
340            // Default user access level for creating a newsletter is quite low
341            // so add a throttle here to prevent abuse (eg. mass vandalism spree)
342            throw new ThrottledError;
343        }
344        $title = Title::makeTitleSafe( NS_NEWSLETTER, $data['Name'] );
345        $editSummaryMsg = $this->context->msg( 'newsletter-create-editsummary' );
346        $result = NewsletterContentHandler::edit(
347            $title,
348            $data['Description'],
349            $input['mainpage'],
350            [ $this->user->getName() ],
351            $editSummaryMsg->inContentLanguage()->plain(),
352            $this->context
353        );
354        if ( $result->isGood() ) {
355            $this->out->addWikiMsg( 'newsletter-create-confirmation', $data['Name'] );
356            return Status::newGood();
357        } else {
358            return $result;
359        }
360    }
361
362    /**
363     * Submit callback for the manage form.
364     *
365     * This is only for editing an existing page. Making a new page
366     * is attemptSave()
367     *
368     * @todo Move most of this code out of SpecialNewsletter class
369     * @param array $data
370     *
371     * @return Status|bool true on success, Status fatal otherwise
372     */
373    public function submitManageForm( array $data ) {
374        $confirmed = (bool)$data['Confirm'];
375
376        $description = trim( $data['Description'] );
377        $mainPage = Title::newFromText( $data['MainPage'] );
378
379        if ( !$mainPage ) {
380            return Status::newFatal( 'newsletter-create-mainpage-error' );
381        }
382
383        $formData = [
384            'Description' => $description,
385            'MainPage' => $mainPage,
386        ];
387
388        $validator = new NewsletterValidator( $formData );
389        $validation = $validator->validate( false );
390        if ( !$validation->isGood() ) {
391            // Invalid input was entered
392            return $validation;
393        }
394
395        $newsletterId = $this->newsletter->getId();
396
397        $title = Title::makeTitleSafe( NS_NEWSLETTER, $this->newsletter->getName() );
398
399        $publisherNames = $data['Publishers'] ? explode( "\n", $data['Publishers'] ) : [];
400
401        // Ask for confirmation before removing all the publishers
402        if ( !$confirmed && count( $publisherNames ) === 0 ) {
403            return Status::newFatal( 'newsletter-manage-no-publishers' );
404        }
405
406        /** @var User[] $newPublishers */
407        $newPublishers = array_map( [ User::class, 'newFromName' ], $publisherNames );
408        $newPublishersIds = self::getIdsFromUsers( $newPublishers );
409
410        // Confirm whether the current user (if already a publisher)
411        // wants to be removed from the publishers group
412        $user = $this->user;
413        if ( !$confirmed && $this->newsletter->isPublisher( $user )
414            && !in_array( $user->getId(), $newPublishersIds )
415        ) {
416            return Status::newFatal( 'newsletter-manage-remove-self-publisher' );
417        }
418
419        $editResult = NewsletterContentHandler::edit(
420            $title,
421            $description,
422            $mainPage->getFullText(),
423            $publisherNames,
424            trim( $data['Summary'] ),
425            $this->context
426        );
427
428        if ( $editResult->isGood() ) {
429            $this->out->redirect( $title->getLocalURL() );
430        } else {
431            return $editResult;
432        }
433
434        return true;
435    }
436
437    /**
438     * Helper function for submitManageForm() to get user IDs from an array
439     * of User objects because we need to do comparison. This is not related
440     * to this class at all. :-/
441     *
442     * @param User[] $users
443     * @return int[]
444     */
445    private static function getIdsFromUsers( $users ) {
446        $ids = [];
447        foreach ( $users as $user ) {
448            $ids[] = $user->getId();
449        }
450        return $ids;
451    }
452
453}