Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
54.29% |
38 / 70 |
|
66.67% |
6 / 9 |
CRAP | |
0.00% |
0 / 1 |
TrackingToolRegistry | |
54.29% |
38 / 70 |
|
66.67% |
6 / 9 |
102.90 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getRegistry | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getConfiguredPEDashboardData | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
30 | |||
getRegistryForTesting | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
setRegistryForTesting | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
newFromDBID | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getDataForForm | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
newFromUserIdentifier | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
newFromRegistryEntry | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getUserInfo | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getToolEventIDFromURL | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\TrackingTool; |
6 | |
7 | use MediaWiki\Config\ServiceOptions; |
8 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup; |
9 | use MediaWiki\Extension\CampaignEvents\Participants\ParticipantsStore; |
10 | use MediaWiki\Extension\CampaignEvents\TrackingTool\Tool\TrackingTool; |
11 | use MediaWiki\Extension\CampaignEvents\TrackingTool\Tool\WikiEduDashboard; |
12 | use MediaWiki\MainConfigNames; |
13 | use RuntimeException; |
14 | use Wikimedia\ObjectFactory\ObjectFactory; |
15 | |
16 | /** |
17 | * This is a registry of known tracking tools, which defines how each tool should be represented in the |
18 | * database and also acts as a factory for TrackingTool objects. |
19 | */ |
20 | class TrackingToolRegistry { |
21 | public const SERVICE_NAME = 'CampaignEventsTrackingToolRegistry'; |
22 | |
23 | public const CONSTRUCTOR_OPTIONS = [ |
24 | 'CampaignEventsProgramsAndEventsDashboardInstance', |
25 | 'CampaignEventsProgramsAndEventsDashboardAPISecret', |
26 | MainConfigNames::CopyUploadProxy, |
27 | ]; |
28 | |
29 | private ObjectFactory $objectFactory; |
30 | private ServiceOptions $options; |
31 | |
32 | /** @var array|null Mock registry that can be set in tests. */ |
33 | private ?array $registryForTests = null; |
34 | |
35 | /** |
36 | * @param ObjectFactory $objectFactory |
37 | * @param ServiceOptions $options |
38 | */ |
39 | public function __construct( ObjectFactory $objectFactory, ServiceOptions $options ) { |
40 | $this->objectFactory = $objectFactory; |
41 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
42 | $this->options = $options; |
43 | } |
44 | |
45 | /** |
46 | * This method returns the internal registry of known tracking tools. This list can potentially be affected by |
47 | * config. Array keys are only used as an internal representation for readability, and For each element |
48 | * the following keys must be set: |
49 | * - display-name-msg (string): Key of i18n message with the display name of the tool |
50 | * - base-url (string): Base URL of the tool's instance |
51 | * - class (string): Name of the class that handles this tool (must extend the TrackingTool abstract class) |
52 | * - db-id (int): ID of the tool stored in the DB |
53 | * - user-id (string): Identifier of the tool as provided by users via the UI or API |
54 | * - extra (array): Any additional information needed by the tool, like an API key. The structure of this |
55 | * array is dependent on the class which uses it. |
56 | * - services (array): List of services that should be injected into the object (before other arguments) |
57 | * Display names, user IDs, and DB IDs are guaranteed to be unique. Classes and base URLs are not, because |
58 | * potentially there could be more instances of the same tool, or a server could host more than one tool, |
59 | * respectively. |
60 | * @return array[] |
61 | * @phan-return array<array{display-name-msg:string,base-url:string,class:class-string,db-id:int,user-id:string,extra:array}> |
62 | */ |
63 | private function getRegistry(): array { |
64 | if ( $this->registryForTests !== null ) { |
65 | return $this->registryForTests; |
66 | } |
67 | |
68 | $registry = []; |
69 | |
70 | $peDashboardData = $this->getConfiguredPEDashboardData(); |
71 | if ( $peDashboardData !== null ) { |
72 | $registry['P&E Dashboard'] = $peDashboardData; |
73 | } |
74 | |
75 | return $registry; |
76 | } |
77 | |
78 | /** |
79 | * Returns the registry definition of the P&E Dashboard, if configured, and null otherwise. |
80 | * |
81 | * @return array{display-name-msg:string,base-url:string,class:class-string,db-id:int,user-id:string,extra:array}|null |
82 | */ |
83 | private function getConfiguredPEDashboardData(): ?array { |
84 | $peDashboardInstance = $this->options->get( 'CampaignEventsProgramsAndEventsDashboardInstance' ); |
85 | if ( $peDashboardInstance === null ) { |
86 | return null; |
87 | } |
88 | |
89 | $dashboardUrl = $peDashboardInstance === 'production' |
90 | ? 'https://outreachdashboard.wmflabs.org/' |
91 | : 'https://dashboard-testing.wikiedu.org/'; |
92 | $apiSecret = $this->options->get( 'CampaignEventsProgramsAndEventsDashboardAPISecret' ); |
93 | if ( !is_string( $apiSecret ) ) { |
94 | throw new RuntimeException( |
95 | '"CampaignEventsProgramsAndEventsDashboardAPISecret" must be configured in order to ' . |
96 | ' use the P&E Dashboard.' |
97 | ); |
98 | } |
99 | return [ |
100 | 'display-name-msg' => 'campaignevents-tracking-tool-p&e-dashboard-name', |
101 | 'base-url' => $dashboardUrl, |
102 | 'class' => WikiEduDashboard::class, |
103 | 'db-id' => 1, |
104 | 'user-id' => 'wikimedia-pe-dashboard', |
105 | 'extra' => [ |
106 | 'secret' => $apiSecret, |
107 | 'proxy' => $this->options->get( MainConfigNames::CopyUploadProxy ) ?: null |
108 | ], |
109 | 'services' => [ |
110 | 'HttpRequestFactory', |
111 | CampaignsCentralUserLookup::SERVICE_NAME, |
112 | ParticipantsStore::SERVICE_NAME |
113 | ] |
114 | ]; |
115 | } |
116 | |
117 | /** |
118 | * Public version of getRegistry used in tests. |
119 | * @return array[] |
120 | * @codeCoverageIgnore |
121 | */ |
122 | public function getRegistryForTesting(): array { |
123 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
124 | throw new RuntimeException( 'This method should only be used in tests' ); |
125 | } |
126 | return $this->getRegistry(); |
127 | } |
128 | |
129 | /** |
130 | * Allows changing the internal registry in tests |
131 | * @param array $registry |
132 | * @codeCoverageIgnore |
133 | */ |
134 | public function setRegistryForTesting( array $registry ): void { |
135 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
136 | throw new RuntimeException( 'This method should only be used in tests' ); |
137 | } |
138 | $this->registryForTests = $registry; |
139 | } |
140 | |
141 | /** |
142 | * Returns a TrackingTool subclass for a tool specified by its DB ID. |
143 | * |
144 | * @param int $dbID |
145 | * @return TrackingTool |
146 | * @throws ToolNotFoundException |
147 | */ |
148 | public function newFromDBID( int $dbID ): TrackingTool { |
149 | foreach ( $this->getRegistry() as $entry ) { |
150 | if ( $entry['db-id'] === $dbID ) { |
151 | return $this->newFromRegistryEntry( $entry ); |
152 | } |
153 | } |
154 | throw new ToolNotFoundException( "No tool with DB ID $dbID" ); |
155 | } |
156 | |
157 | /** |
158 | * Returns data about known tracking tools that can be used to build the edit registration form. This is a subset |
159 | * of the internal registry, only including the 'display-name-msg' and 'user-id' keys. See documentation of |
160 | * getRegistry() for their purpose. |
161 | * |
162 | * @return array[] |
163 | * @phan-return list<array{display-name-msg:string,user-id:string}> |
164 | */ |
165 | public function getDataForForm(): array { |
166 | $ret = []; |
167 | foreach ( $this->getRegistry() as $entry ) { |
168 | $ret[] = array_intersect_key( $entry, [ 'display-name-msg' => true, 'user-id' => true ] ); |
169 | } |
170 | return $ret; |
171 | } |
172 | |
173 | /** |
174 | * Returns a TrackingTool subclass for a tool specified by its user identifier. |
175 | * |
176 | * @param string $userIdentifier |
177 | * @return TrackingTool |
178 | * @throws ToolNotFoundException |
179 | */ |
180 | public function newFromUserIdentifier( string $userIdentifier ): TrackingTool { |
181 | foreach ( $this->getRegistry() as $entry ) { |
182 | if ( $entry['user-id'] === $userIdentifier ) { |
183 | return $this->newFromRegistryEntry( $entry ); |
184 | } |
185 | } |
186 | throw new ToolNotFoundException( "No tool with user ID $userIdentifier" ); |
187 | } |
188 | |
189 | /** |
190 | * @param array $entry |
191 | * @phpcs:ignore Generic.Files.LineLength |
192 | * @phan-param array{display-name-msg:string,base-url:string,class:class-string,db-id:int,user-id:string,extra:array} $entry |
193 | * @return TrackingTool |
194 | * @suppress PhanTypeInvalidCallableArraySize https://github.com/phan/phan/issues/1648 |
195 | */ |
196 | private function newFromRegistryEntry( array $entry ): TrackingTool { |
197 | $class = $entry['class']; |
198 | return $this->objectFactory->createObject( [ |
199 | 'class' => $class, |
200 | 'args' => [ $entry['db-id'], $entry['base-url'], $entry['extra'] ], |
201 | 'services' => $entry['services'] ?? [] |
202 | ] ); |
203 | } |
204 | |
205 | /** |
206 | * Returns information about a tracking tool association that may be used for presentational purposes. |
207 | * |
208 | * @param int $dbID |
209 | * @param string $toolEventID |
210 | * @return array |
211 | * @phan-return array{user-id:string,display-name-msg:string,tool-event-url:string} |
212 | */ |
213 | public function getUserInfo( int $dbID, string $toolEventID ): array { |
214 | foreach ( $this->getRegistry() as $entry ) { |
215 | if ( $entry['db-id'] === $dbID ) { |
216 | /** |
217 | * @var TrackingTool $className Note that this is actually a string, but annotating it like this lets |
218 | * PHPStorm autocomplete the methods and find their usages. |
219 | */ |
220 | $className = $entry['class']; |
221 | return [ |
222 | 'user-id' => $entry['user-id'], |
223 | 'display-name-msg' => $entry['display-name-msg'], |
224 | 'tool-event-url' => $className::buildToolEventURL( $entry['base-url'], $toolEventID ), |
225 | ]; |
226 | } |
227 | } |
228 | throw new ToolNotFoundException( "No tool with DB ID $dbID" ); |
229 | } |
230 | |
231 | /** |
232 | * @param string $userID |
233 | * @param string $toolEventURL |
234 | * @return string |
235 | * @throws InvalidToolURLException |
236 | */ |
237 | public function getToolEventIDFromURL( string $userID, string $toolEventURL ): string { |
238 | foreach ( $this->getRegistry() as $entry ) { |
239 | if ( $entry['user-id'] === $userID ) { |
240 | /** |
241 | * @var TrackingTool $className Note that this is actually a string, but annotating it like this lets |
242 | * PHPStorm autocomplete the methods and find their usages. |
243 | */ |
244 | $className = $entry['class']; |
245 | return $className::extractEventIDFromURL( $entry['base-url'], $toolEventURL ); |
246 | } |
247 | } |
248 | throw new ToolNotFoundException( "No tool with user ID $userID" ); |
249 | } |
250 | } |