Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
35.17% |
83 / 236 |
|
26.67% |
4 / 15 |
CRAP | |
0.00% |
0 / 1 |
NewsletterContentHandler | |
35.17% |
83 / 236 |
|
26.67% |
4 / 15 |
368.79 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeEmptyContent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
unserializeContent | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getContentClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isParserCacheSupported | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSecondaryDataUpdates | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
fillParserOutput | |
67.05% |
59 / 88 |
|
0.00% |
0 / 1 |
11.90 | |||
edit | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
42 | |||
getSlotDiffRendererWithOptions | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getPublishersFromJSONData | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getNewsletterActionButtons | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
20 | |||
getHTMLForm | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
doLinkCacheQuery | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
buildUserList | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
setupNavigationLinks | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Newsletter\Content; |
4 | |
5 | use ApiMain; |
6 | use ApiUsageException; |
7 | use Content; |
8 | use DerivativeContext; |
9 | use FormatJson; |
10 | use HTMLForm; |
11 | use IContextSource; |
12 | use Iterator; |
13 | use JsonContentHandler; |
14 | use LogEventsList; |
15 | use MediaWiki\Content\Renderer\ContentParseParams; |
16 | use MediaWiki\Deferred\DeferrableUpdate; |
17 | use MediaWiki\Extension\Newsletter\Newsletter; |
18 | use MediaWiki\Html\Html; |
19 | use MediaWiki\Linker\Linker; |
20 | use MediaWiki\MediaWikiServices; |
21 | use MediaWiki\Output\OutputPage; |
22 | use MediaWiki\Parser\ParserOutput; |
23 | use MediaWiki\Request\DerivativeRequest; |
24 | use MediaWiki\Revision\SlotRenderingProvider; |
25 | use MediaWiki\SpecialPage\SpecialPage; |
26 | use MediaWiki\Status\Status; |
27 | use MediaWiki\Title\Title; |
28 | use MediaWiki\User\UserArray; |
29 | use MediaWiki\User\UserArrayFromResult; |
30 | use MWContentSerializationException; |
31 | use OOUI\ButtonGroupWidget; |
32 | use OOUI\ButtonWidget; |
33 | use ParserOptions; |
34 | use RequestContext; |
35 | |
36 | /** |
37 | * @license GPL-2.0-or-later |
38 | * @author tonythomas |
39 | */ |
40 | class NewsletterContentHandler extends JsonContentHandler { |
41 | |
42 | /** Subpage actions */ |
43 | private const NEWSLETTER_ANNOUNCE = 'announce'; |
44 | private const NEWSLETTER_SUBSCRIBE = 'subscribe'; |
45 | private const NEWSLETTER_UNSUBSCRIBE = 'unsubscribe'; |
46 | private const NEWSLETTER_SUBSCRIBERS = 'subscribers'; |
47 | |
48 | /** |
49 | * @param string $modelId |
50 | */ |
51 | public function __construct( $modelId = 'NewsletterContent' ) { |
52 | parent::__construct( $modelId ); |
53 | } |
54 | |
55 | /** |
56 | * @return NewsletterContent |
57 | */ |
58 | public function makeEmptyContent() { |
59 | return new NewsletterContent( '{"description":"","mainpage":"","publishers":[]}' ); |
60 | } |
61 | |
62 | /** |
63 | * @param string $text |
64 | * @param string|null $format |
65 | * @return NewsletterContent |
66 | * @throws MWContentSerializationException |
67 | */ |
68 | public function unserializeContent( $text, $format = null ) { |
69 | $this->checkFormat( $format ); |
70 | $content = new NewsletterContent( $text ); |
71 | if ( !$content->isValid() ) { |
72 | throw new MWContentSerializationException( 'The Newsletter content is invalid.' ); |
73 | } |
74 | return $content; |
75 | } |
76 | |
77 | /** |
78 | * @return string |
79 | */ |
80 | protected function getContentClass() { |
81 | return NewsletterContent::class; |
82 | } |
83 | |
84 | /** |
85 | * @return bool |
86 | */ |
87 | public function isParserCacheSupported() { |
88 | return false; |
89 | } |
90 | |
91 | /** |
92 | * @param Title $title The title of the page to supply the updates for. |
93 | * @param Content $content The content to generate data updates for. |
94 | * @param string $role The role (slot) in which the content is being used. |
95 | * @param SlotRenderingProvider $slotOutput A provider that can be used to gain access to |
96 | * a ParserOutput of $content by calling $slotOutput->getSlotParserOutput( $role, false ). |
97 | * @return DeferrableUpdate[] A list of DeferrableUpdate objects for putting information |
98 | * about this content object somewhere. |
99 | */ |
100 | public function getSecondaryDataUpdates( |
101 | Title $title, |
102 | Content $content, |
103 | $role, |
104 | SlotRenderingProvider $slotOutput |
105 | ) { |
106 | $user = RequestContext::getMain()->getUser(); |
107 | // @todo This user object might not be the right one in some cases. |
108 | // but that should be pretty rare in the context of newsletters. |
109 | /** @var NewsletterContent $content */ |
110 | '@phan-var NewsletterContent $content'; |
111 | $newsletterUpdate = new NewsletterDataUpdate( $content, $title, $user ); |
112 | return array_merge( |
113 | parent::getSecondaryDataUpdates( $title, $content, $role, $slotOutput ), |
114 | [ $newsletterUpdate ] |
115 | ); |
116 | } |
117 | |
118 | /** |
119 | * @inheritDoc |
120 | */ |
121 | protected function fillParserOutput( |
122 | Content $content, |
123 | ContentParseParams $cpoParams, |
124 | ParserOutput &$parserOutput |
125 | ) { |
126 | '@phan-var NewsletterContent $content'; |
127 | $title = Title::castFromPageReference( $cpoParams->getPage() ); |
128 | $parserOptions = $cpoParams->getParserOptions(); |
129 | $generateHtml = $cpoParams->getGenerateHtml(); |
130 | |
131 | $parserOutput->addModuleStyles( [ 'ext.newsletter.newsletter.styles' ] ); |
132 | |
133 | if ( $generateHtml ) { |
134 | $text = $title->getText(); |
135 | $newsletter = Newsletter::newFromName( $text ); |
136 | |
137 | $newsletterActionButtons = !$newsletter |
138 | ? '' |
139 | : $this->getNewsletterActionButtons( $newsletter, $parserOptions, $parserOutput ); |
140 | $mainTitle = $content->getMainPage(); |
141 | |
142 | $fields = [ |
143 | 'description' => [ |
144 | 'type' => 'info', |
145 | 'label-message' => 'newsletter-view-description', |
146 | 'default' => $content->getDescription(), |
147 | 'cssclass' => 'newsletter-headered-element', |
148 | 'rows' => 6, |
149 | 'readonly' => true, |
150 | ], |
151 | 'mainpage' => [ |
152 | 'type' => 'info', |
153 | 'label-message' => 'newsletter-view-mainpage', |
154 | 'default' => MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( $mainTitle ), |
155 | 'cssclass' => 'newsletter-headered-element', |
156 | 'raw' => true, |
157 | ], |
158 | 'publishers' => [ |
159 | 'type' => 'info', |
160 | 'label' => wfMessage( 'newsletter-view-publishers' )->inLanguage( |
161 | $parserOptions->getUserLangObj() ) |
162 | ->numParams( count( $content->getPublishers() ) ) |
163 | ->text(), |
164 | 'cssclass' => 'newsletter-headered-element', |
165 | ], |
166 | 'subscribers' => [ |
167 | 'type' => 'info', |
168 | 'label-message' => 'newsletter-view-subscriber-count', |
169 | 'default' => !$newsletter ? 0 : $parserOptions->getUserLangObj()->formatNum( |
170 | $newsletter->getSubscribersCount() ), |
171 | 'cssclass' => 'newsletter-headered-element', |
172 | ], |
173 | ]; |
174 | $publishersArray = $this->getPublishersFromJSONData( $content->getPublishers() ); |
175 | if ( $publishersArray && count( $publishersArray ) > 0 ) { |
176 | // Have this here to avoid calling unneeded functions |
177 | $this->doLinkCacheQuery( $publishersArray ); |
178 | $fields['publishers']['default'] = $this->buildUserList( $publishersArray ); |
179 | $fields['publishers']['raw'] = true; |
180 | } else { |
181 | // Show a message if there are no publishers instead of nothing |
182 | $fields['publishers']['default'] = wfMessage( 'newsletter-view-no-publishers' ) |
183 | ->inLanguage( $parserOptions->getUserLangObj() ) |
184 | ->escaped(); |
185 | } |
186 | if ( $newsletter ) { |
187 | // Show the 10 most recent issues if there have been announcements |
188 | $logs = ''; |
189 | $logCount = LogEventsList::showLogExtract( |
190 | $logs, // by reference |
191 | 'newsletter', |
192 | SpecialPage::getTitleFor( 'Newsletter', (string)$newsletter->getId() ), '', |
193 | [ |
194 | 'lim' => 10, |
195 | 'showIfEmpty' => false, |
196 | 'conds' => [ 'log_action' => 'issue-added' ], |
197 | 'extraUrlParams' => [ 'subtype' => 'issue-added' ], |
198 | ] |
199 | ); |
200 | if ( $logCount !== 0 ) { |
201 | $fields['issues'] = [ |
202 | 'type' => 'info', |
203 | 'raw' => true, |
204 | 'default' => $logs, |
205 | 'label' => wfMessage( 'newsletter-view-issues-log' ) |
206 | ->inLanguage( $parserOptions->getUserLangObj() ) |
207 | ->numParams( $logCount ) |
208 | ->text(), |
209 | 'cssclass' => 'newsletter-headered-element', |
210 | ]; |
211 | } |
212 | } |
213 | $form = $this->getHTMLForm( |
214 | $fields, |
215 | static function () { |
216 | return false; |
217 | } // nothing to submit - the buttons on this page are just links |
218 | ); |
219 | |
220 | $form->suppressDefaultSubmit(); |
221 | $form->prepareForm(); |
222 | |
223 | if ( !$newsletter ) { |
224 | $parserOutput->setText( $form->getBody() ); |
225 | } else { |
226 | $this->setupNavigationLinks( $newsletter, $parserOptions ); |
227 | $parserOutput->setText( $newsletterActionButtons . "<br><br>" . $form->getBody() ); |
228 | } |
229 | |
230 | } else { |
231 | $parserOutput->setText( '' ); |
232 | } |
233 | } |
234 | |
235 | /** |
236 | * @param Title $title |
237 | * @param string $description |
238 | * @param string $mainPage |
239 | * @param array $publishers |
240 | * @param string $summary |
241 | * @param IContextSource $context |
242 | * @return Status |
243 | */ |
244 | public static function edit( Title $title, $description, $mainPage, $publishers, $summary, |
245 | IContextSource $context |
246 | ) { |
247 | $jsonText = FormatJson::encode( |
248 | [ 'description' => $description, 'mainpage' => $mainPage, 'publishers' => $publishers ] |
249 | ); |
250 | if ( $jsonText === null ) { |
251 | return Status::newFatal( 'newsletter-ch-tojsonerror' ); |
252 | } |
253 | |
254 | // FIXME It would be better if this editing directly, instead of |
255 | // invoking the api. |
256 | // Ensure that a valid context is provided to the API in unit tests |
257 | $der = new DerivativeContext( $context ); |
258 | $request = new DerivativeRequest( |
259 | $context->getRequest(), |
260 | [ |
261 | 'action' => 'edit', |
262 | 'title' => $title->getFullText(), |
263 | 'contentmodel' => 'NewsletterContent', |
264 | 'text' => $jsonText, |
265 | 'summary' => $summary, |
266 | 'token' => $context->getUser()->getEditToken(), |
267 | ], |
268 | true // Treat data as POSTed |
269 | ); |
270 | $der->setRequest( $request ); |
271 | |
272 | $status = Status::newGood(); |
273 | try { |
274 | $api = new ApiMain( $der, true ); |
275 | $api->execute(); |
276 | $res = $api->getResult()->getResultData(); |
277 | if ( |
278 | !isset( $res['edit']['result'] ) |
279 | || $res['edit']['result'] !== 'Success' |
280 | ) { |
281 | if ( isset( $res['edit']['message'] ) ) { |
282 | $status->fatal( |
283 | $context->msg( |
284 | $res['edit']['message']['key'], |
285 | $res['edit']['message']['params'] |
286 | ) |
287 | ); |
288 | } else { |
289 | $status->fatal( $context->msg( |
290 | 'newsletter-ch-apierror', |
291 | $res['edit']['code'] ?? '' |
292 | ) ); |
293 | } |
294 | } |
295 | } catch ( ApiUsageException $e ) { |
296 | return Status::wrap( $e->getStatusValue() ); |
297 | } |
298 | return $status; |
299 | } |
300 | |
301 | public function getSlotDiffRendererWithOptions( IContextSource $context, $options = [] ) { |
302 | return new NewsletterSlotDiffRenderer( |
303 | $this->createTextSlotDiffRenderer( $options ), |
304 | $context |
305 | ); |
306 | } |
307 | |
308 | /** |
309 | * @param array $publishersList |
310 | * @return bool|UserArrayFromResult |
311 | */ |
312 | private function getPublishersFromJSONData( $publishersList ) { |
313 | if ( count( $publishersList ) === 0 ) { |
314 | return false; |
315 | } |
316 | |
317 | return UserArray::newFromNames( $publishersList ); |
318 | } |
319 | |
320 | /** |
321 | * Build a group of buttons: Manage, Subscribe|Unsubscribe |
322 | * Buttons will be showed to the user only if they are relevant to the current user. |
323 | * |
324 | * @param Newsletter $newsletter |
325 | * @param ParserOptions &$options |
326 | * @param ParserOutput $parserOutput |
327 | * @return string HTML for the button group |
328 | */ |
329 | private function getNewsletterActionButtons( |
330 | Newsletter $newsletter, |
331 | ParserOptions &$options, |
332 | ParserOutput $parserOutput |
333 | ) { |
334 | // We are building the 'Subscribe' action button for anonymous users as well |
335 | $user = $options->getUserIdentity(); |
336 | $id = $newsletter->getId(); |
337 | $buttons = []; |
338 | |
339 | OutputPage::setupOOUI(); |
340 | $parserOutput->setEnableOOUI( true ); |
341 | $parserOutput->addModuleStyles( [ 'oojs-ui.styles.icons-interactions' ] ); |
342 | |
343 | if ( !$newsletter->isSubscribed( $user ) ) { |
344 | $buttons[] = new ButtonWidget( |
345 | [ |
346 | 'label' => wfMessage( 'newsletter-subscribe-button' )->text(), |
347 | 'flags' => [ 'progressive' ], |
348 | 'href' => SpecialPage::getTitleFor( 'Newsletter', $id . '/' . |
349 | self::NEWSLETTER_SUBSCRIBE )->getFullURL() |
350 | |
351 | ] |
352 | ); |
353 | } else { |
354 | $buttons[] = new ButtonWidget( |
355 | [ |
356 | 'label' => wfMessage( 'newsletter-unsubscribe-button' )->text(), |
357 | 'flags' => [ 'destructive' ], |
358 | 'href' => SpecialPage::getTitleFor( 'Newsletter', $id . '/' . |
359 | self::NEWSLETTER_UNSUBSCRIBE )->getFullURL() |
360 | |
361 | ] |
362 | ); |
363 | } |
364 | $userFactory = MediaWikiServices::getInstance()->getUserFactory(); |
365 | if ( $newsletter->canManage( $userFactory->newFromUserIdentity( $user ) ) ) { |
366 | $buttons[] = new ButtonWidget( |
367 | [ |
368 | 'label' => wfMessage( 'newsletter-manage-button' )->text(), |
369 | 'icon' => 'settings', |
370 | 'href' => Title::makeTitleSafe( NS_NEWSLETTER, $newsletter->getName() )->getEditURL(), |
371 | |
372 | ] |
373 | ); |
374 | $buttons[] = new ButtonWidget( |
375 | [ |
376 | 'label' => wfMessage( 'newsletter-subscribers-button' )->text(), |
377 | 'icon' => 'info', |
378 | 'href' => SpecialPage::getTitleFor( 'Newsletter', $id . '/' . |
379 | self::NEWSLETTER_SUBSCRIBERS )->getFullURL() |
380 | |
381 | ] |
382 | ); |
383 | } |
384 | if ( $newsletter->isPublisher( $user ) ) { |
385 | $buttons[] = new ButtonWidget( |
386 | [ |
387 | 'label' => wfMessage( 'newsletter-announce-button' )->text(), |
388 | 'icon' => 'speechBubble', |
389 | 'href' => SpecialPage::getTitleFor( 'Newsletter', $id . '/' . |
390 | self::NEWSLETTER_ANNOUNCE )->getFullURL() |
391 | ] |
392 | ); |
393 | } |
394 | |
395 | $widget = new ButtonGroupWidget( [ 'items' => $buttons ] ); |
396 | return $widget->toString(); |
397 | } |
398 | |
399 | /** |
400 | * Create a common HTMLForm which can be used by specific page actions |
401 | * |
402 | * @param array $fields array of form fields |
403 | * @param callback $submit submit callback |
404 | * |
405 | * @return HTMLForm |
406 | */ |
407 | private function getHTMLForm( array $fields, /* callable */ $submit ) { |
408 | $form = HTMLForm::factory( |
409 | 'ooui', |
410 | $fields, |
411 | RequestContext::getMain() |
412 | ); |
413 | $form->setSubmitCallback( $submit ); |
414 | return $form; |
415 | } |
416 | |
417 | /** |
418 | * Batch query to determine whether user pages and user talk pages exist |
419 | * or not and add them to LinkCache |
420 | * |
421 | * @param Iterator $users |
422 | */ |
423 | private function doLinkCacheQuery( Iterator $users ) { |
424 | $batch = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch(); |
425 | foreach ( $users as $user ) { |
426 | $batch->addObj( $user->getUserPage() ); |
427 | $batch->addObj( $user->getTalkPage() ); |
428 | } |
429 | $batch->execute(); |
430 | } |
431 | |
432 | /** |
433 | * Get a list of users with user-related links next to each username |
434 | * |
435 | * @param Iterator $users |
436 | * |
437 | * @return string |
438 | */ |
439 | private function buildUserList( Iterator $users ) { |
440 | $str = ''; |
441 | foreach ( $users as $user ) { |
442 | $str .= Html::rawElement( |
443 | 'li', |
444 | [], |
445 | Linker::userLink( $user->getId(), $user->getName() ) . |
446 | Linker::userToolLinks( $user->getId(), $user->getName() ) |
447 | ); |
448 | } |
449 | return Html::rawElement( 'ul', [], $str ); |
450 | } |
451 | |
452 | /** |
453 | * @param Newsletter $newsletter |
454 | * @param ParserOptions $options |
455 | */ |
456 | private function setupNavigationLinks( Newsletter $newsletter, ParserOptions $options ) { |
457 | $context = RequestContext::getMain(); |
458 | $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); |
459 | $listLink = $linkRenderer->makeKnownLink( |
460 | SpecialPage::getTitleFor( 'Newsletters' ), |
461 | $context->msg( 'backlinksubtitle', |
462 | $context->msg( 'newsletter-subtitlelinks-list' )->text() |
463 | )->text() |
464 | ); |
465 | |
466 | $newsletterLink = Linker::makeSelfLinkObj( |
467 | SpecialPage::getTitleFor( 'Newsletter', (string)$newsletter->getId() ), |
468 | htmlspecialchars( $newsletter->getName() ) |
469 | ); |
470 | |
471 | $context->getOutput()->setSubtitle( |
472 | $options->getUserLangObj()->pipeList( [ $listLink, $newsletterLink ] ) |
473 | ); |
474 | } |
475 | } |