Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
25.45% |
57 / 224 |
|
6.67% |
1 / 15 |
CRAP | |
0.00% |
0 / 1 |
MassMessageListContentHandler | |
25.45% |
57 / 224 |
|
6.67% |
1 / 15 |
1172.50 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
makeEmptyContent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getContentClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSlotDiffRendererWithOptions | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
isParserCacheSupported | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
edit | |
92.86% |
26 / 28 |
|
0.00% |
0 / 1 |
4.01 | |||
normalizeTargetArray | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
compareTargets | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
8.12 | |||
extractTarget | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
12 | |||
getPageLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPageViewLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fillParserOutput | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
20 | |||
getTargetsHtml | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
110 | |||
getTargetsBySite | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getAddForm | |
0.00% |
0 / 65 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace MediaWiki\MassMessage\Content; |
4 | |
5 | use MediaWiki\Api\ApiMain; |
6 | use MediaWiki\Api\ApiUsageException; |
7 | use MediaWiki\Content\Content; |
8 | use MediaWiki\Content\ContentHandler; |
9 | use MediaWiki\Content\JsonContentHandler; |
10 | use MediaWiki\Content\Renderer\ContentParseParams; |
11 | use MediaWiki\Context\DerivativeContext; |
12 | use MediaWiki\Context\IContextSource; |
13 | use MediaWiki\Context\RequestContext; |
14 | use MediaWiki\Html\Html; |
15 | use MediaWiki\Json\FormatJson; |
16 | use MediaWiki\Language\Language; |
17 | use MediaWiki\Linker\Linker; |
18 | use MediaWiki\MainConfigNames; |
19 | use MediaWiki\MassMessage\Lookup\DatabaseLookup; |
20 | use MediaWiki\MassMessage\UrlHelper; |
21 | use MediaWiki\MediaWikiServices; |
22 | use MediaWiki\Output\OutputPage; |
23 | use MediaWiki\Parser\ParserOutput; |
24 | use MediaWiki\Request\DerivativeRequest; |
25 | use MediaWiki\Status\Status; |
26 | use MediaWiki\Title\Title; |
27 | use MediaWiki\Widget\TitleInputWidget; |
28 | use OOUI\ActionFieldLayout; |
29 | use OOUI\ButtonInputWidget; |
30 | use OOUI\ComboBoxInputWidget; |
31 | use OOUI\FieldLayout; |
32 | use OOUI\FormLayout; |
33 | |
34 | class MassMessageListContentHandler extends JsonContentHandler { |
35 | |
36 | /** |
37 | * @param string $modelId |
38 | */ |
39 | public function __construct( $modelId = 'MassMessageListContent' ) { |
40 | parent::__construct( $modelId ); |
41 | } |
42 | |
43 | /** |
44 | * @return MassMessageListContent |
45 | */ |
46 | public function makeEmptyContent() { |
47 | return new MassMessageListContent( '{"description":"","targets":[]}' ); |
48 | } |
49 | |
50 | /** |
51 | * @return string |
52 | */ |
53 | protected function getContentClass() { |
54 | return MassMessageListContent::class; |
55 | } |
56 | |
57 | /** |
58 | * @param IContextSource $context |
59 | * @param array $options |
60 | * @return MassMessageListSlotDiffRenderer |
61 | */ |
62 | public function getSlotDiffRendererWithOptions( IContextSource $context, $options = [] ) { |
63 | return new MassMessageListSlotDiffRenderer( |
64 | $this->createTextSlotDiffRenderer( $options ), |
65 | $context |
66 | ); |
67 | } |
68 | |
69 | /** |
70 | * @return bool |
71 | */ |
72 | public function isParserCacheSupported() { |
73 | return true; |
74 | } |
75 | |
76 | /** |
77 | * Edit a delivery list via the edit API |
78 | * @param Title $title |
79 | * @param string $description |
80 | * @param array $targets |
81 | * @param string $summary Message key for edit summary |
82 | * @param bool $isMinor Is this a minor edit |
83 | * @param string $watchlist Value to pass to the edit API for the watchlist parameter. |
84 | * @param IContextSource $context The calling context |
85 | * @return Status |
86 | */ |
87 | public static function edit( |
88 | Title $title, $description, $targets, $summary, $isMinor, $watchlist, IContextSource $context |
89 | ) { |
90 | $jsonText = FormatJson::encode( |
91 | [ 'description' => $description, 'targets' => $targets ] |
92 | ); |
93 | if ( $jsonText === null ) { |
94 | return Status::newFatal( 'massmessage-ch-tojsonerror' ); |
95 | } |
96 | |
97 | // Ensure that a valid context is provided to the API in unit tests |
98 | $der = new DerivativeContext( $context ); |
99 | $requestParameters = [ |
100 | 'action' => 'edit', |
101 | 'title' => $title->getFullText(), |
102 | 'contentmodel' => 'MassMessageListContent', |
103 | 'text' => $jsonText, |
104 | 'watchlist' => $watchlist, |
105 | 'summary' => $summary, |
106 | 'token' => $context->getUser()->getEditToken(), |
107 | ]; |
108 | if ( $isMinor ) { |
109 | $requestParameters['minor'] = $isMinor; |
110 | } |
111 | $request = new DerivativeRequest( |
112 | $context->getRequest(), |
113 | $requestParameters, |
114 | // Treat data as POSTed |
115 | true |
116 | ); |
117 | $der->setRequest( $request ); |
118 | |
119 | try { |
120 | $api = new ApiMain( $der, true ); |
121 | $api->execute(); |
122 | } catch ( ApiUsageException $e ) { |
123 | return Status::wrap( $e->getStatusValue() ); |
124 | } |
125 | return Status::newGood(); |
126 | } |
127 | |
128 | /** |
129 | * Deduplicate and sort a target array |
130 | * @param array[] $targets |
131 | * @return array[] |
132 | */ |
133 | public static function normalizeTargetArray( $targets ) { |
134 | $targets = array_unique( $targets, SORT_REGULAR ); |
135 | usort( $targets, [ __CLASS__, 'compareTargets' ] ); |
136 | return $targets; |
137 | } |
138 | |
139 | /** |
140 | * Compare two targets for ordering |
141 | * @param array $a |
142 | * @param array $b |
143 | * @return int |
144 | */ |
145 | public static function compareTargets( $a, $b ) { |
146 | if ( !array_key_exists( 'site', $a ) && array_key_exists( 'site', $b ) ) { |
147 | return -1; |
148 | } elseif ( array_key_exists( 'site', $a ) && !array_key_exists( 'site', $b ) ) { |
149 | return 1; |
150 | } elseif ( array_key_exists( 'site', $a ) && array_key_exists( 'site', $b ) |
151 | && $a['site'] !== $b['site'] |
152 | ) { |
153 | return strcmp( $a['site'], $b['site'] ); |
154 | } else { |
155 | return strcmp( $a['title'], $b['title'] ); |
156 | } |
157 | } |
158 | |
159 | /** |
160 | * Helper function to extract and validate title and site (if specified) from a target string |
161 | * @param string $target |
162 | * @return array Contains an 'errors' key for an array of errors if the string is invalid |
163 | */ |
164 | public static function extractTarget( $target ) { |
165 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
166 | |
167 | $target = trim( $target ); |
168 | $delimiterPos = strrpos( $target, '@' ); |
169 | if ( $delimiterPos !== false && $delimiterPos < strlen( $target ) ) { |
170 | $titleText = substr( $target, 0, $delimiterPos ); |
171 | $site = strtolower( substr( $target, $delimiterPos + 1 ) ); |
172 | } else { |
173 | $titleText = $target; |
174 | $site = null; |
175 | } |
176 | |
177 | $result = []; |
178 | |
179 | $title = Title::newFromText( $titleText ); |
180 | if ( !$title |
181 | || $title->getText() === '' |
182 | || !$title->canExist() |
183 | ) { |
184 | $result['errors'][] = 'invalidtitle'; |
185 | } else { |
186 | // Use the canonical form. |
187 | $result['title'] = $title->getPrefixedText(); |
188 | } |
189 | |
190 | if ( $site !== null && $site !== UrlHelper::getBaseUrl( $config->get( MainConfigNames::CanonicalServer ) ) ) { |
191 | if ( !$config->get( 'AllowGlobalMessaging' ) || DatabaseLookup::getDBName( $site ) === null ) { |
192 | $result['errors'][] = 'invalidsite'; |
193 | } else { |
194 | $result['site'] = $site; |
195 | } |
196 | } elseif ( $title && $title->isExternal() ) { |
197 | // Target has site set to current wiki, but external title |
198 | // TODO: Provide better error message? |
199 | $result['errors'][] = 'invalidtitle'; |
200 | } |
201 | |
202 | return $result; |
203 | } |
204 | |
205 | /** |
206 | * @param Title $title |
207 | * @param Content|null $content |
208 | * @return Language |
209 | */ |
210 | public function getPageLanguage( Title $title, ?Content $content = null ) { |
211 | // This class inherits from JsonContentHandler, which hardcodes English. |
212 | // Use the default method from ContentHandler instead to get the page/site language. |
213 | return ContentHandler::getPageLanguage( $title, $content ); |
214 | } |
215 | |
216 | /** |
217 | * @param Title $title |
218 | * @param Content|null $content |
219 | * @return Language |
220 | */ |
221 | public function getPageViewLanguage( Title $title, ?Content $content = null ) { |
222 | // Most of the interface is rendered in user language |
223 | return RequestContext::getMain()->getLanguage(); |
224 | } |
225 | |
226 | /** |
227 | * @inheritDoc |
228 | */ |
229 | protected function fillParserOutput( |
230 | Content $content, |
231 | ContentParseParams $cpoParams, |
232 | ParserOutput &$output |
233 | ) { |
234 | '@phan-var MassMessageListContent $content'; |
235 | $services = MediaWikiServices::getInstance(); |
236 | |
237 | $page = $cpoParams->getPage(); |
238 | $revId = $cpoParams->getRevId(); |
239 | $parserOptions = $cpoParams->getParserOptions(); |
240 | // Parse the description text. |
241 | $output = $services->getParser() |
242 | ->parse( $content->getDescription(), $page, $parserOptions, true, true, $revId ); |
243 | $services->getTrackingCategories()->addTrackingCategory( $output, 'massmessage-list-category', $page ); |
244 | $lang = $parserOptions->getUserLangObj(); |
245 | |
246 | if ( $content->hasInvalidTargets() ) { |
247 | $warning = Html::element( 'p', [ 'class' => 'error' ], |
248 | wfMessage( 'massmessage-content-invalidtargets' )->inLanguage( $lang )->text() |
249 | ); |
250 | } else { |
251 | $warning = ''; |
252 | } |
253 | |
254 | // Mark the description language (may be different from user language used to render the rest of the page) |
255 | $description = $output->getRawText(); |
256 | $title = Title::castFromPageReference( $page ); |
257 | $pageLang = $title->getPageLanguage(); |
258 | $attribs = [ 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir() ]; |
259 | |
260 | $output->setEnableOOUI( true ); |
261 | OutputPage::setupOOUI(); |
262 | $output->setText( $warning . Html::rawElement( 'div', $attribs, $description ) . self::getAddForm( $lang ) |
263 | . $this->getTargetsHtml( $content, $lang ) ); |
264 | |
265 | // Update the links table. |
266 | $targets = $content->getTargets(); |
267 | foreach ( $targets as $target ) { |
268 | if ( !array_key_exists( 'site', $target ) ) { |
269 | $output->addLink( Title::newFromText( $target['title'] ) ); |
270 | } else { |
271 | $output->addExternalLink( |
272 | '//' . $target['site'] . $services->getMainConfig()->get( MainConfigNames::Script ) |
273 | . '?title=' . Title::newFromText( $target['title'] )->getPrefixedURL() ); |
274 | } |
275 | } |
276 | |
277 | $output->addModuleStyles( [ 'ext.MassMessage.styles' ] ); |
278 | $output->addModules( [ 'ext.MassMessage.content' ] ); |
279 | } |
280 | |
281 | /** |
282 | * Helper function for fillParserOutput; return HTML for displaying the list of pages. |
283 | * Note that the function assumes that the contents are valid. |
284 | * |
285 | * @param MassMessageListContent $content |
286 | * @param Language $lang |
287 | * @return string |
288 | */ |
289 | private function getTargetsHtml( MassMessageListContent $content, Language $lang ) { |
290 | $services = MediaWikiServices::getInstance(); |
291 | |
292 | $html = Html::element( 'h2', [], |
293 | wfMessage( 'massmessage-content-pages' )->inLanguage( $lang )->text() ); |
294 | |
295 | $sites = $this->getTargetsBySite( $content ); |
296 | |
297 | // If the list is empty |
298 | if ( count( $sites ) === 0 ) { |
299 | $html .= Html::element( 'p', [], |
300 | wfMessage( 'massmessage-content-empty' )->inLanguage( $lang )->text() ); |
301 | return $html; |
302 | } |
303 | |
304 | // Use LinkBatch to cache existence for all local targets for later use by Linker. |
305 | if ( array_key_exists( 'local', $sites ) ) { |
306 | $lb = $services->getLinkBatchFactory()->newLinkBatch(); |
307 | foreach ( $sites['local'] as $target ) { |
308 | $lb->addObj( Title::newFromText( $target ) ); |
309 | } |
310 | $lb->execute(); |
311 | } |
312 | |
313 | // Determine whether there are targets on external wikis. |
314 | $printSites = count( $sites ) !== 1 || !array_key_exists( 'local', $sites ); |
315 | $linkRenderer = $services->getLinkRenderer(); |
316 | foreach ( $sites as $site => $targets ) { |
317 | if ( $printSites ) { |
318 | if ( $site === 'local' ) { |
319 | $html .= Html::element( 'p', [], |
320 | wfMessage( 'massmessage-content-localpages' )->inLanguage( $lang )->text() |
321 | ); |
322 | } else { |
323 | $html .= Html::element( 'p', [], |
324 | wfMessage( 'massmessage-content-pagesonsite', $site )->inLanguage( $lang ) |
325 | ->text() |
326 | ); |
327 | } |
328 | } |
329 | |
330 | $html .= Html::openElement( 'ul' ); |
331 | foreach ( $targets as $target ) { |
332 | $title = Title::newFromText( $target ); |
333 | |
334 | // Generate the HTML for the link to the target. |
335 | if ( $site === 'local' ) { |
336 | $targetLink = $linkRenderer->makeLink( $title ); |
337 | } else { |
338 | $script = $services->getMainConfig()->get( MainConfigNames::Script ); |
339 | $targetLink = Linker::makeExternalLink( |
340 | "//$site$script?title=" . $title->getPrefixedURL(), |
341 | $title->getPrefixedText() |
342 | ); |
343 | } |
344 | |
345 | // Generate the HTML for the remove link. |
346 | $removeLink = Html::element( 'a', |
347 | [ |
348 | 'data-title' => $title->getPrefixedText(), |
349 | 'data-site' => $site, |
350 | 'href' => '#', |
351 | ], |
352 | wfMessage( 'massmessage-content-remove' )->inLanguage( $lang )->text() |
353 | ); |
354 | |
355 | $html .= Html::openElement( 'li' ); |
356 | $html .= Html::rawElement( 'span', [ 'class' => 'mw-massmessage-targetlink' ], |
357 | $targetLink ); |
358 | $html .= Html::rawElement( 'span', [ 'class' => 'mw-massmessage-removelink' ], |
359 | '(' . $removeLink . ')' ); |
360 | $html .= Html::closeElement( 'li' ); |
361 | } |
362 | $html .= Html::closeElement( 'ul' ); |
363 | } |
364 | |
365 | return $html; |
366 | } |
367 | |
368 | /** |
369 | * Helper function for getTargetsHtml; return the array of targets sorted by site. |
370 | * Note that the function assumes that the contents are valid. |
371 | * |
372 | * @param MassMessageListContent $content |
373 | * @return array |
374 | */ |
375 | private function getTargetsBySite( MassMessageListContent $content ) { |
376 | $targets = $content->getTargets(); |
377 | $results = []; |
378 | foreach ( $targets as $target ) { |
379 | if ( array_key_exists( 'site', $target ) ) { |
380 | $results[$target['site']][] = $target['title']; |
381 | } else { |
382 | $results['local'][] = $target['title']; |
383 | } |
384 | } |
385 | return $results; |
386 | } |
387 | |
388 | /** |
389 | * Helper function for fillParserOutput; return HTML for page-adding form and |
390 | * (initially empty and hidden) list of added pages. |
391 | * |
392 | * @param Language $lang |
393 | * @return string |
394 | */ |
395 | private static function getAddForm( Language $lang ) { |
396 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
397 | |
398 | $html = Html::openElement( 'div', [ 'id' => 'mw-massmessage-addpages' ] ); |
399 | $html .= Html::element( 'h2', [], |
400 | wfMessage( 'massmessage-content-addheading' )->inLanguage( $lang )->text() ); |
401 | |
402 | $titleWidget = new TitleInputWidget( [] ); |
403 | $titleLabel = wfMessage( 'massmessage-content-addtitle' )->inLanguage( $lang )->text(); |
404 | $submitWidget = new ButtonInputWidget( [ |
405 | 'type' => 'submit', |
406 | 'label' => wfMessage( 'massmessage-content-addsubmit' )->inLanguage( $lang )->text(), |
407 | ] ); |
408 | $sites = DatabaseLookup::getDatabases(); |
409 | if ( $config->get( 'AllowGlobalMessaging' ) && count( $sites ) > 1 ) { |
410 | // Treat all 3 widgets as distinct items in the layout |
411 | $items = [ |
412 | new FieldLayout( |
413 | $titleWidget, |
414 | [ |
415 | 'id' => 'mw-massmessage-addtitle', |
416 | 'label' => $titleLabel, |
417 | 'align' => 'top', |
418 | ], |
419 | ), |
420 | new FieldLayout( |
421 | new ComboBoxInputWidget( [ |
422 | 'name' => 'site', |
423 | 'placeholder' => UrlHelper::getBaseUrl( $config->get( MainConfigNames::CanonicalServer ) ), |
424 | 'autocomplete' => true, |
425 | 'options' => array_map( |
426 | static function ( $domain ) { |
427 | return [ 'data' => $domain, 'label' => $domain ]; |
428 | }, |
429 | array_keys( $sites ) |
430 | ), |
431 | ] ), |
432 | [ |
433 | 'id' => 'mw-massmessage-addsite', |
434 | 'label' => wfMessage( 'massmessage-content-addsite' )->inLanguage( $lang )->text(), |
435 | 'align' => 'top', |
436 | ] |
437 | ), |
438 | new FieldLayout( $submitWidget ) |
439 | ]; |
440 | } else { |
441 | // Use a joined layout |
442 | $items = [ |
443 | new ActionFieldLayout( |
444 | $titleWidget, |
445 | $submitWidget, |
446 | [ |
447 | 'id' => 'mw-massmessage-addtitle', |
448 | 'label' => $titleLabel, |
449 | 'align' => 'top', |
450 | ] |
451 | ) |
452 | ]; |
453 | } |
454 | $html .= new FormLayout( [ |
455 | 'id' => 'mw-massmessage-addform', |
456 | 'items' => $items, |
457 | 'infusable' => true, |
458 | ] ); |
459 | |
460 | // List of added pages |
461 | $html .= Html::rawElement( |
462 | 'div', |
463 | [ 'id' => 'mw-massmessage-addedlist' ], |
464 | Html::element( 'p', [], wfMessage( 'massmessage-content-addedlistheading' )->inLanguage( $lang )->text() ) . |
465 | Html::element( 'ul', [], '' ) |
466 | ); |
467 | |
468 | $html .= Html::closeElement( 'div' ); |
469 | return $html; |
470 | } |
471 | } |