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