Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 685 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
SpecialCentralNoticeBanners | |
0.00% |
0 / 685 |
|
0.00% |
0 / 16 |
9506 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
56 | |||
ensureBanner | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
showBannerList | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
generateBannerListForm | |
0.00% |
0 / 89 |
|
0.00% |
0 / 1 |
12 | |||
processBannerList | |
0.00% |
0 / 64 |
|
0.00% |
0 / 1 |
380 | |||
setFilterFromUrl | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getFilterUrlParamAsArray | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getBannerPreviewEditLinks | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
20 | |||
showBannerEditor | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
20 | |||
generateBannerEditForm | |
0.00% |
0 / 270 |
|
0.00% |
0 / 1 |
420 | |||
generateCdnPurgeSection | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
12 | |||
processEditBanner | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
210 | |||
processSaveBannerAction | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
56 | |||
getTemplateBannerDropdownItems | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | use MediaWiki\Html\Html; |
4 | use MediaWiki\MediaWikiServices; |
5 | |
6 | /** |
7 | * Special page for management of CentralNotice banners |
8 | */ |
9 | class SpecialCentralNoticeBanners extends CentralNotice { |
10 | /** @var string Name of the banner we're currently editing */ |
11 | private $bannerName = ''; |
12 | |
13 | /** @var Banner|null Banner object we're currently editing */ |
14 | private $banner = null; |
15 | |
16 | /** @var string Filter to apply to the banner search when generating the list */ |
17 | private $bannerFilterString = ''; |
18 | |
19 | /** @var string Language code to render preview materials in */ |
20 | private $bannerLanguagePreview; |
21 | |
22 | /** @var bool If true, form execution must stop and the page will be redirected */ |
23 | private $bannerFormRedirectRequired = false; |
24 | |
25 | /** @var array|null Names of the banners that are marked as templates */ |
26 | private $templateBannerNames = null; |
27 | |
28 | public function __construct() { |
29 | SpecialPage::__construct( 'CentralNoticeBanners' ); |
30 | } |
31 | |
32 | public function doesWrites() { |
33 | return true; |
34 | } |
35 | |
36 | /** |
37 | * Handle all the different types of page requests determined by the first subpage |
38 | * level after the special page title. If needed, the second subpage level is the |
39 | * banner name. |
40 | * |
41 | * Valid actions are: |
42 | * (none) - Display a list of banners |
43 | * edit - Edits an existing banner |
44 | * |
45 | * TODO: Use the "?action=" convention rather than parsing the URL subpath. |
46 | * |
47 | * @param string|null $subPage |
48 | */ |
49 | public function execute( $subPage ) { |
50 | // Do all the common setup |
51 | $this->setHeaders(); |
52 | $this->editable = $this->getUser()->isAllowed( 'centralnotice-admin' ); |
53 | |
54 | // Make sure we have a session |
55 | $this->getRequest()->getSession()->persist(); |
56 | |
57 | // Load things that may have been serialized into the session |
58 | $this->bannerLanguagePreview = $this->getCNSessionVar( |
59 | 'bannerLanguagePreview', |
60 | $this->getLanguage()->getCode() |
61 | ); |
62 | |
63 | // User settable text for some custom message, like usage instructions |
64 | $this->getOutput()->setPageTitle( $this->msg( 'noticetemplate' ) ); |
65 | |
66 | // Allow users to add a custom nav bar (T138284) |
67 | $navBar = $this->msg( 'centralnotice-navbar' )->inContentLanguage(); |
68 | if ( !$navBar->isDisabled() ) { |
69 | $this->getOutput()->addHTML( $navBar->parseAsBlock() ); |
70 | } |
71 | $this->getOutput()->addWikiMsg( 'centralnotice-summary' ); |
72 | |
73 | // Now figure out what to display |
74 | // TODO Use only params instead of subpage to indicate action |
75 | $parts = explode( '/', $subPage ); |
76 | $action = $parts[0] ?: 'list'; |
77 | $this->bannerName = $parts[1] ?? ''; |
78 | |
79 | switch ( strtolower( $action ) ) { |
80 | case 'list': |
81 | // Display the list of banners |
82 | $this->showBannerList(); |
83 | break; |
84 | |
85 | case 'edit': |
86 | if ( $this->bannerName ) { |
87 | $this->ensureBanner( $this->bannerName ); |
88 | $this->showBannerEditor(); |
89 | } else { |
90 | throw new ErrorPageError( 'noticetemplate', 'centralnotice-banner-name-error' ); |
91 | } |
92 | break; |
93 | |
94 | default: |
95 | // Something went wrong; display error page |
96 | throw new ErrorPageError( 'noticetemplate', 'centralnotice-generic-error' ); |
97 | } |
98 | } |
99 | |
100 | /** |
101 | * Ensure that $this->banner is assigned. |
102 | * |
103 | * @param string $bannerName |
104 | * @throws ErrorPageError |
105 | */ |
106 | private function ensureBanner( $bannerName ) { |
107 | if ( !Banner::isValidBannerName( $bannerName ) ) { |
108 | throw new ErrorPageError( 'noticetemplate', 'centralnotice-generic-error' ); |
109 | } |
110 | |
111 | if ( $this->banner ) { |
112 | return; |
113 | } |
114 | |
115 | $this->banner = Banner::fromName( $this->bannerName ); |
116 | |
117 | if ( !$this->banner->exists() ) { |
118 | throw new ErrorPageError( 'centralnotice-banner-not-found-title', |
119 | 'centralnotice-banner-not-found-contents' ); |
120 | } |
121 | } |
122 | |
123 | /** |
124 | * Process the 'banner list' form and display a new one. |
125 | */ |
126 | private function showBannerList() { |
127 | $out = $this->getOutput(); |
128 | $out->setPageTitle( $this->msg( 'centralnotice-manage-templates' ) ); |
129 | $out->addModules( 'ext.centralNotice.adminUi.bannerManager' ); |
130 | |
131 | // Process the form that we sent out |
132 | $formDescriptor = $this->generateBannerListForm( $this->bannerFilterString ); |
133 | $htmlForm = new CentralNoticeHtmlForm( $formDescriptor, $this->getContext() ); |
134 | $htmlForm->setSubmitCallback( [ $this, 'processBannerList' ] ) |
135 | ->prepareForm(); |
136 | $formResult = $htmlForm->trySubmit(); |
137 | |
138 | if ( $this->bannerFormRedirectRequired ) { |
139 | return; |
140 | } |
141 | |
142 | // Re-generate the form in case they changed the filter string, archived something, |
143 | // deleted something, etc... |
144 | $formDescriptor = $this->generateBannerListForm( $this->bannerFilterString ); |
145 | $htmlForm = new CentralNoticeHtmlForm( $formDescriptor, $this->getContext() ); |
146 | |
147 | $htmlForm->setId( 'cn-banner-manager' ) |
148 | ->suppressDefaultSubmit() |
149 | ->setDisplayFormat( 'div' ) |
150 | ->prepareForm() |
151 | ->displayForm( $formResult ); |
152 | } |
153 | |
154 | /** |
155 | * Generates the HTMLForm entities for the 'banner list' form. |
156 | * |
157 | * @param string $filter Filter to use for the banner list |
158 | * |
159 | * @return array of HTMLForm entities |
160 | */ |
161 | private function generateBannerListForm( $filter = '' ) { |
162 | // --- Create the banner search form --- // |
163 | |
164 | // Note: filter is normally set via JS, not form submission. But we |
165 | // leave the info in the submitted form, in any case. |
166 | $formDescriptor = [ |
167 | 'bannerNameFilter' => [ |
168 | 'section' => 'header/banner-search', |
169 | 'class' => HTMLTextField::class, |
170 | 'placeholder-message' => 'centralnotice-filter-template-prompt', |
171 | 'filter-callback' => [ $this, 'sanitizeSearchTerms' ], |
172 | 'default' => $filter, |
173 | ], |
174 | 'filterApply' => [ |
175 | 'section' => 'header/banner-search', |
176 | 'class' => HTMLButtonField::class, |
177 | 'default' => $this->msg( 'centralnotice-filter-template-submit' )->text(), |
178 | ] |
179 | ]; |
180 | |
181 | // --- Create the management options --- // |
182 | $formDescriptor += [ |
183 | 'selectAllBanners' => [ |
184 | 'section' => 'header/banner-bulk-manage', |
185 | 'class' => HTMLCheckField::class, |
186 | 'disabled' => !$this->editable, |
187 | ], |
188 | /* TODO: Actually enable this feature |
189 | 'archiveSelectedBanners' => array( |
190 | 'section' => 'header/banner-bulk-manage', |
191 | 'class' => HTMLButtonField::class, |
192 | 'default' => 'Archive', |
193 | 'disabled' => !$this->editable, |
194 | ), |
195 | */ |
196 | 'deleteSelectedBanners' => [ |
197 | 'section' => 'header/banner-bulk-manage', |
198 | 'class' => HTMLButtonField::class, |
199 | 'default' => $this->msg( 'centralnotice-remove' )->text(), |
200 | 'disabled' => !$this->editable, |
201 | ], |
202 | 'addNewBanner' => [ |
203 | 'section' => 'header/one-off', |
204 | 'class' => HTMLButtonField::class, |
205 | 'default' => $this->msg( 'centralnotice-add-template' )->text(), |
206 | 'disabled' => !$this->editable, |
207 | ], |
208 | 'newBannerName' => [ |
209 | 'section' => 'addBanner', |
210 | 'class' => HTMLTextField::class, |
211 | 'disabled' => !$this->editable, |
212 | 'label' => $this->msg( 'centralnotice-banner-name' )->text(), |
213 | ], |
214 | 'createFromTemplateCheckbox' => [ |
215 | 'section' => 'addBanner', |
216 | 'class' => HTMLCheckField::class, |
217 | 'label' => $this->msg( 'centralnotice-create-from-template-checkbox-label' )->text(), |
218 | 'disabled' => !$this->editable || !$this->getTemplateBannerDropdownItems(), |
219 | ], |
220 | 'newBannerTemplate' => [ |
221 | 'section' => 'addBanner', |
222 | 'class' => HTMLSelectLimitField::class, |
223 | 'cssclass' => 'banner-template-dropdown-hidden', |
224 | 'disabled' => !$this->editable || !$this->getTemplateBannerDropdownItems(), |
225 | 'options' => $this->getTemplateBannerDropdownItems() |
226 | ], |
227 | 'newBannerEditSummary' => [ |
228 | 'section' => 'addBanner', |
229 | 'class' => HTMLTextField::class, |
230 | 'label-message' => 'centralnotice-change-summary-label', |
231 | 'placeholder-message' => 'centralnotice-change-summary-action-prompt', |
232 | 'disabled' => !$this->editable, |
233 | 'filter-callback' => [ $this, 'truncateSummaryField' ] |
234 | ], |
235 | 'removeBannerEditSummary' => [ |
236 | 'section' => 'removeBanner', |
237 | 'class' => HTMLTextField::class, |
238 | 'label-message' => 'centralnotice-change-summary-label', |
239 | 'placeholder-message' => 'centralnotice-change-summary-action-prompt', |
240 | 'disabled' => !$this->editable, |
241 | 'filter-callback' => [ $this, 'truncateSummaryField' ] |
242 | ], |
243 | 'action' => [ |
244 | 'type' => 'hidden', |
245 | ] |
246 | ]; |
247 | |
248 | // --- Add all the banners via the fancy pager object --- |
249 | $pager = new CNBannerPager( |
250 | $this, |
251 | 'banner-list', |
252 | [ |
253 | 'applyTo' => [ |
254 | 'section' => 'banner-list', |
255 | 'class' => HTMLCheckField::class, |
256 | 'cssclass' => 'cn-bannerlist-check-applyto', |
257 | ] |
258 | ], |
259 | [], |
260 | $filter, |
261 | $this->editable |
262 | ); |
263 | $formDescriptor[ 'topPagerNav' ] = $pager->getNavigationBar(); |
264 | $formDescriptor += $pager->getBody(); |
265 | $formDescriptor[ 'bottomPagerNav' ] = $pager->getNavigationBar(); |
266 | |
267 | return $formDescriptor; |
268 | } |
269 | |
270 | /** |
271 | * Callback function from the showBannerList() form that actually processes the |
272 | * response data. |
273 | * |
274 | * @param array $formData |
275 | * |
276 | * @return null|string|array |
277 | */ |
278 | public function processBannerList( $formData ) { |
279 | $this->setFilterFromUrl(); |
280 | |
281 | if ( $formData[ 'action' ] && $this->editable ) { |
282 | switch ( strtolower( $formData[ 'action' ] ) ) { |
283 | case 'create': |
284 | // Attempt to create a new banner and redirect; we validate here because it's |
285 | // a hidden field and that doesn't work so well with the form |
286 | if ( !Banner::isValidBannerName( $formData[ 'newBannerName' ] ) ) { |
287 | return $this->msg( 'centralnotice-banner-name-error' )->parse(); |
288 | } else { |
289 | $this->bannerName = $formData[ 'newBannerName' ]; |
290 | } |
291 | |
292 | if ( Banner::fromName( $this->bannerName )->exists() ) { |
293 | return $this->msg( 'centralnotice-template-exists' )->parse(); |
294 | } else { |
295 | if ( !empty( $formData['newBannerTemplate'] ) ) { |
296 | try { |
297 | $bannerTemplate = Banner::fromName( $formData['newBannerTemplate'] ); |
298 | // This will do data load for the banner, confirming it actually exists in the DB |
299 | // without calling Banner::exists() |
300 | if ( !$bannerTemplate || !$bannerTemplate->isTemplate() ) { |
301 | throw new BannerDataException( |
302 | "Attempted to create a banner based on invalid template" |
303 | ); |
304 | } |
305 | } catch ( BannerDataException $exception ) { |
306 | wfDebugLog( 'CentralNotice', $exception->getMessage() ); |
307 | |
308 | // We do not want to show the actual exception to the user here, |
309 | // since the message does not actually refer to the template being created, |
310 | // but to the template it is being created from |
311 | return $this->msg( 'centralnotice-banner-template-error' )->plain(); |
312 | } |
313 | |
314 | $retval = Banner::addFromBannerTemplate( |
315 | $this->bannerName, |
316 | $this->getUser(), |
317 | $bannerTemplate, |
318 | $formData['newBannerEditSummary'] |
319 | ); |
320 | } else { |
321 | $retval = Banner::addBanner( |
322 | $this->bannerName, |
323 | "<!-- Empty banner -->", |
324 | $this->getUser(), |
325 | false, |
326 | false, |
327 | // Default values of a zillion parameters... |
328 | [], [], null, |
329 | $formData['newBannerEditSummary'] |
330 | ); |
331 | } |
332 | |
333 | if ( $retval ) { |
334 | // Something failed; display error to user |
335 | return $this->msg( $retval )->parse(); |
336 | } else { |
337 | $this->getOutput()->redirect( |
338 | SpecialPage::getTitleFor( |
339 | 'CentralNoticeBanners', "edit/{$this->bannerName}" |
340 | )->getFullURL() |
341 | ); |
342 | $this->bannerFormRedirectRequired = true; |
343 | } |
344 | } |
345 | break; |
346 | |
347 | case 'archive': |
348 | return 'Archiving not yet implemented!'; |
349 | |
350 | case 'remove': |
351 | $summary = $formData['removeBannerEditSummary']; |
352 | $failed = []; |
353 | foreach ( $formData as $element => $value ) { |
354 | $parts = explode( '-', $element, 2 ); |
355 | if ( ( $parts[0] === 'applyTo' ) && ( $value === true ) ) { |
356 | try { |
357 | |
358 | Banner::removeBanner( |
359 | $parts[1], $this->getUser(), $summary ); |
360 | |
361 | } catch ( Exception $ex ) { |
362 | $failed[] = $parts[1]; |
363 | } |
364 | } |
365 | } |
366 | if ( $failed ) { |
367 | return 'some banners were not deleted'; |
368 | |
369 | } else { |
370 | |
371 | // Go back to the special banners page, including the |
372 | // same filter that was already set in the URL. |
373 | $this->getOutput()->redirect( |
374 | SpecialPage::getTitleFor( 'CentralNoticeBanners' ) |
375 | ->getFullURL( $this->getFilterUrlParamAsArray() ) ); |
376 | |
377 | $this->bannerFormRedirectRequired = true; |
378 | } |
379 | break; |
380 | } |
381 | } elseif ( $formData[ 'action' ] ) { |
382 | // Oh noes! The l33t hakorz are here... |
383 | return $this->msg( 'centralnotice-generic-error' )->parse(); |
384 | } |
385 | |
386 | return null; |
387 | } |
388 | |
389 | /** |
390 | * Use a URL parameter to set the filter string for the banner list. |
391 | */ |
392 | private function setFilterFromUrl() { |
393 | // This is the normal param on visible URLs. |
394 | $filterParam = $this->getRequest()->getVal( 'filter', null ); |
395 | |
396 | // If the form was posted the filter parameter'll have a different name. |
397 | if ( $filterParam === null ) { |
398 | $filterParam = |
399 | $this->getRequest()->getVal( 'wpbannerNameFilter', null ); |
400 | } |
401 | |
402 | // Clean, clean... |
403 | if ( $filterParam !== null ) { |
404 | $this->bannerFilterString = $this->sanitizeSearchTerms( $filterParam ); |
405 | } |
406 | } |
407 | |
408 | /** |
409 | * Return an array for use in constructing a URL query part with or without |
410 | * a filter parameter, as required. |
411 | * |
412 | * @return array |
413 | */ |
414 | public function getFilterUrlParamAsArray() { |
415 | return $this->bannerFilterString ? |
416 | [ 'filter' => $this->bannerFilterString ] : []; |
417 | } |
418 | |
419 | /** |
420 | * Returns array of navigation links to banner preview URL and |
421 | * edit link to the banner's wikipage if the user is allowed. |
422 | * |
423 | * FIXME Some of this code is repeated in BannerRenderer, but probably |
424 | * should be elsewhere. |
425 | * |
426 | * @return array |
427 | */ |
428 | private function getBannerPreviewEditLinks() { |
429 | $linkRenderer = $this->getLinkRenderer(); |
430 | $links = [ |
431 | $linkRenderer->makeKnownLink( |
432 | SpecialPage::getTitleFor( 'Randompage' ), |
433 | $this->msg( 'centralnotice-live-preview' )->text(), |
434 | [ 'class' => 'cn-banner-list-element-label-text' ], |
435 | [ |
436 | 'banner' => $this->bannerName, |
437 | 'uselang' => $this->bannerLanguagePreview, |
438 | 'force' => '1', |
439 | ] |
440 | ) |
441 | ]; |
442 | |
443 | $bannerTitle = $this->banner->getTitle(); |
444 | // $bannerTitle can be null sometimes |
445 | if ( $bannerTitle && $this->getUser()->isAllowed( 'editinterface' ) ) { |
446 | $links[] = $linkRenderer->makeLink( |
447 | $bannerTitle, |
448 | $this->msg( 'centralnotice-banner-edit-onwiki' )->text(), |
449 | [ 'class' => 'cn-banner-list-element-label-text' ], |
450 | [ 'action' => 'edit' ] |
451 | ); |
452 | } |
453 | if ( $bannerTitle ) { |
454 | $links[] = $linkRenderer->makeLink( |
455 | $bannerTitle, |
456 | $this->msg( 'centralnotice-banner-history' )->text(), |
457 | [ 'class' => 'cn-banner-list-element-label-text' ], |
458 | [ 'action' => 'history' ] |
459 | ); |
460 | } |
461 | return $links; |
462 | } |
463 | |
464 | /** |
465 | * Display the banner editor and process edits |
466 | */ |
467 | private function showBannerEditor() { |
468 | global $wgUseCdn; |
469 | |
470 | $out = $this->getOutput(); |
471 | $out->addModules( 'ext.centralNotice.adminUi.bannerEditor' ); |
472 | $this->addHelpLink( |
473 | '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:CentralNotice', |
474 | true |
475 | ); |
476 | |
477 | $out->setPageTitle( $this->bannerName ); |
478 | $out->setSubtitle( |
479 | $this->getLanguage()->pipeList( $this->getBannerPreviewEditLinks() ) |
480 | ); |
481 | |
482 | // Generate the form |
483 | $formDescriptor = $this->generateBannerEditForm(); |
484 | |
485 | // Now begin form processing |
486 | $htmlForm = new CentralNoticeHtmlForm( |
487 | $formDescriptor, $this->getContext(), 'centralnotice' ); |
488 | $htmlForm->setSubmitCallback( [ $this, 'processEditBanner' ] ) |
489 | ->prepareForm(); |
490 | |
491 | $formResult = $htmlForm->tryAuthorizedSubmit(); |
492 | |
493 | if ( $this->bannerFormRedirectRequired ) { |
494 | return; |
495 | } |
496 | |
497 | // Recreate the form because something could have changed |
498 | $formDescriptor = $this->generateBannerEditForm(); |
499 | |
500 | $htmlForm = new CentralNoticeHtmlForm( |
501 | $formDescriptor, $this->getContext(), 'centralnotice' ); |
502 | $htmlForm->setSubmitCallback( [ $this, 'processEditBanner' ] ) |
503 | ->setId( 'cn-banner-editor' ); |
504 | |
505 | // Push the form back to the user |
506 | $htmlForm->suppressDefaultSubmit() |
507 | ->setId( 'cn-banner-editor' ) |
508 | ->setDisplayFormat( 'div' ) |
509 | ->prepareForm() |
510 | ->displayForm( $formResult ); |
511 | |
512 | // Send banner name into page for access from JS |
513 | $out->addHTML( Xml::element( 'span', |
514 | [ |
515 | 'id' => 'centralnotice-data-container', |
516 | 'data-banner-name' => $this->bannerName |
517 | ] |
518 | ) ); |
519 | |
520 | if ( !$this->banner->isTemplate() ) { |
521 | // Controls to purge banner loader URLs from CDN caches for a given language. |
522 | if ( $wgUseCdn ) { |
523 | $out->addHTML( $this->generateCdnPurgeSection() ); |
524 | } |
525 | |
526 | $out->addHTML( Xml::element( 'h2', |
527 | [ 'class' => 'cn-special-section' ], |
528 | $this->msg( 'centralnotice-campaigns-using-banner' )->text() ) ); |
529 | |
530 | $pager = new CNCampaignPager( $this, false, $this->banner->getId() ); |
531 | $out->addModules( 'ext.centralNotice.adminUi.campaignPager' ); |
532 | $out->addHTML( $pager->getBodyOutput()->getText() ); |
533 | $out->addHTML( $pager->getNavigationBar() ); |
534 | } |
535 | } |
536 | |
537 | private function generateBannerEditForm() { |
538 | global $wgCentralNoticeBannerMixins, $wgNoticeUseTranslateExtension, $wgLanguageCode; |
539 | |
540 | $languages = MediaWikiServices::getInstance()->getLanguageNameUtils() |
541 | ->getLanguageNames( $this->getLanguage()->getCode() ); |
542 | array_walk( |
543 | $languages, |
544 | static function ( &$val, $index ) { |
545 | $val = "$index - $val"; |
546 | } |
547 | ); |
548 | $languages = array_flip( $languages ); |
549 | |
550 | $bannerSettings = $this->banner->getBannerSettings( $this->bannerName, true ); |
551 | |
552 | $formDescriptor = []; |
553 | |
554 | /* --- Use banner as template --- */ |
555 | $campaignNames = $this->banner->getCampaignNames(); |
556 | $formDescriptor['banner-is-template'] = [ |
557 | 'section' => 'banner-template', |
558 | 'type' => 'check', |
559 | 'disabled' => !$this->editable || $campaignNames, |
560 | 'label-message' => 'centralnotice-banner-is-template', |
561 | 'default' => $this->banner->isTemplate() |
562 | ]; |
563 | |
564 | if ( $campaignNames ) { |
565 | $formDescriptor[ 'banner-assigned-to-campaign' ] = [ |
566 | 'section' => 'banner-template', |
567 | 'class' => HTMLInfoField::class, |
568 | 'label-message' => 'centralnotice-messages-banner-in-campaign', |
569 | 'default' => implode( ', ', $campaignNames ) |
570 | ]; |
571 | } |
572 | |
573 | /* --- Banner Settings --- */ |
574 | $formDescriptor['banner-class'] = [ |
575 | 'section' => 'settings', |
576 | 'type' => 'selectorother', |
577 | 'disabled' => !$this->editable, |
578 | 'label-message' => 'centralnotice-banner-class', |
579 | 'help-message' => 'centralnotice-banner-class-desc', |
580 | 'options' => Banner::getAllUsedCategories(), |
581 | 'size' => 30, |
582 | 'maxlength' => 255, |
583 | 'default' => $this->banner->getCategory(), |
584 | ]; |
585 | |
586 | $selected = []; |
587 | if ( $bannerSettings[ 'anon' ] === 1 ) { |
588 | $selected[] = 'anonymous'; |
589 | } |
590 | if ( $bannerSettings[ 'account' ] === 1 ) { |
591 | $selected[] = 'registered'; |
592 | } |
593 | $formDescriptor[ 'display-to' ] = [ |
594 | 'section' => 'settings', |
595 | 'type' => 'multiselect', |
596 | 'disabled' => !$this->editable, |
597 | 'label-message' => 'centralnotice-banner-display', |
598 | 'options' => [ |
599 | $this->msg( 'centralnotice-banner-logged-in' )->escaped() => 'registered', |
600 | $this->msg( 'centralnotice-banner-anonymous' )->escaped() => 'anonymous' |
601 | ], |
602 | 'default' => $selected, |
603 | 'cssclass' => 'separate-form-element', |
604 | ]; |
605 | |
606 | $assignedDevices = array_values( |
607 | CNDeviceTarget::getDevicesAssociatedWithBanner( $this->banner->getId() ) |
608 | ); |
609 | $availableDevices = []; |
610 | foreach ( CNDeviceTarget::getAvailableDevices() as $k => $value ) { |
611 | $header = htmlspecialchars( $value[ 'header' ] ); |
612 | $label = $this->getOutput()->parseInlineAsInterface( $value[ 'label' ] ); |
613 | $availableDevices[ "($header) $label" ] = $header; |
614 | } |
615 | $formDescriptor[ 'device-classes' ] = [ |
616 | 'section' => 'settings', |
617 | 'type' => 'multiselect', |
618 | 'disabled' => !$this->editable, |
619 | 'label-message' => 'centralnotice-banner-display-on', |
620 | 'options' => $availableDevices, |
621 | 'default' => $assignedDevices, |
622 | 'cssclass' => 'separate-form-element', |
623 | ]; |
624 | |
625 | // TODO Remove. See T225831. |
626 | $mixinNames = array_keys( $wgCentralNoticeBannerMixins ); |
627 | $availableMixins = array_combine( $mixinNames, $mixinNames ); |
628 | $selectedMixins = array_keys( $this->banner->getMixins() ); |
629 | $formDescriptor['mixins'] = [ |
630 | 'section' => 'settings', |
631 | 'type' => 'multiselect', |
632 | 'disabled' => !$this->editable, |
633 | 'label-message' => 'centralnotice-banner-mixins', |
634 | 'help-message' => 'centralnotice-banner-mixins-help', |
635 | 'cssclass' => 'separate-form-element', |
636 | 'options' => $availableMixins, |
637 | 'default' => $selectedMixins, |
638 | ]; |
639 | |
640 | /* --- Translatable Messages Section --- */ |
641 | $messages = $this->banner->getMessageFieldsFromCache(); |
642 | |
643 | $linkRenderer = $this->getLinkRenderer(); |
644 | if ( $messages ) { |
645 | // Only show this part of the form if messages exist |
646 | |
647 | $formDescriptor[ 'translate-language' ] = [ |
648 | 'section' => 'banner-messages', |
649 | 'class' => LanguageSelectHeaderElement::class, |
650 | 'label-message' => 'centralnotice-language', |
651 | 'options' => $languages, |
652 | 'default' => $this->bannerLanguagePreview, |
653 | 'cssclass' => 'separate-form-element', |
654 | ]; |
655 | |
656 | $messageReadOnly = false; |
657 | if ( $wgNoticeUseTranslateExtension && |
658 | ( $this->bannerLanguagePreview !== $wgLanguageCode ) |
659 | ) { |
660 | $messageReadOnly = true; |
661 | } |
662 | foreach ( $messages as $messageName => $count ) { |
663 | if ( $wgNoticeUseTranslateExtension ) { |
664 | // Create per message link to the translate extension |
665 | $title = SpecialPage::getTitleFor( 'Translate' ); |
666 | $label = Html::rawElement( 'td', [], |
667 | $linkRenderer->makeLink( |
668 | $title, |
669 | $messageName, |
670 | [], |
671 | [ |
672 | 'group' => BannerMessageGroup::getTranslateGroupName( |
673 | $this->banner->getName() |
674 | ), |
675 | 'task' => 'view' |
676 | ] |
677 | ) |
678 | ); |
679 | } else { |
680 | $label = htmlspecialchars( $messageName ); |
681 | } |
682 | |
683 | $formDescriptor[ "message-$messageName" ] = [ |
684 | 'section' => 'banner-messages', |
685 | 'class' => HTMLCentralNoticeBannerMessage::class, |
686 | 'label-raw' => $label, |
687 | 'banner' => $this->bannerName, |
688 | 'message' => $messageName, |
689 | 'language' => $this->bannerLanguagePreview, |
690 | 'cssclass' => 'separate-form-element', |
691 | ]; |
692 | |
693 | if ( !$this->editable || $messageReadOnly ) { |
694 | $formDescriptor[ "message-$messageName" ][ 'readonly' ] = true; |
695 | } |
696 | } |
697 | |
698 | if ( $wgNoticeUseTranslateExtension ) { |
699 | $formDescriptor[ 'priority-langs' ] = [ |
700 | 'section' => 'banner-messages', |
701 | 'class' => HTMLLargeMultiSelectField::class, |
702 | 'disabled' => !$this->editable, |
703 | 'label-message' => 'centralnotice-prioritylangs', |
704 | 'options' => $languages, |
705 | 'default' => $bannerSettings[ 'prioritylangs' ], |
706 | 'help-message' => 'centralnotice-prioritylangs-explain', |
707 | 'cssclass' => 'separate-form-element cn-multiselect', |
708 | ]; |
709 | } |
710 | |
711 | if ( $wgNoticeUseTranslateExtension && BannerMessageGroup::isUsingGroupReview() ) { |
712 | $readyStateLangs = BannerMessageGroup::getLanguagesInState( |
713 | $this->bannerName, |
714 | 'ready' |
715 | ); |
716 | |
717 | if ( $readyStateLangs ) { |
718 | $formDescriptor[ 'pending-languages' ] = [ |
719 | 'section' => 'banner-messages', |
720 | 'class' => HTMLInfoField::class, |
721 | 'disabled' => !$this->editable, |
722 | 'label-message' => 'centralnotice-messages-pending-approval', |
723 | 'default' => implode( ', ', $readyStateLangs ), |
724 | 'cssclass' => 'separate-form-element', |
725 | ]; |
726 | } |
727 | } |
728 | } |
729 | |
730 | /* -- The banner editor -- */ |
731 | $formDescriptor[ 'banner-magic-words' ] = [ |
732 | 'section' => 'edit-template', |
733 | 'class' => HTMLInfoField::class, |
734 | 'default' => Html::rawElement( |
735 | 'div', |
736 | [ 'class' => 'separate-form-element' ], |
737 | $this->msg( 'centralnotice-edit-template-summary' )->parse() ), |
738 | 'rawrow' => true, |
739 | ]; |
740 | |
741 | $renderer = new BannerRenderer( $this->getContext(), $this->banner ); |
742 | $magicWords = $renderer->getMagicWords(); |
743 | foreach ( $magicWords as &$word ) { |
744 | $word = wfEscapeWikiText( '{{{' . $word . '}}}' ); |
745 | } |
746 | $formDescriptor[ 'banner-mixin-words' ] = [ |
747 | 'section' => 'edit-template', |
748 | 'type' => 'info', |
749 | 'default' => $this->msg( |
750 | 'centralnotice-edit-template-magicwords', |
751 | $this->getLanguage()->listToText( $magicWords ) |
752 | )->parse(), |
753 | 'rawrow' => true, |
754 | ]; |
755 | |
756 | $buttons = []; |
757 | // TODO: Fix this gawdawful method of inserting the close button |
758 | $buttons[] = |
759 | '<a href="#" onclick="mw.centralNotice.adminUi.bannerEditor.insertButton(\'close\');' . |
760 | 'return false;">' . $this->msg( 'centralnotice-close-button' )->escaped() . '</a>'; |
761 | $formDescriptor[ 'banner-insert-button' ] = [ |
762 | 'section' => 'edit-template', |
763 | 'class' => HTMLInfoField::class, |
764 | 'rawrow' => true, |
765 | 'default' => Html::rawElement( |
766 | 'div', |
767 | [ 'class' => 'banner-editing-top-hint separate-form-element' ], |
768 | $this->msg( 'centralnotice-insert' )-> |
769 | rawParams( $this->getLanguage()->commaList( $buttons ) )-> |
770 | escaped() ), |
771 | ]; |
772 | |
773 | $formDescriptor[ 'banner-body' ] = [ |
774 | 'section' => 'edit-template', |
775 | 'type' => 'textarea', |
776 | 'readonly' => !$this->editable, |
777 | 'hidelabel' => true, |
778 | 'placeholder' => '<!-- blank banner -->', |
779 | 'default' => $this->banner->getBodyContent(), |
780 | 'cssclass' => 'separate-form-element' |
781 | ]; |
782 | |
783 | $links = []; |
784 | foreach ( $this->banner->getIncludedTemplates() as $titleObj ) { |
785 | $links[] = $linkRenderer->makeLink( $titleObj ); |
786 | } |
787 | if ( $links ) { |
788 | $formDescriptor[ 'links' ] = [ |
789 | 'section' => 'edit-template', |
790 | 'type' => 'info', |
791 | 'label-message' => 'centralnotice-templates-included', |
792 | 'default' => implode( '<br />', $links ), |
793 | 'raw' => true |
794 | ]; |
795 | } |
796 | |
797 | /* --- Form bottom options --- */ |
798 | $formDescriptor[ 'summary' ] = [ |
799 | 'section' => 'form-actions', |
800 | 'class' => HTMLTextField::class, |
801 | 'label-message' => 'centralnotice-change-summary-label', |
802 | 'placeholder' => $this->msg( 'centralnotice-change-summary-prompt' ), |
803 | 'disabled' => !$this->editable, |
804 | 'filter-callback' => [ $this, 'truncateSummaryField' ] |
805 | ]; |
806 | |
807 | $formDescriptor[ 'save-button' ] = [ |
808 | 'section' => 'form-actions', |
809 | 'class' => HTMLSubmitField::class, |
810 | 'default' => $this->msg( 'centralnotice-save-banner' )->text(), |
811 | 'disabled' => !$this->editable, |
812 | 'cssclass' => 'cn-formbutton', |
813 | 'hidelabel' => true, |
814 | ]; |
815 | |
816 | $formDescriptor[ 'clone-button' ] = [ |
817 | 'section' => 'form-actions', |
818 | 'class' => HTMLButtonField::class, |
819 | 'default' => $this->msg( 'centralnotice-clone' )->text(), |
820 | 'disabled' => !$this->editable, |
821 | 'cssclass' => 'cn-formbutton', |
822 | 'hidelabel' => true, |
823 | ]; |
824 | |
825 | /* TODO: Add this back in when we can actually support it |
826 | $formDescriptor[ 'archive-button' ] = array( |
827 | 'section' => 'form-actions', |
828 | 'class' => HTMLButtonField::class, |
829 | 'default' => $this->msg( 'centralnotice-archive-banner' )->text(), |
830 | 'disabled' => !$this->editable, |
831 | 'cssclass' => 'cn-formbutton', |
832 | 'hidelabel' => true, |
833 | ); |
834 | */ |
835 | |
836 | $formDescriptor[ 'delete-button' ] = [ |
837 | 'section' => 'form-actions', |
838 | 'class' => HTMLButtonField::class, |
839 | 'default' => $this->msg( 'centralnotice-delete-banner' )->text(), |
840 | 'disabled' => !$this->editable, |
841 | 'cssclass' => 'cn-formbutton', |
842 | 'hidelabel' => true, |
843 | ]; |
844 | |
845 | /* --- Hidden fields and such --- */ |
846 | $formDescriptor[ 'cloneName' ] = [ |
847 | 'section' => 'clone-banner', |
848 | 'type' => 'text', |
849 | 'disabled' => !$this->editable, |
850 | 'label-message' => 'centralnotice-clone-name', |
851 | ]; |
852 | |
853 | $formDescriptor[ 'cloneEditSummary' ] = [ |
854 | 'section' => 'clone-banner', |
855 | 'class' => HTMLTextField::class, |
856 | 'label-message' => 'centralnotice-change-summary-label', |
857 | 'placeholder' => $this->msg( 'centralnotice-change-summary-action-prompt' ), |
858 | 'disabled' => !$this->editable, |
859 | 'filter-callback' => [ $this, 'truncateSummaryField' ] |
860 | ]; |
861 | |
862 | $formDescriptor[ 'deleteEditSummary' ] = [ |
863 | 'section' => 'delete-banner', |
864 | 'class' => HTMLTextField::class, |
865 | 'label-message' => 'centralnotice-change-summary-label', |
866 | 'placeholder-message' => 'centralnotice-change-summary-action-prompt', |
867 | 'disabled' => !$this->editable, |
868 | 'filter-callback' => [ $this, 'truncateSummaryField' ] |
869 | ]; |
870 | |
871 | $formDescriptor[ 'action' ] = [ |
872 | 'section' => 'form-actions', |
873 | 'type' => 'hidden', |
874 | // The default is to save for historical reasons. TODO: review. |
875 | 'default' => 'save', |
876 | ]; |
877 | |
878 | return $formDescriptor; |
879 | } |
880 | |
881 | /** |
882 | * Generate a string with the HTML for controls to request a front-end (CDN) cache |
883 | * purge of banner content for a language. |
884 | * |
885 | * @return string |
886 | */ |
887 | private function generateCdnPurgeSection() { |
888 | $purgeControls = Xml::element( 'h2', |
889 | [ 'class' => 'cn-special-section' ], |
890 | $this->msg( 'centralnotice-banner-cdn-controls' )->text() ); |
891 | |
892 | $purgeControls .= Html::openElement( 'fieldset', [ 'class' => 'prefsection' ] ); |
893 | |
894 | $purgeControls .= Html::openElement( 'label' ); |
895 | $purgeControls .= |
896 | $this->msg( 'centralnotice-banner-cdn-label' )->escaped() . ' '; |
897 | |
898 | $disabledAttr = $this->editable ? [] : [ 'disabled' => true ]; |
899 | |
900 | $purgeControls .= Html::openElement( 'select', |
901 | $disabledAttr + [ 'id' => 'cn-cdn-cache-language' ] ); |
902 | |
903 | // Retrieve the list of languages in user's language |
904 | // FIXME Similar code in SpecialBannerAllocation::execute(), maybe switch |
905 | // to language selector? |
906 | $languages = MediaWikiServices::getInstance()->getLanguageNameUtils() |
907 | ->getLanguageNames( $this->getLanguage()->getCode() ); |
908 | ksort( $languages ); |
909 | |
910 | foreach ( $languages as $code => $name ) { |
911 | $purgeControls .= Xml::option( |
912 | $this->msg( 'centralnotice-language-listing', $code, $name )->text(), |
913 | $code ); |
914 | } |
915 | |
916 | $purgeControls .= Html::closeElement( 'select' ); |
917 | $purgeControls .= Html::closeElement( 'label' ); |
918 | |
919 | $purgeControls .= ' ' . Html::element( 'button', |
920 | $disabledAttr + [ 'id' => 'cn-cdn-cache-purge' ], |
921 | $this->msg( 'centralnotice-banner-cdn-button' )->text() |
922 | ); |
923 | |
924 | $purgeControls .= Html::element( |
925 | 'div', |
926 | [ 'class' => 'htmlform-help' ], |
927 | $this->msg( 'centralnotice-banner-cdn-help' )->text() |
928 | ); |
929 | |
930 | $purgeControls .= Html::closeElement( 'fieldset' ); |
931 | |
932 | return $purgeControls; |
933 | } |
934 | |
935 | public function processEditBanner( $formData ) { |
936 | // First things first! Figure out what the heck we're actually doing! |
937 | switch ( $formData[ 'action' ] ) { |
938 | case 'update-lang': |
939 | $newLanguage = $formData[ 'translate-language' ]; |
940 | $this->setCNSessionVar( 'bannerLanguagePreview', $newLanguage ); |
941 | $this->bannerLanguagePreview = $newLanguage; |
942 | break; |
943 | |
944 | case 'delete': |
945 | if ( !$this->editable ) { |
946 | return null; |
947 | } |
948 | if ( !Banner::isValidBannerName( $this->bannerName ) ) { |
949 | throw new ErrorPageError( 'noticetemplate', 'centralnotice-banner-name-error' ); |
950 | } |
951 | try { |
952 | Banner::removeBanner( |
953 | $this->bannerName, $this->getUser(), |
954 | $formData[ 'deleteEditSummary' ] ); |
955 | |
956 | $this->getOutput()->redirect( $this->getPageTitle( '' )->getCanonicalURL() ); |
957 | $this->bannerFormRedirectRequired = true; |
958 | } catch ( Exception $ex ) { |
959 | return htmlspecialchars( $ex->getMessage() ) . " <br /> " . |
960 | $this->msg( 'centralnotice-template-still-bound', $this->bannerName )->parse(); |
961 | } |
962 | break; |
963 | |
964 | case 'archive': |
965 | if ( !$this->editable ) { |
966 | return null; |
967 | } |
968 | return 'Archiving currently does not work'; |
969 | |
970 | case 'clone': |
971 | if ( !$this->editable ) { |
972 | return null; |
973 | } |
974 | $newBannerName = $formData[ 'cloneName' ]; |
975 | if ( !Banner::isValidBannerName( $newBannerName ) ) { |
976 | throw new ErrorPageError( 'noticetemplate', 'centralnotice-banner-name-error' ); |
977 | } |
978 | |
979 | $this->ensureBanner( $this->bannerName ); |
980 | $this->banner->cloneBanner( |
981 | $newBannerName, $this->getUser(), |
982 | $formData[ 'cloneEditSummary' ] |
983 | ); |
984 | |
985 | $this->getOutput()->redirect( |
986 | $this->getPageTitle( "Edit/$newBannerName" )->getCanonicalURL() |
987 | ); |
988 | $this->bannerFormRedirectRequired = true; |
989 | break; |
990 | |
991 | case 'save': |
992 | if ( !$this->editable ) { |
993 | return null; |
994 | } |
995 | |
996 | $ret = $this->processSaveBannerAction( $formData ); |
997 | |
998 | // Clear the edit summary field in the request so the form |
999 | // doesn't re-display the same value. Note: this is a hack :( |
1000 | $this->getRequest()->setVal( 'wpsummary', '' ); |
1001 | |
1002 | return $ret; |
1003 | |
1004 | default: |
1005 | // Nothing was requested, so do nothing |
1006 | break; |
1007 | } |
1008 | } |
1009 | |
1010 | private function processSaveBannerAction( $formData ) { |
1011 | global $wgNoticeUseTranslateExtension, $wgLanguageCode; |
1012 | |
1013 | $this->ensureBanner( $this->bannerName ); |
1014 | $summary = $formData['summary']; |
1015 | |
1016 | /* --- Update the translations --- */ |
1017 | // But only if we aren't using translate or if the preview language is the content language |
1018 | if ( !$wgNoticeUseTranslateExtension || $this->bannerLanguagePreview === $wgLanguageCode ) { |
1019 | foreach ( $formData as $key => $value ) { |
1020 | if ( str_starts_with( $key, 'message-' ) ) { |
1021 | $messageName = substr( $key, strlen( 'message-' ) ); |
1022 | $bannerMessage = $this->banner->getMessageField( $messageName ); |
1023 | |
1024 | $bannerMessage->update( |
1025 | $value, $this->bannerLanguagePreview, $this->getUser(), |
1026 | $summary ); |
1027 | } |
1028 | } |
1029 | } |
1030 | |
1031 | /* --- Banner settings --- */ |
1032 | if ( array_key_exists( 'priority-langs', $formData ) ) { |
1033 | $prioLang = $formData[ 'priority-langs' ]; |
1034 | if ( !is_array( $prioLang ) ) { |
1035 | $prioLang = [ $prioLang ]; |
1036 | } |
1037 | } else { |
1038 | $prioLang = []; |
1039 | } |
1040 | |
1041 | $this->banner->setAllocation( |
1042 | in_array( 'anonymous', $formData[ 'display-to' ] ), |
1043 | in_array( 'registered', $formData[ 'display-to' ] ) |
1044 | ); |
1045 | $this->banner->setCategory( $formData[ 'banner-class' ] ); |
1046 | $this->banner->setDevices( $formData[ 'device-classes' ] ); |
1047 | $this->banner->setPriorityLanguages( $prioLang ); |
1048 | $this->banner->setBodyContent( $formData[ 'banner-body' ] ); |
1049 | |
1050 | $this->banner->setMixins( $formData['mixins'] ); |
1051 | $this->banner->setIsTemplate( (bool)$formData['banner-is-template'] ); |
1052 | |
1053 | $this->banner->save( $this->getUser(), $summary ); |
1054 | |
1055 | // Deferred update to purge CDN caches for banner content (for user's lang) |
1056 | DeferredUpdates::addUpdate( |
1057 | new CdnCacheUpdateBannerLoader( $this->getLanguage()->getCode(), $this->banner ), |
1058 | DeferredUpdates::POSTSEND |
1059 | ); |
1060 | |
1061 | return null; |
1062 | } |
1063 | |
1064 | /** |
1065 | * Returns all template banner names for dropdown |
1066 | * |
1067 | * @return array |
1068 | */ |
1069 | private function getTemplateBannerDropdownItems() { |
1070 | if ( $this->templateBannerNames === null ) { |
1071 | $dbr = CNDatabase::getDb(); |
1072 | $this->templateBannerNames = []; |
1073 | |
1074 | $res = $dbr->select( |
1075 | [ |
1076 | 'templates' => 'cn_templates' |
1077 | ], |
1078 | [ |
1079 | 'tmp_name' |
1080 | ], |
1081 | [ |
1082 | 'templates.tmp_is_template' => true, |
1083 | ], |
1084 | __METHOD__ |
1085 | ); |
1086 | |
1087 | foreach ( $res as $row ) { |
1088 | // name of the banner as a key for HTMLSelectField |
1089 | $this->templateBannerNames[$row->tmp_name] = $row->tmp_name; |
1090 | } |
1091 | } |
1092 | |
1093 | return $this->templateBannerNames; |
1094 | } |
1095 | } |