Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 251 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
NewsletterEditPage | |
0.00% |
0 / 251 |
|
0.00% |
0 / 9 |
2450 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
edit | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 | |||
getEscapedName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getManageForm | |
0.00% |
0 / 99 |
|
0.00% |
0 / 1 |
380 | |||
getForm | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
getFormFields | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
2 | |||
attemptSave | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
72 | |||
submitManageForm | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
110 | |||
getIdsFromUsers | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Newsletter; |
4 | |
5 | use BadRequestError; |
6 | use HTMLForm; |
7 | use IContextSource; |
8 | use MediaWiki\Extension\Newsletter\Content\NewsletterContentHandler; |
9 | use MediaWiki\MediaWikiServices; |
10 | use MediaWiki\Output\OutputPage; |
11 | use MediaWiki\Revision\RevisionRecord; |
12 | use MediaWiki\Revision\SlotRecord; |
13 | use MediaWiki\Status\Status; |
14 | use MediaWiki\Title\Title; |
15 | use MediaWiki\User\User; |
16 | use MediaWiki\User\UserArray; |
17 | use PermissionsError; |
18 | use ReadOnlyError; |
19 | use ThrottledError; |
20 | use UserBlockedError; |
21 | |
22 | /** |
23 | * @license GPL-2.0-or-later |
24 | * @author tonythomas |
25 | */ |
26 | |
27 | class 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 | } |