Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 685
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialCentralNoticeBanners
0.00% covered (danger)
0.00%
0 / 685
0.00% covered (danger)
0.00%
0 / 16
9506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 ensureBanner
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 showBannerList
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 generateBannerListForm
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 1
12
 processBannerList
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
380
 setFilterFromUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getFilterUrlParamAsArray
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getBannerPreviewEditLinks
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 showBannerEditor
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
20
 generateBannerEditForm
0.00% covered (danger)
0.00%
0 / 270
0.00% covered (danger)
0.00%
0 / 1
420
 generateCdnPurgeSection
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 processEditBanner
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
210
 processSaveBannerAction
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
56
 getTemplateBannerDropdownItems
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3use MediaWiki\Html\Html;
4use MediaWiki\MediaWikiServices;
5
6/**
7 * Special page for management of CentralNotice banners
8 */
9class 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}