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            // Prevents stripping of false values 8p
67            'formatversion' => 2
68        ];
69
70        $url = wfAppendQuery( $cnApiUrl, $q );
71
72        $apiResult = MediaWikiServices::getInstance()->getHttpRequestFactory()->get(
73            $url,
74            [ 'timeout' => self::API_REQUEST_TIMEOUT * 0.8 ],
75            __METHOD__
76        );
77
78        if ( !$apiResult ) {
79            wfLogWarning( 'Couldn\'t get banner choice data via API.' );
80            return false;
81        }
82
83        $parsedApiResult = FormatJson::parse( $apiResult, FormatJson::FORCE_ASSOC );
84
85        if ( !$parsedApiResult->isGood() ) {
86            wfLogWarning( 'Couldn\'t parse banner choice data from API.' );
87            return false;
88        }
89
90        $result = $parsedApiResult->getValue();
91
92        if ( isset( $result['error'] ) ) {
93            wfLogWarning( 'Error fetching banner choice data via API: ' .
94                $result['error']['info'] . ': ' . $result['error']['code'] );
95
96            return false;
97        }
98
99        return $result['choices'];
100    }
101
102    /**
103     * @inheritDoc
104     */
105    public function getScript( RL\Context $context ) {
106        $choices = $this->getChoices( $context );
107        if ( !$choices ) {
108            // If there are no choices, this module will have no dependencies,
109            // but other modules that create mw.centralNotice may be brought
110            // in elsewhere. Let's the check for its existence here, too, for
111            // robustness.
112            return 'mw.centralNotice = ( mw.centralNotice || {} );' .
113                'mw.centralNotice.choiceData = [];';
114        } else {
115
116            // If there are choices, this module should depend on (at least)
117            // ext.centralNotice.display, which will create mw.centralNotice.
118            // However, RL may experience errors that cause these dynamic
119            // dependencies to not be set as expected; so we check, just in case.
120            // In such an error state, ext.centralNotice.startUp.js logs to the
121            // console.
122            return 'mw.centralNotice = ( mw.centralNotice || {} );' .
123                'mw.centralNotice.choiceData = ' .
124                Html::encodeJsVar( $choices ) . ';';
125        }
126    }
127
128    /**
129     * @inheritDoc
130     */
131    public function getDependencies( ?RL\Context $context = null ) {
132        $cnCampaignMixins = $this->getConfig()->get( 'CentralNoticeCampaignMixins' );
133
134        // If this method is called with no context argument (the old method
135        // signature) emit a warning, but don't stop the show.
136        if ( !$context ) {
137            wfLogWarning( '$context is required for campaign mixins.' );
138            return [];
139        }
140
141        // Get the choices (possible campaigns and banners) for this user
142        $choices = $this->getChoices( $context );
143        if ( !$choices ) {
144            // If there are no choices, no dependencies
145            return [];
146        }
147
148        // Run through the choices to get all needed mixin RL modules
149        $dependencies = [];
150        foreach ( $choices as $choice ) {
151            foreach ( $choice['mixins'] as $mixinName => $mixinParams ) {
152                if ( !$cnCampaignMixins[$mixinName]['subscribingModule'] ) {
153                    throw new ConfigException(
154                        "No subscribing module for found campaign mixin {$mixinName}" );
155                }
156
157                $dependencies[] =
158                    $cnCampaignMixins[$mixinName]['subscribingModule'];
159            }
160        }
161
162        // The display module is needed to process choices
163        $dependencies[] = 'ext.centralNotice.display';
164
165        // Since campaigns targeting the user could have the same mixin RL
166        // modules, remove any duplicates.
167        return array_unique( $dependencies );
168    }
169
170    /**
171     * @inheritDoc
172     */
173    public function getDefinitionSummary( RL\Context $context ) {
174        $summary = parent::getDefinitionSummary( $context );
175        $summary[] = [
176            'choices' => $this->getChoices( $context ),
177        ];
178        return $summary;
179    }
180}