Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
44.33% |
391 / 882 |
|
22.92% |
11 / 48 |
CRAP | |
0.00% |
0 / 1 |
Campaign | |
44.33% |
391 / 882 |
|
22.92% |
11 / 48 |
4689.61 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getId | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getName | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getStartTime | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getEndTime | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getPriority | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isEnabled | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isLocked | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isArchived | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isGeotargeted | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getBuckets | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
loadBasicSettings | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
20 | |||
campaignExists | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getActiveCampaignsAndBanners | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
30 | |||
getCampaignSettings | |
98.04% |
50 / 51 |
|
0.00% |
0 / 1 |
3 | |||
getHistoricalCampaigns | |
70.51% |
55 / 78 |
|
0.00% |
0 / 1 |
11.08 | |||
getCampaignMixins | |
45.45% |
35 / 77 |
|
0.00% |
0 / 1 |
57.54 | |||
updateCampaignMixins | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
56 | |||
getAllCampaignNames | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
addCampaign | |
85.06% |
74 / 87 |
|
0.00% |
0 / 1 |
12.48 | |||
removeCampaign | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
3.05 | |||
removeCampaignByName | |
100.00% |
35 / 35 |
|
100.00% |
1 / 1 |
1 | |||
addTemplateTo | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
2 | |||
removeTemplateFor | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getNoticeId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getNoticeName | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getNoticeProjects | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
getNoticeLanguages | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
getNoticeCountries | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
getNoticeRegions | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 | |||
getTitleForURL | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getQueryForURL | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getCanonicalURL | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
updateNoticeDate | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
setBooleanCampaignSetting | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
setNumericCampaignSetting | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
56 | |||
updateWeight | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
updateBucket | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
updateProjectName | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
updateProjects | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
20 | |||
updateProjectLanguages | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
20 | |||
updateCountries | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
20 | |||
updateRegions | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
20 | |||
processAfterCampaignChange | |
89.19% |
33 / 37 |
|
0.00% |
0 / 1 |
7.06 | |||
processSettingsForHook | |
73.33% |
11 / 15 |
|
0.00% |
0 / 1 |
4.30 | |||
settingNameIsValid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setType | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
campaignLogs | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | |
3 | use MediaWiki\MediaWikiServices; |
4 | |
5 | class Campaign { |
6 | |
7 | /** @var int|null */ |
8 | private $id = null; |
9 | /** @var string|null */ |
10 | private $name = null; |
11 | |
12 | /** @var MWTimestamp Start datetime of campaign */ |
13 | private $start = null; |
14 | |
15 | /** @var MWTimestamp End datetime of campaign */ |
16 | private $end = null; |
17 | |
18 | /** @var int Priority level of the campaign, higher is more important */ |
19 | private $priority = null; |
20 | |
21 | /** @var bool True if the campaign is enabled for showing */ |
22 | private $enabled = null; |
23 | |
24 | /** @var bool True if the campaign is currently non editable */ |
25 | private $locked = null; |
26 | |
27 | /** @var bool True if the campaign has been moved to the archive */ |
28 | private $archived = null; |
29 | |
30 | /** @var bool True if there is geo-targeting data for ths campaign */ |
31 | private $geotargeted = null; |
32 | |
33 | /** @var int The number of buckets in this campaign */ |
34 | private $buckets = null; |
35 | |
36 | /** @var int */ |
37 | private $throttle = null; |
38 | |
39 | /** |
40 | * Construct a lazily loaded CentralNotice campaign object |
41 | * |
42 | * @param string|int $campaignIdentifier Either an ID or name for the campaign |
43 | */ |
44 | public function __construct( $campaignIdentifier ) { |
45 | if ( is_int( $campaignIdentifier ) ) { |
46 | $this->id = $campaignIdentifier; |
47 | } else { |
48 | $this->name = $campaignIdentifier; |
49 | } |
50 | } |
51 | |
52 | /** |
53 | * Get the unique numerical ID for this campaign |
54 | * |
55 | * @throws CampaignExistenceException If lazy loading failed. |
56 | * @return int |
57 | */ |
58 | public function getId() { |
59 | if ( $this->id === null ) { |
60 | $this->loadBasicSettings(); |
61 | } |
62 | |
63 | return $this->id; |
64 | } |
65 | |
66 | /** |
67 | * Get the unique name for this campaign |
68 | * |
69 | * @throws CampaignExistenceException If lazy loading failed. |
70 | * @return string |
71 | */ |
72 | public function getName() { |
73 | if ( $this->name === null ) { |
74 | $this->loadBasicSettings(); |
75 | } |
76 | |
77 | return $this->name; |
78 | } |
79 | |
80 | /** |
81 | * Get the start time for the campaign. Only applicable if the campaign is enabled. |
82 | * |
83 | * @throws CampaignExistenceException If lazy loading failed. |
84 | * @return MWTimestamp |
85 | */ |
86 | public function getStartTime() { |
87 | if ( $this->start === null ) { |
88 | $this->loadBasicSettings(); |
89 | } |
90 | |
91 | return $this->start; |
92 | } |
93 | |
94 | /** |
95 | * Get the end time for the campaign. Only applicable if the campaign is enabled. |
96 | * |
97 | * @throws CampaignExistenceException If lazy loading failed. |
98 | * @return MWTimestamp |
99 | */ |
100 | public function getEndTime() { |
101 | if ( $this->end === null ) { |
102 | $this->loadBasicSettings(); |
103 | } |
104 | |
105 | return $this->end; |
106 | } |
107 | |
108 | /** |
109 | * Get the priority level for this campaign. The larger this is the higher the priority is. |
110 | * |
111 | * @throws CampaignExistenceException If lazy loading failed. |
112 | * @return int |
113 | */ |
114 | public function getPriority() { |
115 | if ( $this->priority === null ) { |
116 | $this->loadBasicSettings(); |
117 | } |
118 | |
119 | return $this->priority; |
120 | } |
121 | |
122 | /** |
123 | * Returns the enabled/disabled status of the campaign. |
124 | * |
125 | * If a campaign is enabled it is eligible to be shown to users. |
126 | * |
127 | * @throws CampaignExistenceException If lazy loading failed. |
128 | * @return bool |
129 | */ |
130 | public function isEnabled() { |
131 | if ( $this->enabled === null ) { |
132 | $this->loadBasicSettings(); |
133 | } |
134 | |
135 | return $this->enabled; |
136 | } |
137 | |
138 | /** |
139 | * Returns the locked/unlocked status of the campaign. A locked campaign is not able to be |
140 | * edited until unlocked. |
141 | * |
142 | * @throws CampaignExistenceException If lazy loading failed. |
143 | * @return bool |
144 | */ |
145 | public function isLocked() { |
146 | if ( $this->locked === null ) { |
147 | $this->loadBasicSettings(); |
148 | } |
149 | |
150 | return $this->locked; |
151 | } |
152 | |
153 | /** |
154 | * Returns the archival status of the campaign. An archived campaign is not allowed to be |
155 | * edited. |
156 | * |
157 | * @throws CampaignExistenceException If lazy loading failed. |
158 | * @return bool |
159 | */ |
160 | public function isArchived() { |
161 | if ( $this->archived === null ) { |
162 | $this->loadBasicSettings(); |
163 | } |
164 | |
165 | return $this->archived; |
166 | } |
167 | |
168 | /** |
169 | * Returned the geotargeted status of this campaign. Will be true if GeoIP information should |
170 | * be used to determine user eligibility. |
171 | * |
172 | * @throws CampaignExistenceException If lazy loading failed. |
173 | * @return bool |
174 | */ |
175 | public function isGeotargeted() { |
176 | if ( $this->geotargeted === null ) { |
177 | $this->loadBasicSettings(); |
178 | } |
179 | |
180 | return $this->geotargeted; |
181 | } |
182 | |
183 | /** |
184 | * Get the number of buckets in this campaign. |
185 | * |
186 | * @throws CampaignExistenceException If lazy loading failed. |
187 | * @return int |
188 | */ |
189 | public function getBuckets() { |
190 | if ( $this->buckets === null ) { |
191 | $this->loadBasicSettings(); |
192 | } |
193 | |
194 | return $this->buckets; |
195 | } |
196 | |
197 | /** |
198 | * Load basic campaign settings from the database table cn_notices |
199 | * |
200 | * @throws CampaignExistenceException If the campaign doesn't exist |
201 | */ |
202 | private function loadBasicSettings() { |
203 | $db = CNDatabase::getDb(); |
204 | |
205 | // What selector are we using? |
206 | if ( $this->id !== null ) { |
207 | $selector = [ 'not_id' => $this->id ]; |
208 | } elseif ( $this->name !== null ) { |
209 | $selector = [ 'not_name' => $this->name ]; |
210 | } else { |
211 | throw new CampaignExistenceException( "No valid database key available for campaign." ); |
212 | } |
213 | |
214 | // Get campaign info from database |
215 | $row = $db->selectRow( |
216 | [ 'notices' => 'cn_notices' ], |
217 | [ |
218 | 'not_id', |
219 | 'not_name', |
220 | 'not_start', |
221 | 'not_end', |
222 | 'not_enabled', |
223 | 'not_preferred', |
224 | 'not_locked', |
225 | 'not_archived', |
226 | 'not_geo', |
227 | 'not_buckets', |
228 | 'not_throttle', |
229 | ], |
230 | $selector, |
231 | __METHOD__ |
232 | ); |
233 | if ( $row ) { |
234 | $this->id = $row->not_id; |
235 | $this->name = $row->not_name; |
236 | $this->start = new MWTimestamp( $row->not_start ); |
237 | $this->end = new MWTimestamp( $row->not_end ); |
238 | $this->enabled = (bool)$row->not_enabled; |
239 | $this->priority = (int)$row->not_preferred; |
240 | $this->locked = (bool)$row->not_locked; |
241 | $this->archived = (bool)$row->not_archived; |
242 | $this->geotargeted = (bool)$row->not_geo; |
243 | $this->buckets = (int)$row->not_buckets; |
244 | $this->throttle = (int)$row->not_throttle; |
245 | } else { |
246 | throw new CampaignExistenceException( |
247 | "Campaign could not be retrieved from database " . |
248 | "with id '{$this->id}' or name '{$this->name}'" |
249 | ); |
250 | } |
251 | } |
252 | |
253 | /** |
254 | * See if a given campaign exists in the database |
255 | * |
256 | * @param string $campaignName |
257 | * |
258 | * @return bool |
259 | */ |
260 | public static function campaignExists( $campaignName ) { |
261 | $dbr = CNDatabase::getDb(); |
262 | |
263 | return (bool)$dbr->selectRow( |
264 | 'cn_notices', |
265 | 'not_name', |
266 | [ 'not_name' => $campaignName ], |
267 | __METHOD__ |
268 | ); |
269 | } |
270 | |
271 | /** |
272 | * Get a list of active/active-and-future campaigns and associated banners. |
273 | * |
274 | * @param bool $includeFuture Include campaigns that haven't started yet, too. |
275 | * |
276 | * @return array An array of campaigns, whose elements are arrays with campaign name, |
277 | * an array of associated banners, and campaign start and end times. |
278 | */ |
279 | public static function getActiveCampaignsAndBanners( $includeFuture = false ) { |
280 | $dbr = CNDatabase::getDb( DB_REPLICA ); |
281 | $time = $dbr->timestamp(); |
282 | |
283 | $conds = [ |
284 | 'notices.not_end >= ' . $dbr->addQuotes( $time ), |
285 | 'notices.not_enabled' => 1, |
286 | 'notices.not_archived' => 0 |
287 | ]; |
288 | |
289 | if ( !$includeFuture ) { |
290 | $conds[] = 'notices.not_start <= ' . $dbr->addQuotes( $time ); |
291 | } |
292 | |
293 | // Query campaigns and banners at once |
294 | $dbRows = $dbr->select( |
295 | [ |
296 | 'notices' => 'cn_notices', |
297 | 'assignments' => 'cn_assignments', |
298 | 'templates' => 'cn_templates' |
299 | ], |
300 | [ |
301 | 'notices.not_id', |
302 | 'notices.not_name', |
303 | 'notices.not_start', |
304 | 'notices.not_end', |
305 | 'templates.tmp_name' |
306 | ], |
307 | $conds, |
308 | __METHOD__, |
309 | [], |
310 | [ |
311 | 'assignments' => [ |
312 | 'LEFT OUTER JOIN', 'notices.not_id = assignments.not_id' |
313 | ], |
314 | 'templates' => [ |
315 | 'LEFT OUTER JOIN', 'assignments.tmp_id = templates.tmp_id' |
316 | ] |
317 | ] |
318 | ); |
319 | |
320 | $campaigns = []; |
321 | |
322 | foreach ( $dbRows as $dbRow ) { |
323 | $campaignId = $dbRow->not_id; |
324 | |
325 | // The first time we see any campaign, create the corresponding outer K/V |
326 | // entry. Note that these keys don't make it into data structure we return. |
327 | if ( !isset( $campaigns[$campaignId] ) ) { |
328 | $campaigns[$campaignId] = [ |
329 | 'name' => $dbRow->not_name, |
330 | 'start' => $dbRow->not_start, |
331 | 'end' => $dbRow->not_end, |
332 | ]; |
333 | } |
334 | |
335 | $bannerName = $dbRow->tmp_name; |
336 | // Automagically PHP creates the inner array as needed |
337 | if ( $bannerName ) { |
338 | $campaigns[$campaignId]['banners'][] = $bannerName; |
339 | } |
340 | } |
341 | |
342 | return array_values( $campaigns ); |
343 | } |
344 | |
345 | /** |
346 | * Return settings for a campaign |
347 | * |
348 | * @param string $campaignName The name of the campaign |
349 | * |
350 | * @return array|bool an array of settings or false if the campaign does not exist |
351 | */ |
352 | public static function getCampaignSettings( $campaignName ) { |
353 | $dbr = CNDatabase::getDb(); |
354 | |
355 | // Get campaign info from database |
356 | $row = $dbr->selectRow( |
357 | [ 'notices' => 'cn_notices' ], |
358 | [ |
359 | 'not_id', |
360 | 'not_start', |
361 | 'not_end', |
362 | 'not_enabled', |
363 | 'not_preferred', |
364 | 'not_locked', |
365 | 'not_archived', |
366 | 'not_geo', |
367 | 'not_buckets', |
368 | 'not_throttle', |
369 | 'not_type', |
370 | ], |
371 | [ 'not_name' => $campaignName ], |
372 | __METHOD__ |
373 | ); |
374 | if ( $row ) { |
375 | $campaign = [ |
376 | 'start' => $row->not_start, |
377 | 'end' => $row->not_end, |
378 | 'enabled' => $row->not_enabled, |
379 | 'preferred' => $row->not_preferred, |
380 | 'locked' => $row->not_locked, |
381 | 'archived' => $row->not_archived, |
382 | 'geo' => $row->not_geo, |
383 | 'buckets' => $row->not_buckets, |
384 | 'throttle' => $row->not_throttle, |
385 | 'type' => $row->not_type |
386 | ]; |
387 | } else { |
388 | return false; |
389 | } |
390 | |
391 | $projects = self::getNoticeProjects( $campaignName ); |
392 | $languages = self::getNoticeLanguages( $campaignName ); |
393 | $geo_countries = self::getNoticeCountries( $campaignName ); |
394 | $geo_regions = self::getNoticeRegions( $campaignName ); |
395 | $campaign[ 'projects' ] = implode( ", ", $projects ); |
396 | $campaign[ 'languages' ] = implode( ", ", $languages ); |
397 | $campaign[ 'countries' ] = implode( ", ", $geo_countries ); |
398 | $campaign[ 'regions' ] = implode( ", ", $geo_regions ); |
399 | |
400 | $bannersIn = Banner::getCampaignBanners( $row->not_id ); |
401 | $bannersOut = []; |
402 | // All we want are the banner names, weights, and buckets |
403 | foreach ( $bannersIn as $row ) { |
404 | $outKey = $row['name']; |
405 | $bannersOut[$outKey]['weight'] = $row['weight']; |
406 | $bannersOut[$outKey]['bucket'] = $row['bucket']; |
407 | } |
408 | // Encode into a JSON string for storage |
409 | $campaign[ 'banners' ] = FormatJson::encode( $bannersOut ); |
410 | $campaign[ 'mixins' ] = |
411 | FormatJson::encode( self::getCampaignMixins( $campaignName, true ) ); |
412 | |
413 | return $campaign; |
414 | } |
415 | |
416 | /** |
417 | * Get all campaign configurations as of timestamp $ts |
418 | * |
419 | * @param int $ts |
420 | * @return array of settings structs having the following properties: |
421 | * id |
422 | * name |
423 | * enabled |
424 | * projects: array of sister project names |
425 | * languages: array of language codes |
426 | * countries: array of country codes |
427 | * regions: array of region codes |
428 | * preferred: campaign priority |
429 | * geo: is geolocated? |
430 | * buckets: number of buckets |
431 | * banners: array of banner objects, as returned by getHistoricalBanner, |
432 | * plus the following information from the parent campaign: |
433 | * campaign: name of the campaign |
434 | * campaign_z_index |
435 | * campaign_num_buckets |
436 | * campaign_throttle |
437 | */ |
438 | public static function getHistoricalCampaigns( $ts ) { |
439 | $dbr = CNDatabase::getDb(); |
440 | $tsEnc = $dbr->addQuotes( $dbr->timestamp( $ts ) ); |
441 | |
442 | $res = $dbr->select( |
443 | "cn_notice_log", |
444 | [ |
445 | "log_id" => "MAX(notlog_id)", |
446 | ], |
447 | [ |
448 | "notlog_timestamp <= $tsEnc", |
449 | ], |
450 | __METHOD__, |
451 | [ |
452 | "GROUP BY" => "notlog_not_id", |
453 | ] |
454 | ); |
455 | |
456 | $campaigns = []; |
457 | foreach ( $res as $row ) { |
458 | $campaignRow = $dbr->selectRow( |
459 | "cn_notice_log", |
460 | [ |
461 | "id" => "notlog_not_id", |
462 | "name" => "notlog_not_name", |
463 | "enabled" => "notlog_end_enabled", |
464 | "projects" => "notlog_end_projects", |
465 | "languages" => "notlog_end_languages", |
466 | "countries" => "notlog_end_countries", |
467 | "regions" => "notlog_end_regions", |
468 | "preferred" => "notlog_end_preferred", |
469 | "geotargeted" => "notlog_end_geo", |
470 | "banners" => "notlog_end_banners", |
471 | "bucket_count" => "notlog_end_buckets", |
472 | "throttle" => "notlog_end_throttle", |
473 | ], |
474 | [ |
475 | "notlog_id" => $row->log_id, |
476 | "notlog_end_start <= $tsEnc", |
477 | "notlog_end_end >= $tsEnc", |
478 | "notlog_end_enabled = 1", |
479 | ], |
480 | __METHOD__ |
481 | ); |
482 | |
483 | if ( !$campaignRow ) { |
484 | continue; |
485 | } |
486 | $campaign = (array)$campaignRow; |
487 | $campaign['projects'] = explode( ", ", $campaign['projects'] ); |
488 | $campaign['languages'] = explode( ", ", $campaign['languages'] ); |
489 | $campaign['countries'] = explode( ", ", $campaign['countries'] ); |
490 | $campaign['regions'] = explode( ", ", $campaign['regions'] ); |
491 | if ( $campaign['banners'] === null ) { |
492 | $campaign['banners'] = []; |
493 | } else { |
494 | $campaign['banners'] = FormatJson::decode( $campaign['banners'], true ); |
495 | if ( !is_array( current( $campaign['banners'] ) ) ) { |
496 | // Old log format; only had weight |
497 | foreach ( $campaign['banners'] as $key => &$value ) { |
498 | $value = [ |
499 | 'weight' => $value, |
500 | 'bucket' => 0 |
501 | ]; |
502 | } |
503 | } |
504 | } |
505 | if ( $campaign['bucket_count'] === null ) { |
506 | // Fix for legacy logs before bucketing |
507 | $campaign['bucket_count'] = 1; |
508 | } |
509 | foreach ( $campaign['banners'] as $name => &$banner ) { |
510 | $historical_banner = Banner::getHistoricalBanner( $name, $ts ); |
511 | |
512 | if ( $historical_banner === null ) { |
513 | // FIXME: crazy hacks |
514 | $historical_banner = Banner::getBannerSettings( $name ); |
515 | $historical_banner['label'] = wfMessage( 'centralnotice-damaged-log', $name )->text(); |
516 | $historical_banner['display_anon'] = $historical_banner['anon']; |
517 | $historical_banner['display_account'] = $historical_banner['account']; |
518 | $historical_banner['devices'] = [ 'desktop' ]; |
519 | } |
520 | $banner['name'] = $name; |
521 | $banner['label'] = $name; |
522 | |
523 | $campaign_info = [ |
524 | 'campaign' => $campaign['name'], |
525 | 'campaign_z_index' => $campaign['preferred'], |
526 | 'campaign_num_buckets' => $campaign['bucket_count'], |
527 | 'campaign_throttle' => $campaign['throttle'], |
528 | ]; |
529 | |
530 | $banner = array_merge( $banner, $campaign_info, $historical_banner ); |
531 | } |
532 | |
533 | $campaigns[] = $campaign; |
534 | } |
535 | return $campaigns; |
536 | } |
537 | |
538 | /** |
539 | * Retrieve campaign mixins settings for this campaign. |
540 | * |
541 | * If $compact is true, retrieve only enabled mixins, and return a compact |
542 | * data structure in which keys are mixin names and values are parameter |
543 | * settings. |
544 | * |
545 | * If $compact is false, mixins that were once enabled for this campaign but |
546 | * are now disabled will be included, showing their last parameter settings. |
547 | * The data structure will be an array whose keys are mixin names and whose |
548 | * values are arrays with 'enabled' and 'parameters' keys. Note that mixins |
549 | * that were never enabled for this campaign will be omitted. |
550 | * |
551 | * @param string $campaignName |
552 | * @param bool $compact |
553 | * @return array |
554 | */ |
555 | public static function getCampaignMixins( $campaignName, $compact = false ) { |
556 | global $wgCentralNoticeCampaignMixins; |
557 | |
558 | $dbr = CNDatabase::getDb(); |
559 | |
560 | // Prepare query conditions |
561 | $conds = [ 'notices.not_name' => $campaignName ]; |
562 | if ( $compact ) { |
563 | $conds['notice_mixins.nmxn_enabled'] = 1; |
564 | } |
565 | |
566 | $dbRows = $dbr->select( |
567 | [ |
568 | 'notices' => 'cn_notices', |
569 | 'notice_mixins' => 'cn_notice_mixins', |
570 | 'notice_mixin_params' => 'cn_notice_mixin_params' |
571 | ], |
572 | [ |
573 | 'notice_mixins.nmxn_mixin_name', |
574 | 'notice_mixins.nmxn_enabled', |
575 | 'notice_mixin_params.nmxnp_param_name', |
576 | 'notice_mixin_params.nmxnp_param_value' |
577 | ], |
578 | $conds, |
579 | __METHOD__, |
580 | [], |
581 | [ |
582 | 'notice_mixins' => [ |
583 | 'INNER JOIN', 'notices.not_id = notice_mixins.nmxn_not_id' |
584 | ], |
585 | 'notice_mixin_params' => [ |
586 | 'LEFT OUTER JOIN', |
587 | 'notice_mixins.nmxn_id = notice_mixin_params.nmxnp_notice_mixin_id' |
588 | ] |
589 | ] |
590 | ); |
591 | |
592 | // Build up the results |
593 | // We expect a row for every parameter name-value pair for every mixin, |
594 | // and maybe some with null name-value pairs (for mixins with no |
595 | // parameters). |
596 | $campaignMixins = []; |
597 | foreach ( $dbRows as $dbRow ) { |
598 | $mixinName = $dbRow->nmxn_mixin_name; |
599 | |
600 | // A mixin may have been removed from the code but may still |
601 | // leave stuff in the database. In that case, skip it! |
602 | if ( !isset( $wgCentralNoticeCampaignMixins[$mixinName] ) ) { |
603 | continue; |
604 | } |
605 | |
606 | // First time we have a result row for this mixin? |
607 | if ( !isset( $campaignMixins[$mixinName] ) ) { |
608 | // Data structure depends on $compact |
609 | if ( $compact ) { |
610 | $campaignMixins[$mixinName] = []; |
611 | |
612 | } else { |
613 | $campaignMixins[$mixinName] = [ |
614 | 'enabled' => (bool)$dbRow->nmxn_enabled, |
615 | 'parameters' => [] |
616 | ]; |
617 | } |
618 | } |
619 | |
620 | // If there are mixin params in this row, add them in |
621 | if ( $dbRow->nmxnp_param_name !== null ) { |
622 | $paramName = $dbRow->nmxnp_param_name; |
623 | $mixinDef = $wgCentralNoticeCampaignMixins[$mixinName]; |
624 | |
625 | // Handle mixin parameters being removed, too |
626 | if ( !isset( $mixinDef['parameters'][$paramName] ) ) { |
627 | continue; |
628 | } |
629 | |
630 | $paramType = $mixinDef['parameters'][$paramName]['type']; |
631 | |
632 | switch ( $paramType ) { |
633 | case 'string': |
634 | $paramVal = $dbRow->nmxnp_param_value; |
635 | break; |
636 | |
637 | case 'integer': |
638 | $paramVal = intval( $dbRow->nmxnp_param_value ); |
639 | break; |
640 | |
641 | case 'float': |
642 | $paramVal = floatval( $dbRow->nmxnp_param_value ); |
643 | break; |
644 | |
645 | case 'boolean': |
646 | $paramVal = ( $dbRow->nmxnp_param_value === 'true' ); |
647 | break; |
648 | |
649 | case 'json': |
650 | $paramVal = json_decode( $dbRow->nmxnp_param_value ); |
651 | |
652 | if ( $paramVal === null ) { |
653 | wfLogWarning( 'Couldn\'t decode json param ' . $paramName |
654 | . ' for mixin ' . $mixinName . ' in campaign ' . |
655 | $campaignName . '.' ); |
656 | |
657 | // In this case, it's fine to emit a null value for the |
658 | // parameter. Both Admin UI and subscribing client-side |
659 | // code should handle it gracefully and warn in the console. |
660 | // TODO Handle this better, server-side. |
661 | } |
662 | |
663 | break; |
664 | |
665 | default: |
666 | throw new DomainException( |
667 | 'Unknown parameter type ' . $paramType ); |
668 | } |
669 | |
670 | // Again, data structure depends on $compact |
671 | if ( $compact ) { |
672 | $campaignMixins[$mixinName][$paramName] = $paramVal; |
673 | } else { |
674 | $campaignMixins[$mixinName]['parameters'][$paramName] |
675 | = $paramVal; |
676 | } |
677 | } |
678 | } |
679 | |
680 | // Ensure consistent ordering, since it's needed for |
681 | // CNChoiceDataResourceLoaderModule (which gets this data via |
682 | // ChoiceDataProvider) for consistent RL module hashes. |
683 | |
684 | array_walk( $campaignMixins, static function ( &$campaignMixin ) { |
685 | ksort( $campaignMixin ); |
686 | } ); |
687 | |
688 | ksort( $campaignMixins ); |
689 | |
690 | return $campaignMixins; |
691 | } |
692 | |
693 | /** |
694 | * Update enabled or disabled status and parameters for a campaign mixin, |
695 | * for a given campaign. |
696 | * |
697 | * @param string $campaignName |
698 | * @param string $mixinName |
699 | * @param bool $enable |
700 | * @param array|null $params For mixins with no parameters, set to an empty array. |
701 | */ |
702 | public static function updateCampaignMixins( |
703 | $campaignName, $mixinName, $enable, $params = null |
704 | ) { |
705 | global $wgCentralNoticeCampaignMixins; |
706 | |
707 | // TODO Error handling! |
708 | |
709 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
710 | |
711 | // Get the campaign ID |
712 | // Note: the need to fetch the ID here highlights the need for some |
713 | // kind of ORM. |
714 | $noticeId = $dbw->selectRow( 'cn_notices', 'not_id', |
715 | [ 'not_name' => $campaignName ], __METHOD__ )->not_id; |
716 | |
717 | if ( $enable ) { |
718 | if ( $params === null ) { |
719 | throw new InvalidArgumentException( 'Paremeters info required to enable mixin ' . |
720 | $mixinName . ' for campaign ' . $campaignName ); |
721 | } |
722 | |
723 | $dbw->newInsertQueryBuilder() |
724 | ->insertInto( 'cn_notice_mixins' ) |
725 | ->row( [ |
726 | 'nmxn_not_id' => $noticeId, |
727 | 'nmxn_mixin_name' => $mixinName, |
728 | 'nmxn_enabled' => 1 |
729 | ] ) |
730 | ->onDuplicateKeyUpdate() |
731 | ->uniqueIndexFields( [ 'nmxn_not_id', 'nmxn_mixin_name' ] ) |
732 | ->set( [ |
733 | 'nmxn_enabled' => 1 |
734 | ] ) |
735 | ->caller( __METHOD__ ) |
736 | ->execute(); |
737 | |
738 | $noticeMixinId = $dbw->selectRow( |
739 | 'cn_notice_mixins', |
740 | 'nmxn_id', |
741 | [ |
742 | 'nmxn_not_id' => $noticeId, |
743 | 'nmxn_mixin_name' => $mixinName |
744 | ], |
745 | __METHOD__ |
746 | )->nmxn_id; |
747 | |
748 | foreach ( $params as $paramName => $paramVal ) { |
749 | $mixinDef = $wgCentralNoticeCampaignMixins[$mixinName]; |
750 | |
751 | // Handle an undefined parameter. Not likely to happen, maybe |
752 | // in the middle of a deploy that removes a parameter. |
753 | if ( !isset( $mixinDef['parameters'][$paramName] ) ) { |
754 | wfLogWarning( 'No definition found for the parameter ' |
755 | . $paramName . ' for the campaign mixn ' . |
756 | $mixinName . '.' ); |
757 | |
758 | continue; |
759 | } |
760 | |
761 | // Munge boolean params for database storage. (Other types |
762 | // should end up as strings, which will be fine.) |
763 | if ( $mixinDef['parameters'][$paramName]['type'] === 'boolean' ) { |
764 | $paramVal = ( $paramVal ? 'true' : 'false' ); |
765 | } |
766 | |
767 | $dbw->newInsertQueryBuilder() |
768 | ->insertInto( 'cn_notice_mixin_params' ) |
769 | ->row( [ |
770 | 'nmxnp_notice_mixin_id' => $noticeMixinId, |
771 | 'nmxnp_param_name' => $paramName, |
772 | 'nmxnp_param_value' => $paramVal |
773 | ] ) |
774 | ->onDuplicateKeyUpdate() |
775 | ->uniqueIndexFields( [ 'nmxnp_notice_mixin_id', 'nmxnp_param_name' ] ) |
776 | ->set( [ |
777 | 'nmxnp_param_value' => $paramVal |
778 | ] ) |
779 | ->caller( __METHOD__ ) |
780 | ->execute(); |
781 | } |
782 | |
783 | } else { |
784 | |
785 | // When we disable a mixin, just set enabled to false; since we keep |
786 | // the old parameter values in case the mixin is re-enabled, we also |
787 | // keep the row in this table, since the id is used in the param |
788 | // table. |
789 | $dbw->newUpdateQueryBuilder() |
790 | ->update( 'cn_notice_mixins' ) |
791 | ->set( [ 'nmxn_enabled' => 0 ] ) |
792 | ->where( [ |
793 | 'nmxn_not_id' => $noticeId, |
794 | 'nmxn_mixin_name' => $mixinName |
795 | ] ) |
796 | ->caller( __METHOD__ ) |
797 | ->execute(); |
798 | } |
799 | } |
800 | |
801 | /** |
802 | * Get all the campaigns in the database |
803 | * |
804 | * @return array an array of campaign names |
805 | */ |
806 | public static function getAllCampaignNames() { |
807 | $dbr = CNDatabase::getDb(); |
808 | $res = $dbr->select( 'cn_notices', 'not_name', '', __METHOD__ ); |
809 | $notices = []; |
810 | foreach ( $res as $row ) { |
811 | $notices[] = $row->not_name; |
812 | } |
813 | return $notices; |
814 | } |
815 | |
816 | /** |
817 | * Add a new campaign to the database |
818 | * |
819 | * @param string $noticeName Name of the campaign |
820 | * @param bool $enabled Boolean setting, true or false |
821 | * @param string $startTs Campaign start in UTC |
822 | * @param array $projects Targeted project types (wikipedia, wikibooks, etc.) |
823 | * @param array $project_languages Targeted project languages (en, de, etc.) |
824 | * @param bool $geotargeted Boolean setting, true or false |
825 | * @param array $geo_countries Targeted countries |
826 | * @param array $geo_regions Targeted regions in format CountryCode_RegionCode |
827 | * @param int $throttle limit allocations, 0 - 100 |
828 | * @param int $priority priority level, LOW_PRIORITY - EMERGENCY_PRIORITY |
829 | * @param User $user User adding the campaign |
830 | * @param string|null $type Type of campaign |
831 | * @param string|null $summary Change summary provided by the user |
832 | * @return int|string noticeId on success, or message key for error |
833 | */ |
834 | public static function addCampaign( $noticeName, $enabled, $startTs, $projects, |
835 | $project_languages, $geotargeted, $geo_countries, $geo_regions, $throttle, |
836 | $priority, $user, $type, $summary = null |
837 | ) { |
838 | $noticeName = trim( $noticeName ); |
839 | if ( self::campaignExists( $noticeName ) ) { |
840 | return 'centralnotice-notice-exists'; |
841 | } elseif ( !$projects ) { |
842 | return 'centralnotice-no-project'; |
843 | } elseif ( !$project_languages ) { |
844 | return 'centralnotice-no-language'; |
845 | } |
846 | |
847 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
848 | $dbw->startAtomic( __METHOD__ ); |
849 | |
850 | $endTime = strtotime( '+1 hour', (int)wfTimestamp( TS_UNIX, $startTs ) ); |
851 | $endTs = wfTimestamp( TS_MW, $endTime ); |
852 | |
853 | $dbw->newInsertQueryBuilder() |
854 | ->insertInto( 'cn_notices' ) |
855 | ->row( [ |
856 | 'not_name' => $noticeName, |
857 | 'not_enabled' => (int)$enabled, |
858 | 'not_start' => $dbw->timestamp( $startTs ), |
859 | 'not_end' => $dbw->timestamp( $endTs ), |
860 | 'not_geo' => (int)$geotargeted, |
861 | 'not_throttle' => $throttle, |
862 | 'not_preferred' => $priority, |
863 | 'not_type' => $type |
864 | ] ) |
865 | ->caller( __METHOD__ ) |
866 | ->execute(); |
867 | $not_id = $dbw->insertId(); |
868 | |
869 | if ( $not_id ) { |
870 | // Do multi-row insert for campaign projects |
871 | $insertArray = []; |
872 | foreach ( $projects as $project ) { |
873 | $insertArray[] = [ 'np_notice_id' => $not_id, 'np_project' => $project ]; |
874 | } |
875 | $dbw->newInsertQueryBuilder() |
876 | ->insertInto( 'cn_notice_projects' ) |
877 | ->ignore() |
878 | ->rows( $insertArray ) |
879 | ->caller( __METHOD__ ) |
880 | ->execute(); |
881 | |
882 | // Do multi-row insert for campaign languages |
883 | $insertArray = []; |
884 | foreach ( $project_languages as $code ) { |
885 | $insertArray[] = [ 'nl_notice_id' => $not_id, 'nl_language' => $code ]; |
886 | } |
887 | $dbw->newInsertQueryBuilder() |
888 | ->insertInto( 'cn_notice_languages' ) |
889 | ->ignore() |
890 | ->rows( $insertArray ) |
891 | ->caller( __METHOD__ ) |
892 | ->execute(); |
893 | |
894 | if ( $geotargeted ) { |
895 | |
896 | // Do multi-row insert for campaign countries |
897 | if ( $geo_countries ) { |
898 | $insertArray = []; |
899 | foreach ( $geo_countries as $code ) { |
900 | $insertArray[] = [ 'nc_notice_id' => $not_id, 'nc_country' => $code ]; |
901 | } |
902 | $dbw->newInsertQueryBuilder() |
903 | ->insertInto( 'cn_notice_countries' ) |
904 | ->ignore() |
905 | ->rows( $insertArray ) |
906 | ->caller( __METHOD__ ) |
907 | ->execute(); |
908 | } |
909 | |
910 | // Do multi-row insert for campaign regions |
911 | if ( $geo_regions ) { |
912 | $insertArray = []; |
913 | foreach ( $geo_regions as $code ) { |
914 | $insertArray[] = [ 'nr_notice_id' => $not_id, 'nr_region' => $code ]; |
915 | } |
916 | $dbw->newInsertQueryBuilder() |
917 | ->insertInto( 'cn_notice_regions' ) |
918 | ->ignore() |
919 | ->rows( $insertArray ) |
920 | ->caller( __METHOD__ ) |
921 | ->execute(); |
922 | } |
923 | |
924 | } |
925 | |
926 | $dbw->endAtomic( __METHOD__ ); |
927 | |
928 | // Log the creation of the campaign |
929 | $beginSettings = []; |
930 | $endSettings = [ |
931 | 'projects' => implode( ", ", $projects ), |
932 | 'languages' => implode( ", ", $project_languages ), |
933 | 'countries' => implode( ", ", $geo_countries ), |
934 | 'regions' => implode( ", ", $geo_regions ), |
935 | 'start' => $dbw->timestamp( $startTs ), |
936 | 'end' => $dbw->timestamp( $endTs ), |
937 | 'enabled' => (int)$enabled, |
938 | 'preferred' => 0, |
939 | 'locked' => 0, |
940 | 'archived' => 0, |
941 | 'geo' => (int)$geotargeted, |
942 | 'throttle' => $throttle, |
943 | 'type' => $type |
944 | ]; |
945 | self::processAfterCampaignChange( 'created', $not_id, $noticeName, $user, |
946 | $beginSettings, $endSettings, $summary ); |
947 | |
948 | return $not_id; |
949 | } |
950 | |
951 | throw new RuntimeException( 'insertId() did not return a value.' ); |
952 | } |
953 | |
954 | /** |
955 | * Remove a campaign from the database |
956 | * |
957 | * @param string $campaignName Name of the campaign |
958 | * @param User $user User removing the campaign |
959 | * |
960 | * @return bool|string True on success, string with message key for error |
961 | */ |
962 | public static function removeCampaign( $campaignName, $user ) { |
963 | // TODO This method is never used? |
964 | $dbr = CNDatabase::getDb( DB_PRIMARY ); |
965 | |
966 | $res = $dbr->selectField( 'cn_notices', 'not_locked', |
967 | [ 'not_name' => $campaignName ], |
968 | __METHOD__ |
969 | ); |
970 | if ( $res === false ) { |
971 | return 'centralnotice-remove-notice-doesnt-exist'; |
972 | } |
973 | if ( $res ) { |
974 | return 'centralnotice-notice-is-locked'; |
975 | } |
976 | |
977 | self::removeCampaignByName( $campaignName, $user ); |
978 | |
979 | return true; |
980 | } |
981 | |
982 | private static function removeCampaignByName( $campaignName, $user ) { |
983 | // Log the removal of the campaign |
984 | $campaignId = self::getNoticeId( $campaignName ); |
985 | self::processAfterCampaignChange( 'removed', $campaignId, $campaignName, $user ); |
986 | |
987 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
988 | $dbw->startAtomic( __METHOD__ ); |
989 | $dbw->newDeleteQueryBuilder() |
990 | ->deleteFrom( 'cn_assignments' ) |
991 | ->where( [ 'not_id' => $campaignId ] ) |
992 | ->caller( __METHOD__ ) |
993 | ->execute(); |
994 | $dbw->newDeleteQueryBuilder() |
995 | ->deleteFrom( 'cn_notices' ) |
996 | ->where( [ 'not_name' => $campaignName ] ) |
997 | ->caller( __METHOD__ ) |
998 | ->execute(); |
999 | $dbw->newDeleteQueryBuilder() |
1000 | ->deleteFrom( 'cn_notice_languages' ) |
1001 | ->where( [ 'nl_notice_id' => $campaignId ] ) |
1002 | ->caller( __METHOD__ ) |
1003 | ->execute(); |
1004 | $dbw->newDeleteQueryBuilder() |
1005 | ->deleteFrom( 'cn_notice_projects' ) |
1006 | ->where( [ 'np_notice_id' => $campaignId ] ) |
1007 | ->caller( __METHOD__ ) |
1008 | ->execute(); |
1009 | $dbw->newDeleteQueryBuilder() |
1010 | ->deleteFrom( 'cn_notice_countries' ) |
1011 | ->where( [ 'nc_notice_id' => $campaignId ] ) |
1012 | ->caller( __METHOD__ ) |
1013 | ->execute(); |
1014 | $dbw->newDeleteQueryBuilder() |
1015 | ->deleteFrom( 'cn_notice_regions' ) |
1016 | ->where( [ 'nr_notice_id' => $campaignId ] ) |
1017 | ->caller( __METHOD__ ) |
1018 | ->execute(); |
1019 | $dbw->endAtomic( __METHOD__ ); |
1020 | } |
1021 | |
1022 | /** |
1023 | * Assign a banner to a campaign at a certain weight |
1024 | * @param string $noticeName |
1025 | * @param string $templateName |
1026 | * @param int $weight |
1027 | * @param int $bucket |
1028 | * @return bool|string True on success, string with message key for error |
1029 | */ |
1030 | public static function addTemplateTo( $noticeName, $templateName, $weight, $bucket = 0 ) { |
1031 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1032 | |
1033 | $noticeId = self::getNoticeId( $noticeName ); |
1034 | $templateId = Banner::fromName( $templateName )->getId(); |
1035 | $res = $dbw->select( 'cn_assignments', 'asn_id', |
1036 | [ |
1037 | 'tmp_id' => $templateId, |
1038 | 'not_id' => $noticeId |
1039 | ], |
1040 | __METHOD__ |
1041 | ); |
1042 | |
1043 | if ( $res->numRows() > 0 ) { |
1044 | return 'centralnotice-template-already-exists'; |
1045 | } |
1046 | |
1047 | $dbw->newInsertQueryBuilder() |
1048 | ->insertInto( 'cn_assignments' ) |
1049 | ->row( [ |
1050 | 'tmp_id' => $templateId, |
1051 | 'tmp_weight' => $weight, |
1052 | 'not_id' => $noticeId, |
1053 | 'asn_bucket' => $bucket, |
1054 | ] ) |
1055 | ->caller( __METHOD__ ) |
1056 | ->execute(); |
1057 | |
1058 | return true; |
1059 | } |
1060 | |
1061 | /** |
1062 | * Remove a banner assignment from a campaign |
1063 | * @param string $noticeName |
1064 | * @param string $templateName |
1065 | */ |
1066 | public static function removeTemplateFor( $noticeName, $templateName ) { |
1067 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1068 | $noticeId = self::getNoticeId( $noticeName ); |
1069 | $templateId = Banner::fromName( $templateName )->getId(); |
1070 | $dbw->newDeleteQueryBuilder() |
1071 | ->deleteFrom( 'cn_assignments' ) |
1072 | ->where( [ 'tmp_id' => $templateId, 'not_id' => $noticeId ] ) |
1073 | ->caller( __METHOD__ ) |
1074 | ->execute(); |
1075 | } |
1076 | |
1077 | /** |
1078 | * Lookup the ID for a campaign based on the campaign name |
1079 | * @param string $noticeName |
1080 | * @return int|null |
1081 | */ |
1082 | public static function getNoticeId( $noticeName ) { |
1083 | $dbr = CNDatabase::getDb(); |
1084 | $row = $dbr->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $noticeName ], __METHOD__ ); |
1085 | if ( $row ) { |
1086 | return $row->not_id; |
1087 | } else { |
1088 | return null; |
1089 | } |
1090 | } |
1091 | |
1092 | /** |
1093 | * Lookup the name of a campaign based on the campaign ID |
1094 | * @param int $noticeId |
1095 | * @return null|string |
1096 | */ |
1097 | public static function getNoticeName( $noticeId ) { |
1098 | $dbr = CNDatabase::getDb(); |
1099 | if ( is_numeric( $noticeId ) ) { |
1100 | $row = $dbr->selectRow( 'cn_notices', 'not_name', [ 'not_id' => $noticeId ], __METHOD__ ); |
1101 | if ( $row ) { |
1102 | return $row->not_name; |
1103 | } |
1104 | } |
1105 | return null; |
1106 | } |
1107 | |
1108 | public static function getNoticeProjects( $noticeName ) { |
1109 | $dbr = CNDatabase::getDb(); |
1110 | $row = $dbr->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $noticeName ], __METHOD__ ); |
1111 | $projects = []; |
1112 | if ( $row ) { |
1113 | $res = $dbr->select( 'cn_notice_projects', 'np_project', |
1114 | [ 'np_notice_id' => $row->not_id ], __METHOD__ ); |
1115 | foreach ( $res as $projectRow ) { |
1116 | $projects[] = $projectRow->np_project; |
1117 | } |
1118 | } |
1119 | sort( $projects ); |
1120 | return $projects; |
1121 | } |
1122 | |
1123 | public static function getNoticeLanguages( $noticeName ) { |
1124 | $dbr = CNDatabase::getDb(); |
1125 | $row = $dbr->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $noticeName ], __METHOD__ ); |
1126 | $languages = []; |
1127 | if ( $row ) { |
1128 | $res = $dbr->select( 'cn_notice_languages', 'nl_language', |
1129 | [ 'nl_notice_id' => $row->not_id ], __METHOD__ ); |
1130 | foreach ( $res as $langRow ) { |
1131 | $languages[] = $langRow->nl_language; |
1132 | } |
1133 | } |
1134 | sort( $languages ); |
1135 | return $languages; |
1136 | } |
1137 | |
1138 | /** |
1139 | * @param string $noticeName |
1140 | * |
1141 | * @return string[] |
1142 | */ |
1143 | public static function getNoticeCountries( $noticeName ) { |
1144 | $dbr = CNDatabase::getDb(); |
1145 | $row = $dbr->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $noticeName ], __METHOD__ ); |
1146 | $countries = []; |
1147 | if ( $row ) { |
1148 | $res = $dbr->select( 'cn_notice_countries', 'nc_country', |
1149 | [ 'nc_notice_id' => $row->not_id ], __METHOD__ ); |
1150 | foreach ( $res as $countryRow ) { |
1151 | $countries[] = $countryRow->nc_country; |
1152 | } |
1153 | } |
1154 | sort( $countries ); |
1155 | return $countries; |
1156 | } |
1157 | |
1158 | /** |
1159 | * @param string $noticeName |
1160 | * |
1161 | * @return string[] |
1162 | */ |
1163 | public static function getNoticeRegions( $noticeName ) { |
1164 | $dbr = CNDatabase::getDb(); |
1165 | $row = $dbr->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $noticeName ], __METHOD__ ); |
1166 | $regions = []; |
1167 | if ( $row ) { |
1168 | $res = $dbr->select( 'cn_notice_regions', 'nr_region', |
1169 | [ 'nr_notice_id' => $row->not_id ], __METHOD__ ); |
1170 | foreach ( $res as $regionRow ) { |
1171 | $regions[] = $regionRow->nr_region; |
1172 | } |
1173 | } |
1174 | sort( $regions ); |
1175 | return $regions; |
1176 | } |
1177 | |
1178 | /** |
1179 | * Returns a Title object to use in obtaining the URL of a campaign. |
1180 | * @return Title |
1181 | */ |
1182 | public static function getTitleForURL() { |
1183 | return SpecialPage::getTitleFor( 'CentralNotice' ); |
1184 | } |
1185 | |
1186 | /** |
1187 | * Returns an array with key/value pairs for a query string, to use in obtaining the |
1188 | * URL of the campaign with the specified name. |
1189 | * |
1190 | * @param string $campaignName |
1191 | * @return string[] |
1192 | */ |
1193 | public static function getQueryForURL( $campaignName ) { |
1194 | return [ |
1195 | 'subaction' => 'noticeDetail', |
1196 | 'notice' => $campaignName |
1197 | ]; |
1198 | } |
1199 | |
1200 | /** |
1201 | * Returns the canonical URL for campaign with the specified name (as returned by |
1202 | * Title::getCanonicalURL()). |
1203 | * |
1204 | * Usage note: This method should be considered part of CentralNotice's public API. |
1205 | * It's called from outside the extension in EventBus::onCentralNoticeCampaignChange(). |
1206 | * |
1207 | * @param string $campaignName |
1208 | * @return string |
1209 | */ |
1210 | public static function getCanonicalURL( $campaignName ) { |
1211 | return self::getTitleForURL()->getCanonicalURL( |
1212 | self::getQueryForURL( $campaignName ) ); |
1213 | } |
1214 | |
1215 | /** |
1216 | * @param string $noticeName |
1217 | * @param string $start Date |
1218 | * @param string $end Date |
1219 | * @return bool|string True on success, string with message key for error |
1220 | */ |
1221 | public static function updateNoticeDate( $noticeName, $start, $end ) { |
1222 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1223 | |
1224 | // Start/end don't line up |
1225 | if ( $start > $end || $end < $start ) { |
1226 | return 'centralnotice-invalid-date-range'; |
1227 | } |
1228 | |
1229 | // Invalid campaign name |
1230 | if ( !self::campaignExists( $noticeName ) ) { |
1231 | return 'centralnotice-notice-doesnt-exist'; |
1232 | } |
1233 | |
1234 | // Overlap over a date within the same project and language |
1235 | $startDate = $dbw->timestamp( $start ); |
1236 | $endDate = $dbw->timestamp( $end ); |
1237 | |
1238 | $dbw->newUpdateQueryBuilder() |
1239 | ->update( 'cn_notices' ) |
1240 | ->set( [ |
1241 | 'not_start' => $startDate, |
1242 | 'not_end' => $endDate |
1243 | ] ) |
1244 | ->where( [ 'not_name' => $noticeName ] ) |
1245 | ->caller( __METHOD__ ) |
1246 | ->execute(); |
1247 | |
1248 | return true; |
1249 | } |
1250 | |
1251 | /** |
1252 | * Update a boolean setting on a campaign |
1253 | * |
1254 | * @param string $noticeName Name of the campaign |
1255 | * @param string $settingName Name of a boolean setting (enabled, locked, or geo) |
1256 | * @param bool $settingValue Value to use for the setting, true or false |
1257 | */ |
1258 | public static function setBooleanCampaignSetting( $noticeName, $settingName, $settingValue ) { |
1259 | if ( !self::campaignExists( $noticeName ) ) { |
1260 | // Exit quietly since campaign may have been deleted at the same time. |
1261 | return; |
1262 | } else { |
1263 | $settingName = strtolower( $settingName ); |
1264 | if ( !self::settingNameIsValid( $settingName ) ) { |
1265 | throw new InvalidArgumentException( "Invalid setting name" ); |
1266 | } |
1267 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1268 | $dbw->newUpdateQueryBuilder() |
1269 | ->update( 'cn_notices' ) |
1270 | ->set( [ 'not_' . $settingName => (int)$settingValue ] ) |
1271 | ->where( [ 'not_name' => $noticeName ] ) |
1272 | ->caller( __METHOD__ ) |
1273 | ->execute(); |
1274 | } |
1275 | } |
1276 | |
1277 | /** |
1278 | * Updates a numeric setting on a campaign |
1279 | * |
1280 | * @param string $noticeName Name of the campaign |
1281 | * @param string $settingName Name of a numeric setting (preferred) |
1282 | * @param int $settingValue Value to use |
1283 | * @param int $max The max that the value can take, default 1 |
1284 | * @param int $min The min that the value can take, default 0 |
1285 | * @throws InvalidArgumentException|RangeException |
1286 | */ |
1287 | public static function setNumericCampaignSetting( |
1288 | $noticeName, $settingName, $settingValue, $max = 1, $min = 0 |
1289 | ) { |
1290 | if ( $max <= $min ) { |
1291 | throw new RangeException( 'Max must be greater than min.' ); |
1292 | } |
1293 | |
1294 | if ( !is_numeric( $settingValue ) ) { |
1295 | throw new InvalidArgumentException( 'Setting value must be numeric.' ); |
1296 | } |
1297 | |
1298 | if ( $settingValue > $max ) { |
1299 | $settingValue = $max; |
1300 | } |
1301 | |
1302 | if ( $settingValue < $min ) { |
1303 | $settingValue = $min; |
1304 | } |
1305 | |
1306 | if ( !self::campaignExists( $noticeName ) ) { |
1307 | // Exit quietly since campaign may have been deleted at the same time. |
1308 | return; |
1309 | } else { |
1310 | $settingName = strtolower( $settingName ); |
1311 | if ( !self::settingNameIsValid( $settingName ) ) { |
1312 | throw new InvalidArgumentException( "Invalid setting name" ); |
1313 | } |
1314 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1315 | $dbw->newUpdateQueryBuilder() |
1316 | ->update( 'cn_notices' ) |
1317 | ->set( [ 'not_' . $settingName => $settingValue ] ) |
1318 | ->where( [ 'not_name' => $noticeName ] ) |
1319 | ->caller( __METHOD__ ) |
1320 | ->execute(); |
1321 | } |
1322 | } |
1323 | |
1324 | /** |
1325 | * Updates the weight of a banner in a campaign. |
1326 | * |
1327 | * @param string $noticeName Name of the campaign to update |
1328 | * @param int $templateId ID of the banner in the campaign |
1329 | * @param int $weight New banner weight |
1330 | */ |
1331 | public static function updateWeight( $noticeName, $templateId, $weight ) { |
1332 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1333 | $noticeId = self::getNoticeId( $noticeName ); |
1334 | $dbw->newUpdateQueryBuilder() |
1335 | ->update( 'cn_assignments' ) |
1336 | ->set( [ 'tmp_weight' => $weight ] ) |
1337 | ->where( [ |
1338 | 'tmp_id' => $templateId, |
1339 | 'not_id' => $noticeId |
1340 | ] ) |
1341 | ->caller( __METHOD__ ) |
1342 | ->execute(); |
1343 | } |
1344 | |
1345 | /** |
1346 | * Updates the bucket of a banner in a campaign. Buckets alter what is shown to the end user |
1347 | * which can affect the relative weight of the banner in a campaign. |
1348 | * |
1349 | * @param string $noticeName Name of the campaign to update |
1350 | * @param int $templateId ID of the banner in the campaign |
1351 | * @param int $bucket New bucket number |
1352 | */ |
1353 | public static function updateBucket( $noticeName, $templateId, $bucket ) { |
1354 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1355 | $noticeId = self::getNoticeId( $noticeName ); |
1356 | $dbw->newUpdateQueryBuilder() |
1357 | ->update( 'cn_assignments' ) |
1358 | ->set( [ 'asn_bucket' => $bucket ] ) |
1359 | ->where( [ |
1360 | 'tmp_id' => $templateId, |
1361 | 'not_id' => $noticeId |
1362 | ] ) |
1363 | ->caller( __METHOD__ ) |
1364 | ->execute(); |
1365 | } |
1366 | |
1367 | // @todo FIXME: Unused. |
1368 | public static function updateProjectName( $notice, $projectName ) { |
1369 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1370 | $dbw->newUpdateQueryBuilder() |
1371 | ->update( 'cn_notices' ) |
1372 | ->set( [ 'not_project' => $projectName ] ) |
1373 | ->where( [ |
1374 | 'not_name' => $notice |
1375 | ] ) |
1376 | ->caller( __METHOD__ ) |
1377 | ->execute(); |
1378 | } |
1379 | |
1380 | public static function updateProjects( $notice, $newProjects ) { |
1381 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1382 | $dbw->startAtomic( __METHOD__ ); |
1383 | |
1384 | // Get the previously assigned projects |
1385 | $oldProjects = self::getNoticeProjects( $notice ); |
1386 | |
1387 | // Get the notice id |
1388 | $row = $dbw->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $notice ], __METHOD__ ); |
1389 | |
1390 | // Add newly assigned projects |
1391 | $addProjects = array_diff( $newProjects, $oldProjects ); |
1392 | $insertArray = []; |
1393 | foreach ( $addProjects as $project ) { |
1394 | $insertArray[] = [ 'np_notice_id' => $row->not_id, 'np_project' => $project ]; |
1395 | } |
1396 | if ( $insertArray ) { |
1397 | $dbw->newInsertQueryBuilder() |
1398 | ->insertInto( 'cn_notice_projects' ) |
1399 | ->ignore() |
1400 | ->rows( $insertArray ) |
1401 | ->caller( __METHOD__ ) |
1402 | ->execute(); |
1403 | } |
1404 | |
1405 | // Remove disassociated projects |
1406 | $removeProjects = array_diff( $oldProjects, $newProjects ); |
1407 | if ( $removeProjects ) { |
1408 | $dbw->newDeleteQueryBuilder() |
1409 | ->deleteFrom( 'cn_notice_projects' ) |
1410 | ->where( [ 'np_notice_id' => $row->not_id, 'np_project' => $removeProjects ] ) |
1411 | ->caller( __METHOD__ ) |
1412 | ->execute(); |
1413 | } |
1414 | |
1415 | $dbw->endAtomic( __METHOD__ ); |
1416 | } |
1417 | |
1418 | public static function updateProjectLanguages( $notice, $newLanguages ) { |
1419 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1420 | $dbw->startAtomic( __METHOD__ ); |
1421 | |
1422 | // Get the previously assigned languages |
1423 | $oldLanguages = self::getNoticeLanguages( $notice ); |
1424 | |
1425 | // Get the notice id |
1426 | $row = $dbw->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $notice ], __METHOD__ ); |
1427 | |
1428 | // Add newly assigned languages |
1429 | $addLanguages = array_diff( $newLanguages, $oldLanguages ); |
1430 | $insertArray = []; |
1431 | foreach ( $addLanguages as $code ) { |
1432 | $insertArray[] = [ 'nl_notice_id' => $row->not_id, 'nl_language' => $code ]; |
1433 | } |
1434 | if ( $insertArray ) { |
1435 | $dbw->newInsertQueryBuilder() |
1436 | ->insertInto( 'cn_notice_languages' ) |
1437 | ->ignore() |
1438 | ->rows( $insertArray ) |
1439 | ->caller( __METHOD__ ) |
1440 | ->execute(); |
1441 | } |
1442 | |
1443 | // Remove disassociated languages |
1444 | $removeLanguages = array_diff( $oldLanguages, $newLanguages ); |
1445 | if ( $removeLanguages ) { |
1446 | $dbw->newDeleteQueryBuilder() |
1447 | ->deleteFrom( 'cn_notice_languages' ) |
1448 | ->where( [ 'nl_notice_id' => $row->not_id, 'nl_language' => $removeLanguages ] ) |
1449 | ->caller( __METHOD__ ) |
1450 | ->execute(); |
1451 | } |
1452 | |
1453 | $dbw->endAtomic( __METHOD__ ); |
1454 | } |
1455 | |
1456 | /** |
1457 | * Update countries targeted for a campaign |
1458 | * @param string $notice |
1459 | * @param array $newCountries |
1460 | */ |
1461 | public static function updateCountries( $notice, $newCountries ) { |
1462 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1463 | $dbw->startAtomic( __METHOD__ ); |
1464 | |
1465 | // Get the previously assigned countries |
1466 | $oldCountries = self::getNoticeCountries( $notice ); |
1467 | |
1468 | // Get the notice id |
1469 | $row = $dbw->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $notice ], __METHOD__ ); |
1470 | |
1471 | // Add newly assigned countries |
1472 | $addCountries = array_diff( $newCountries, $oldCountries ); |
1473 | $insertArray = []; |
1474 | foreach ( $addCountries as $code ) { |
1475 | $insertArray[] = [ 'nc_notice_id' => $row->not_id, 'nc_country' => $code ]; |
1476 | } |
1477 | if ( $insertArray ) { |
1478 | $dbw->newInsertQueryBuilder() |
1479 | ->insertInto( 'cn_notice_countries' ) |
1480 | ->ignore() |
1481 | ->rows( $insertArray ) |
1482 | ->caller( __METHOD__ ) |
1483 | ->execute(); |
1484 | } |
1485 | |
1486 | // Remove disassociated countries |
1487 | $removeCountries = array_diff( $oldCountries, $newCountries ); |
1488 | if ( $removeCountries ) { |
1489 | $dbw->newDeleteQueryBuilder() |
1490 | ->deleteFrom( 'cn_notice_countries' ) |
1491 | ->where( [ 'nc_notice_id' => $row->not_id, 'nc_country' => $removeCountries ] ) |
1492 | ->caller( __METHOD__ ) |
1493 | ->execute(); |
1494 | } |
1495 | |
1496 | $dbw->endAtomic( __METHOD__ ); |
1497 | } |
1498 | |
1499 | /** |
1500 | * Update regions targeted for a campaign |
1501 | * @param string $notice |
1502 | * @param array $newRegions in format CountryCode_RegionCode |
1503 | */ |
1504 | public static function updateRegions( $notice, $newRegions ) { |
1505 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1506 | $dbw->startAtomic( __METHOD__ ); |
1507 | |
1508 | // Get the previously assigned regions |
1509 | $oldRegions = self::getNoticeRegions( $notice ); |
1510 | |
1511 | // Get the notice id |
1512 | $row = $dbw->selectRow( 'cn_notices', 'not_id', [ 'not_name' => $notice ], __METHOD__ ); |
1513 | |
1514 | // Add newly assigned regions |
1515 | $addRegions = array_diff( $newRegions, $oldRegions ); |
1516 | $insertArray = []; |
1517 | foreach ( $addRegions as $code ) { |
1518 | $insertArray[] = [ 'nr_notice_id' => $row->not_id, 'nr_region' => $code ]; |
1519 | } |
1520 | if ( $insertArray ) { |
1521 | $dbw->newInsertQueryBuilder() |
1522 | ->insertInto( 'cn_notice_regions' ) |
1523 | ->ignore() |
1524 | ->rows( $insertArray ) |
1525 | ->caller( __METHOD__ ) |
1526 | ->execute(); |
1527 | } |
1528 | |
1529 | // Remove disassociated regions |
1530 | $removeRegions = array_diff( $oldRegions, $newRegions ); |
1531 | if ( $removeRegions ) { |
1532 | $dbw->newDeleteQueryBuilder() |
1533 | ->deleteFrom( 'cn_notice_regions' ) |
1534 | ->where( [ 'nr_notice_id' => $row->not_id, 'nr_region' => $removeRegions ] ) |
1535 | ->caller( __METHOD__ ) |
1536 | ->execute(); |
1537 | } |
1538 | |
1539 | $dbw->endAtomic( __METHOD__ ); |
1540 | } |
1541 | |
1542 | /** |
1543 | * Log any changes related to a campaign |
1544 | * |
1545 | * @param string $action 'created', 'modified', or 'removed' |
1546 | * @param int $campaignId ID of the campaign |
1547 | * @param string $campaignName Name of the campaign |
1548 | * @param User $user User causing the change |
1549 | * @param array $beginSettings array of campaign settings before changes (optional). |
1550 | * If provided, it should include at least start, end, enabled and archived. |
1551 | * @param array $endSettings array of campaign settings after changes (optional). |
1552 | * If provided, it should include at least start, end, enabled and archived. |
1553 | * @param string|null $summary Change summary provided by the user |
1554 | */ |
1555 | public static function processAfterCampaignChange( |
1556 | $action, $campaignId, $campaignName, $user, $beginSettings = [], |
1557 | $endSettings = [], $summary = null |
1558 | ) { |
1559 | ChoiceDataProvider::invalidateCache(); |
1560 | |
1561 | // Summary shouldn't actually come in null, but just in case... |
1562 | if ( $summary === null ) { |
1563 | $summary = ''; |
1564 | } |
1565 | |
1566 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1567 | $time = $dbw->timestamp(); |
1568 | |
1569 | $log = [ |
1570 | 'notlog_timestamp' => $time, |
1571 | 'notlog_user_id' => $user->getId(), |
1572 | 'notlog_action' => $action, |
1573 | 'notlog_not_id' => $campaignId, |
1574 | 'notlog_not_name' => $campaignName, |
1575 | 'notlog_comment' => $summary, |
1576 | ]; |
1577 | |
1578 | foreach ( $beginSettings as $key => $value ) { |
1579 | if ( !self::settingNameIsValid( $key ) ) { |
1580 | throw new InvalidArgumentException( "Invalid setting name" ); |
1581 | } |
1582 | $log[ 'notlog_begin_' . $key ] = $value; |
1583 | } |
1584 | |
1585 | foreach ( $endSettings as $key => $value ) { |
1586 | if ( !self::settingNameIsValid( $key ) ) { |
1587 | throw new InvalidArgumentException( "Invalid setting name" ); |
1588 | } |
1589 | $log[ 'notlog_end_' . $key ] = $value; |
1590 | } |
1591 | |
1592 | ( new CentralNoticeHookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
1593 | ->onCentralNoticeCampaignChange( |
1594 | $action, |
1595 | $time, |
1596 | $campaignName, |
1597 | $user, |
1598 | self::processSettingsForHook( $beginSettings ), |
1599 | self::processSettingsForHook( $endSettings ), |
1600 | $summary |
1601 | ); |
1602 | |
1603 | // Only log the change if it is done by an actual user (rather than a testing script) |
1604 | // FIXME There must be a cleaner way to do this? |
1605 | if ( $user->getId() > 0 ) { // User::getID returns 0 for anonymous or non-existant users |
1606 | $dbw->newInsertQueryBuilder() |
1607 | ->insertInto( 'cn_notice_log' ) |
1608 | ->row( $log ) |
1609 | ->caller( __METHOD__ ) |
1610 | ->execute(); |
1611 | } |
1612 | } |
1613 | |
1614 | /** |
1615 | * Prepare campaign settings to be sent to the CampaignChange hook. This is necessary |
1616 | * since the settings provided to processAfterCampaignChange() are in a format |
1617 | * that is appropriate for the cn_notice_log table, but not for the hook. |
1618 | * |
1619 | * @param array $settings |
1620 | * @return array|null |
1621 | */ |
1622 | private static function processSettingsForHook( $settings ) { |
1623 | if ( !$settings ) { |
1624 | return null; |
1625 | } |
1626 | |
1627 | if ( isset( $settings[ 'banners' ] ) ) { |
1628 | $banners = json_decode( $settings[ 'banners' ] ); |
1629 | |
1630 | // This should never happen, since the string should just have been json-encoded |
1631 | // in getCampaignSettings(). |
1632 | if ( $banners === null ) { |
1633 | throw new UnexpectedValueException( 'Json decoding error for banner settings' ); |
1634 | } |
1635 | |
1636 | // Names of banners are object properties |
1637 | $banners = array_keys( (array)$banners ); |
1638 | |
1639 | } else { |
1640 | $banners = []; |
1641 | } |
1642 | |
1643 | return [ |
1644 | 'start' => $settings[ 'start' ], |
1645 | 'end' => $settings[ 'end' ], |
1646 | 'enabled' => (bool)$settings[ 'enabled' ], |
1647 | 'archived' => (bool)$settings[ 'archived' ], |
1648 | 'banners' => $banners, |
1649 | ]; |
1650 | } |
1651 | |
1652 | /** |
1653 | * Check that a string is a valid setting name. |
1654 | * @param string $settingName |
1655 | * @return bool |
1656 | */ |
1657 | private static function settingNameIsValid( $settingName ) { |
1658 | return ( preg_match( '/^[a-z_]*$/', $settingName ) === 1 ); |
1659 | } |
1660 | |
1661 | public static function setType( $campaignName, $type ) { |
1662 | // Following pattern from setNumericaCampaignSettings() and exiting with no |
1663 | // error if the campaign doesn't exist. TODO Is this right? |
1664 | if ( !self::campaignExists( $campaignName ) ) { |
1665 | return; |
1666 | } |
1667 | |
1668 | $dbw = CNDatabase::getDb( DB_PRIMARY ); |
1669 | $dbw->newUpdateQueryBuilder() |
1670 | ->update( 'cn_notices' ) |
1671 | ->set( [ 'not_type' => $type ] ) |
1672 | ->where( [ 'not_name' => $campaignName ] ) |
1673 | ->caller( __METHOD__ ) |
1674 | ->execute(); |
1675 | } |
1676 | |
1677 | public static function campaignLogs( |
1678 | $campaign = false, $username = false, $start = false, $end = false, $limit = 50, $offset = 0 |
1679 | ) { |
1680 | // Read from the primary database to avoid concurrency problems |
1681 | $dbr = CNDatabase::getDb(); |
1682 | $conds = []; |
1683 | if ( $start ) { |
1684 | $conds[] = "notlog_timestamp >= " . $dbr->addQuotes( $start ); |
1685 | } |
1686 | if ( $end ) { |
1687 | $conds[] = "notlog_timestamp < " . $dbr->addQuotes( $end ); |
1688 | } |
1689 | if ( $campaign ) { |
1690 | // This used to be a LIKE, but that was undocumented, |
1691 | // and filters prevented the % and \ character from being |
1692 | // used. The one character _ wildcard could have been used |
1693 | // from the api, but that was completely undocumented. |
1694 | // This was sketchy security wise, so the LIKE was removed. |
1695 | $conds["notlog_not_name"] = $campaign; |
1696 | } |
1697 | if ( $username ) { |
1698 | $user = User::newFromName( $username ); |
1699 | if ( $user ) { |
1700 | $conds["notlog_user_id"] = $user->getId(); |
1701 | } |
1702 | } |
1703 | |
1704 | $res = $dbr->select( 'cn_notice_log', '*', $conds, |
1705 | __METHOD__, |
1706 | [ |
1707 | "ORDER BY" => "notlog_timestamp DESC", |
1708 | "LIMIT" => $limit, |
1709 | "OFFSET" => $offset, |
1710 | ] |
1711 | ); |
1712 | $logs = []; |
1713 | foreach ( $res as $row ) { |
1714 | $entry = new CampaignLog( $row ); |
1715 | $logs[] = array_merge( get_object_vars( $entry ), $entry->changes() ); |
1716 | } |
1717 | return $logs; |
1718 | } |
1719 | } |