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