Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
10.29% covered (danger)
10.29%
7 / 68
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CNChoiceDataResourceLoaderModule
10.29% covered (danger)
10.29%
7 / 68
0.00% covered (danger)
0.00%
0 / 5
200.80
0.00% covered (danger)
0.00%
0 / 1
 getChoices
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
3.65
 getFromApi
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
 getScript
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getDependencies
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 getDefinitionSummary
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\Config\ConfigException;
4use MediaWiki\Html\Html;
5use MediaWiki\Json\FormatJson;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\ResourceLoader as RL;
8
9/**
10 * ResourceLoader module for sending banner choices to the client.
11 *
12 * Note: This class has been intentionally left stateless, due to how
13 * ResourceLoader works. This class has no expectation of having getScript() or
14 * getModifiedHash() called in the same request.
15 */
16class CNChoiceDataResourceLoaderModule extends RL\Module {
17
18    private const API_REQUEST_TIMEOUT = 20;
19
20    /**
21     * @param RL\Context $context
22     * @return array
23     */
24    protected function getChoices( RL\Context $context ) {
25        $config = $this->getConfig();
26        $project = $config->get( 'NoticeProject' );
27        $language = $context->getLanguage();
28
29        // Only fetch the data via the API if $wgCentralNoticeApiUrl is set.
30        // Otherwise, use the DB.
31        $apiUrl = $config->get( 'CentralNoticeApiUrl' );
32        if ( $apiUrl ) {
33            $choices = $this->getFromApi( $project, $language );
34
35            if ( !$choices ) {
36                wfLogWarning( 'Couldn\'t fetch banner choice data via API. ' .
37                    'wgCentralNoticeApiUrl = ' . $apiUrl );
38
39                return [];
40            }
41        } else {
42            $choices = ChoiceDataProvider::getChoices( $project, $language );
43        }
44
45        return $choices;
46    }
47
48    /**
49     * Get the banner choices data via an API call to the infrastructure wiki.
50     * If the call fails, we return false.
51     *
52     * @param string $project
53     * @param string $language
54     *
55     * @return array|bool
56     */
57    private function getFromApi( $project, $language ) {
58        $cnApiUrl = $this->getConfig()->get( 'CentralNoticeApiUrl' );
59
60        // Make the URL
61        $q = [
62            'action' => 'centralnoticechoicedata',
63            'project' => $project,
64            'language' => $language,
65            'format' => 'json',
66            'formatversion' => 2 // Prevents stripping of false values 8p
67        ];
68
69        $url = wfAppendQuery( $cnApiUrl, $q );
70
71        $apiResult = MediaWikiServices::getInstance()->getHttpRequestFactory()->get(
72            $url,
73            [ 'timeout' => self::API_REQUEST_TIMEOUT * 0.8 ],
74            __METHOD__
75        );
76
77        if ( !$apiResult ) {
78            wfLogWarning( 'Couldn\'t get banner choice data via API.' );
79            return false;
80        }
81
82        $parsedApiResult = FormatJson::parse( $apiResult, FormatJson::FORCE_ASSOC );
83
84        if ( !$parsedApiResult->isGood() ) {
85            wfLogWarning( 'Couldn\'t parse banner choice data from API.' );
86            return false;
87        }
88
89        $result = $parsedApiResult->getValue();
90
91        if ( isset( $result['error'] ) ) {
92            wfLogWarning( 'Error fetching banner choice data via API: ' .
93                $result['error']['info'] . ': ' . $result['error']['code'] );
94
95            return false;
96        }
97
98        return $result['choices'];
99    }
100
101    /**
102     * @inheritDoc
103     */
104    public function getScript( RL\Context $context ) {
105        $choices = $this->getChoices( $context );
106        if ( !$choices ) {
107            // If there are no choices, this module will have no dependencies,
108            // but other modules that create mw.centralNotice may be brought
109            // in elsewhere. Let's the check for its existence here, too, for
110            // robustness.
111            return 'mw.centralNotice = ( mw.centralNotice || {} );' .
112                'mw.centralNotice.choiceData = [];';
113        } else {
114
115            // If there are choices, this module should depend on (at least)
116            // ext.centralNotice.display, which will create mw.centralNotice.
117            // However, RL may experience errors that cause these dynamic
118            // dependencies to not be set as expected; so we check, just in case.
119            // In such an error state, ext.centralNotice.startUp.js logs to the
120            // console.
121            return 'mw.centralNotice = ( mw.centralNotice || {} );' .
122                'mw.centralNotice.choiceData = ' .
123                Html::encodeJsVar( $choices ) . ';';
124        }
125    }
126
127    /**
128     * @inheritDoc
129     */
130    public function getDependencies( ?RL\Context $context = null ) {
131        $cnCampaignMixins = $this->getConfig()->get( 'CentralNoticeCampaignMixins' );
132
133        // If this method is called with no context argument (the old method
134        // signature) emit a warning, but don't stop the show.
135        if ( !$context ) {
136            wfLogWarning( '$context is required for campaign mixins.' );
137            return [];
138        }
139
140        // Get the choices (possible campaigns and banners) for this user
141        $choices = $this->getChoices( $context );
142        if ( !$choices ) {
143            // If there are no choices, no dependencies
144            return [];
145        }
146
147        // Run through the choices to get all needed mixin RL modules
148        $dependencies = [];
149        foreach ( $choices as $choice ) {
150            foreach ( $choice['mixins'] as $mixinName => $mixinParams ) {
151                if ( !$cnCampaignMixins[$mixinName]['subscribingModule'] ) {
152                    throw new ConfigException(
153                        "No subscribing module for found campaign mixin {$mixinName}" );
154                }
155
156                $dependencies[] =
157                    $cnCampaignMixins[$mixinName]['subscribingModule'];
158            }
159        }
160
161        // The display module is needed to process choices
162        $dependencies[] = 'ext.centralNotice.display';
163
164        // Since campaigns targeting the user could have the same mixin RL
165        // modules, remove any duplicates.
166        return array_unique( $dependencies );
167    }
168
169    /**
170     * @inheritDoc
171     */
172    public function getDefinitionSummary( RL\Context $context ) {
173        $summary = parent::getDefinitionSummary( $context );
174        $summary[] = [
175            'choices' => $this->getChoices( $context ),
176        ];
177        return $summary;
178    }
179}