Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 212
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialBannerAllocation
0.00% covered (danger)
0.00%
0 / 212
0.00% covered (danger)
0.00%
0 / 5
600
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
 execute
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 1
156
 showList
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
42
 getTable
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 createRows
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Wikimedia Foundation
4 *
5 * LICENSE
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 */
18
19use MediaWiki\Html\Html;
20use MediaWiki\Language\LanguageNameUtils;
21use MediaWiki\SpecialPage\SpecialPage;
22
23/**
24 * SpecialBannerAllocation
25 *
26 * Special page for handling banner allocation.
27 */
28class SpecialBannerAllocation extends CentralNotice {
29    /**
30     * The project being used for banner allocation.
31     *
32     * @see $wgNoticeProjects
33     *
34     * @var string
35     */
36    public $project = 'wikipedia';
37
38    /**
39     * The language being used for banner allocation
40     *
41     * This should always be a lowercase language code.
42     *
43     * @var string
44     */
45    public $language = 'en';
46
47    /**
48     * The country being used for banner allocation.
49     *
50     * This should always be an uppercase country code or the empty string.
51     *
52     * @var string
53     */
54    public $locationCountry = 'US';
55
56    /**
57     * The region being used for banner allocation.
58     *
59     * This should always be an uppercase region code or the empty string.
60     *
61     * @var string
62     */
63    public $locationRegion = '';
64
65    public function __construct(
66        private readonly LanguageNameUtils $languageNameUtils,
67    ) {
68        // Register special page
69        parent::__construct( 'BannerAllocation' );
70    }
71
72    /**
73     * Handle different types of page requests
74     * @param string|null $sub
75     */
76    public function execute( $sub ) {
77        global $wgNoticeProjects, $wgLanguageCode, $wgNoticeProject;
78        $out = $this->getOutput();
79        $request = $this->getRequest();
80
81        $this->project = $request->getText( 'project', $wgNoticeProject );
82        $this->language = $request->getText( 'language', $wgLanguageCode );
83
84        // If the form has been submitted, the country code or region code should be passed along.
85        $locationCountrySubmitted = $request->getVal( 'country' );
86        $locationRegionSubmitted = $request->getVal( 'region' );
87        $this->locationCountry = $locationCountrySubmitted ?: $this->locationCountry;
88        $this->locationRegion = $locationRegionSubmitted ?: $this->locationRegion;
89
90        // Convert submitted location to boolean value. If it true, showList() will be called.
91        $locationSubmitted = ( $locationCountrySubmitted || $locationRegionSubmitted );
92
93        // Begin output
94        $this->setHeaders();
95
96        // Output ResourceLoader module for styling and javascript functions
97        $out->addModules( [
98            'ext.centralNotice.adminUi',
99        ] );
100
101        // Initialize error variable
102        $this->centralNoticeError = false;
103
104        // Allow users to add a custom nav bar (T138284)
105        $navBar = $this->msg( 'centralnotice-navbar' )->inContentLanguage();
106        if ( !$navBar->isDisabled() ) {
107            $out->addHTML( $navBar->parseAsBlock() );
108        }
109        // Show summary
110        $out->addWikiMsg( 'centralnotice-summary' );
111
112        // Begin Banners tab content
113        $out->addHTML( Html::openElement( 'div', [ 'id' => 'preferences' ] ) );
114
115        $htmlOut = '';
116
117        // Begin Allocation selection fieldset
118        $htmlOut .= Html::openElement( 'fieldset', [ 'class' => 'prefsection' ] );
119
120        $htmlOut .= Html::openElement( 'form', [ 'method' => 'get' ] );
121        $htmlOut .= Html::element( 'h2', [], $this->msg( 'centralnotice-view-allocation' )->text() );
122        $htmlOut .= $this->msg( 'centralnotice-allocation-instructions' )->parseAsBlock();
123
124        $htmlOut .= Html::openElement( 'table', [ 'id' => 'envpicker', 'cellpadding' => 7 ] );
125        $htmlOut .= Html::openElement( 'tr' );
126        $htmlOut .= Html::rawElement( 'td',
127            [ 'style' => 'width: 20%;' ],
128            $this->msg( 'centralnotice-project-name' )->parse() );
129        $htmlOut .= Html::openElement( 'td' );
130        $htmlOut .= Html::openElement( 'select', [ 'name' => 'project' ] );
131
132        foreach ( $wgNoticeProjects as $value ) {
133            $htmlOut .= Html::element(
134                'option',
135                [ 'value' => $value, 'selected' => $value === $this->project ],
136                $value
137            );
138        }
139
140        $htmlOut .= Html::closeElement( 'select' );
141        $htmlOut .= Html::closeElement( 'td' );
142        $htmlOut .= Html::closeElement( 'tr' );
143        $htmlOut .= Html::openElement( 'tr' );
144        $htmlOut .= Html::element( 'td',
145            [ 'valign' => 'top' ],
146            $this->msg( 'centralnotice-project-lang' )->text() );
147        $htmlOut .= Html::openElement( 'td' );
148
149        // Retrieve the list of languages in user's language
150        $languages = $this->languageNameUtils
151            ->getLanguageNames( $this->getLanguage()->getCode() );
152        // Make sure the site language is in the list; a custom language code
153        // might not have a defined name...
154        if ( !array_key_exists( $wgLanguageCode, $languages ) ) {
155            $languages[$wgLanguageCode] = $wgLanguageCode;
156        }
157
158        ksort( $languages );
159
160        $htmlOut .= Html::openElement( 'select', [ 'name' => 'language' ] );
161
162        foreach ( $languages as $code => $name ) {
163            $htmlOut .= Html::element(
164                'option',
165                [ 'value' => $code, 'selected' => $code === $this->language ],
166                $this->msg( 'centralnotice-language-listing', $code, $name )->text()
167            );
168        }
169
170        $htmlOut .= Html::closeElement( 'select' );
171        $htmlOut .= Html::closeElement( 'td' );
172        $htmlOut .= Html::closeElement( 'tr' );
173
174        // Country dropdown
175        $htmlOut .= Html::openElement( 'tr' );
176        $htmlOut .= Html::element( 'td', [], $this->msg( 'centralnotice-country' )->text() );
177        $htmlOut .= Html::openElement( 'td' );
178
179        $userLanguageCode = $this->getLanguage()->getCode();
180        $countries = GeoTarget::getCountriesList( $userLanguageCode );
181
182        $htmlOut .= Html::openElement(
183            'select', [ 'name' => 'country', 'id' => 'centralnotice-country' ]
184        );
185
186        foreach ( $countries as $code => $country ) {
187            $htmlOut .= Html::element(
188                'option',
189                [ 'value' => $code, 'selected' => $code === $this->locationCountry ],
190                $country->getName()
191            );
192        }
193
194        $htmlOut .= Html::closeElement( 'select' );
195        $htmlOut .= Html::closeElement( 'td' );
196        $htmlOut .= Html::closeElement( 'tr' );
197        // End Country dropdown
198
199        // Region dropdown
200        $htmlOut .= Html::openElement( 'tr' );
201        $htmlOut .= Html::element( 'td', [], $this->msg( 'centralnotice-region' )->text() );
202        $htmlOut .= Html::openElement( 'td' );
203        $htmlOut .= Html::openElement(
204            'select', [ 'name' => 'region', 'id' => 'centralnotice-region' ]
205        );
206
207        // set a client-side config variable with an associative array so we can
208        // dynamically populate this dropdown based on selected country.
209        $regionOptions = [];
210        foreach ( $countries as $countryCode => $country ) {
211            $regionOptions[$countryCode] = [];
212            foreach ( $country->getRegions() as $regionCode => $regionName ) {
213                $regionOptions[$countryCode][$regionCode] = $regionName;
214            }
215        }
216        $out->addJsConfigVars( [ 'CentralNoticeRegionOptions' => $regionOptions ] );
217
218        $htmlOut .= Html::closeElement( 'select' );
219        $htmlOut .= Html::closeElement( 'td' );
220        $htmlOut .= Html::closeElement( 'tr' );
221        // End Region dropdown
222
223        $htmlOut .= Html::closeElement( 'table' );
224
225        $htmlOut .= Html::rawElement( 'div',
226            [ 'class' => 'cn-buttons' ],
227            Html::submitButton( $this->msg( 'centralnotice-view' )->text() )
228        );
229        $htmlOut .= Html::closeElement( 'form' );
230
231        // End Allocation selection fieldset
232        $htmlOut .= Html::closeElement( 'fieldset' );
233
234        $out->addHTML( $htmlOut );
235
236        // Handle form submissions
237        if ( $locationSubmitted ) {
238            $this->showList();
239        }
240
241        // End Banners tab content
242        $out->addHTML( Html::closeElement( 'div' ) );
243    }
244
245    /**
246     * Show a list of banners with allocation. Newer banners are shown first.
247     */
248    public function showList() {
249        global $wgNoticeNumberOfBuckets;
250
251        // Obtain all banners & campaigns
252        $request = $this->getRequest();
253        $project = $request->getText( 'project' );
254        $country = $request->getText( 'country' );
255        $region = $request->getText( 'region' );
256        $language = $request->getText( 'language' );
257
258        // Begin building HTML
259        $htmlOut = '';
260
261        // Begin Allocation list fieldset
262        $htmlOut .= Html::openElement( 'fieldset', [ 'class' => 'prefsection' ] );
263
264        // Given our project and language combination, get banner choice data,
265        // then filter on country
266        $choiceData = ChoiceDataProvider::getChoices( $project, $language );
267
268        // Iterate through each possible device type and get allocation information
269        $devices = CNDeviceTarget::getAvailableDevices();
270        foreach ( $devices as $deviceId => $deviceData ) {
271            $htmlOut .= Html::openElement(
272                'div',
273                [
274                    'id' => "cn-allocation-{$project}-{$language}-{$country}-{$deviceId}",
275                    'class' => 'cn-allocation-group'
276                ]
277            );
278
279            $htmlOut .= Html::rawElement(
280                'h3', [],
281                $this->msg(
282                    'centralnotice-allocation-description',
283                    wfEscapeWikiText( $language ),
284                    wfEscapeWikiText( $project ),
285                    wfEscapeWikiText( $country ),
286                    // Messages keys via CNDeviceTarget and CNDatabasePatcher:
287                    // centralnotice-devicetype-desktop, centralnotice-devicetype-android,
288                    // centralnotice-devicetype-ipad, centralnotice-devicetype-iphone,
289                    // centralnotice-devicetype-unknown
290                    $deviceData['label']
291                )->parse()
292            );
293
294            // FIXME matrix is chosen dynamically based on more UI inputs
295            $matrix = [];
296            for ( $i = 0; $i < $wgNoticeNumberOfBuckets; $i++ ) {
297                $matrix[] = [ 'anonymous' => 'true', 'bucket' => $i ];
298            }
299            for ( $i = 0; $i < $wgNoticeNumberOfBuckets; $i++ ) {
300                $matrix[] = [ 'anonymous' => 'false', 'bucket' => $i ];
301            }
302
303            foreach ( $matrix as $target ) {
304                if ( $target['anonymous'] === 'true' ) {
305                    $label = $this->msg( 'centralnotice-banner-anonymous' )->text();
306                    $status = AllocationCalculator::ANONYMOUS;
307                } else {
308                    $label = $this->msg( 'centralnotice-banner-logged-in' )->text();
309                    $status = AllocationCalculator::LOGGED_IN;
310                }
311                $label .= ' -- ' . $this->msg( 'centralnotice-bucket-letter' )->
312                    rawParams( chr( $target['bucket'] + 65 ) )->text();
313
314                $possibleBannersAllCampaigns =
315                    AllocationCalculator::filterAndAllocate( $country,
316                    $region, $status, $deviceData['header'], $target['bucket'],
317                    $choiceData );
318
319                $htmlOut .= $this->getTable( $label, $possibleBannersAllCampaigns );
320            }
321
322            $htmlOut .= Html::closeElement( 'div' );
323        }
324
325        // End Allocation list fieldset
326        $htmlOut .= Html::closeElement( 'fieldset' );
327
328        $this->getOutput()->addHTML( $htmlOut );
329        $this->getOutput()->addModuleStyles( 'jquery.tablesorter.styles' );
330        $this->getOutput()->addModules( 'jquery.tablesorter' );
331    }
332
333    /**
334     * Generate the HTML for an allocation table
335     * @param string $type The title for the table
336     * @param array $banners The banners as allocated by AllocationCalculator
337     * @return string HTML for the table
338     */
339    public function getTable( $type, $banners ) {
340        $htmlOut = Html::openElement( 'table',
341            [ 'cellpadding' => 9, 'class' => 'wikitable sortable', 'style' => 'margin: 1em 0;' ]
342        );
343        $htmlOut .= Html::element( 'h4', [], $type );
344
345        if ( count( $banners ) > 0 ) {
346            $htmlOut .= Html::rawElement( 'tr', [],
347                Html::element( 'th', [ 'width' => '5%' ],
348                    $this->msg( 'centralnotice-percentage' )->text() ) .
349                Html::element( 'th', [ 'width' => '30%' ],
350                    $this->msg( 'centralnotice-banner' )->text() ) .
351                Html::element( 'th', [ 'width' => '30%' ],
352                    $this->msg( 'centralnotice-notice' )->text() )
353            );
354        }
355        $htmlOut .= $this->createRows( $banners );
356
357        $htmlOut .= Html::closeElement( 'table' );
358
359        return $htmlOut;
360    }
361
362    /**
363     * @param array[] $banners
364     * @return string HTML
365     */
366    public function createRows( $banners ) {
367        $viewCampaign = SpecialPage::getTitleFor( 'CentralNotice' );
368        $htmlOut = '';
369        if ( count( $banners ) > 0 ) {
370            $linkRenderer = $this->getLinkRenderer();
371            foreach ( $banners as $banner ) {
372                $percentage = sprintf( "%0.2f", round( $banner['allocation'] * 100, 2 ) );
373
374                // Row begin
375                $htmlOut .= Html::openElement( 'tr', [ 'class' => 'mw-sp-centralnotice-allocationrow' ] );
376
377                // Percentage
378                $htmlOut .= Html::element( 'td', [ 'align' => 'right' ],
379                    $this->msg( 'percent', $percentage )->text()
380                );
381
382                // Banner name
383                $viewBanner = SpecialPage::getTitleFor( 'CentralNoticeBanners', "edit/{$banner['name']}" );
384
385                $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top' ],
386                    Html::rawElement( 'span',
387                        [ 'class' => 'cn-' . $banner['campaign'] . '-' . $banner['name'] ],
388                        $linkRenderer->makeLink(
389                            $viewBanner,
390                            $banner['name'],
391                            [],
392                            [ 'template' => $banner['name'] ]
393                        )
394                    )
395                );
396
397                // Campaign name
398                $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top' ],
399                    $linkRenderer->makeLink(
400                        $viewCampaign,
401                        $banner['campaign'],
402                        [],
403                        [
404                            'subaction' => 'noticeDetail',
405                            'notice' => $banner['campaign']
406                        ]
407                    )
408                );
409
410                // Row end
411                $htmlOut .= Html::closeElement( 'tr' );
412            }
413
414        } else {
415            $htmlOut .= Html::rawElement( 'tr', [],
416                Html::rawElement( 'td', [],
417                    $this->msg( 'centralnotice-no-allocation' )->parseAsBlock()
418                )
419            );
420        }
421        return $htmlOut;
422    }
423}