Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 97 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
CampaignStats | |
0.00% |
0 / 97 |
|
0.00% |
0 / 5 |
306 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getStatsForRecord | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getStatsForRecords | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
90 | |||
getUploadedMedia | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
getSummaryCounts | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\MediaUploader\Campaign; |
4 | |
5 | use MediaWiki\Extension\MediaUploader\Config\RawConfig; |
6 | use Title; |
7 | use WANObjectCache; |
8 | use Wikimedia\Rdbms\Database; |
9 | use Wikimedia\Rdbms\IConnectionProvider; |
10 | use Wikimedia\Rdbms\IReadableDatabase; |
11 | |
12 | /** |
13 | * Facility for retrieving statistics about campaigns. |
14 | */ |
15 | class CampaignStats { |
16 | |
17 | private IConnectionProvider $dbProvider; |
18 | |
19 | /** @var WANObjectCache */ |
20 | private $cache; |
21 | |
22 | /** @var RawConfig */ |
23 | private $rawConfig; |
24 | |
25 | /** |
26 | * CampaignStats constructor. |
27 | * |
28 | * @param IConnectionProvider $dbProvider |
29 | * @param WANObjectCache $cache |
30 | * @param RawConfig $rawConfig |
31 | * |
32 | * @internal Only for use by ServiceWiring |
33 | */ |
34 | public function __construct( |
35 | IConnectionProvider $dbProvider, |
36 | WANObjectCache $cache, |
37 | RawConfig $rawConfig |
38 | ) { |
39 | $this->dbProvider = $dbProvider; |
40 | $this->cache = $cache; |
41 | $this->rawConfig = $rawConfig; |
42 | } |
43 | |
44 | /** |
45 | * Get stats for a single record. |
46 | * @see CampaignStats::getStatsForRecords() |
47 | * |
48 | * @param CampaignRecord $record |
49 | * |
50 | * @return array|null stats record or null on failure |
51 | */ |
52 | public function getStatsForRecord( CampaignRecord $record ): ?array { |
53 | $stats = $this->getStatsForRecords( [ $record ] ); |
54 | return $stats[$record->getPageId() ?: -1] ?? null; |
55 | } |
56 | |
57 | /** |
58 | * The records must have their titles populated. Otherwise, this will |
59 | * throw an exception. |
60 | * |
61 | * @param CampaignRecord[] $records |
62 | * |
63 | * @return array Map: campaign ID => stats array. |
64 | * Keys: 'trackingCategory': string (DB key) |
65 | * 'uploadedMediaCount': int |
66 | * 'contributorsCount': int |
67 | * 'uploadedMedia': string[] (DB keys of max. 24 files) |
68 | */ |
69 | public function getStatsForRecords( array $records ): array { |
70 | $recordMap = []; |
71 | foreach ( $records as $record ) { |
72 | // Note: the page ID won't actually ever be null, but it's hard to |
73 | // convince Phan to this. |
74 | $recordMap[$record->getPageId() ?: -1] = $record; |
75 | } |
76 | |
77 | $cache = $this->cache; |
78 | $keys = $cache->makeMultiKeys( |
79 | array_keys( $recordMap ), |
80 | static function ( $id ) use ( $cache ) { |
81 | return $cache->makeKey( |
82 | 'mediauploader', |
83 | 'campaign-stats', |
84 | $id |
85 | ); |
86 | } |
87 | ); |
88 | |
89 | $fromCache = $cache->getMultiWithUnionSetCallback( |
90 | $keys, |
91 | $this->rawConfig->getSetting( 'campaignStatsMaxAge' ), |
92 | function ( array $ids, array &$ttls, array &$setOpts ) use ( $recordMap ) { |
93 | $db = $this->dbProvider->getReplicaDatabase(); |
94 | $setOpts += Database::getCacheSetOptions( $db ); |
95 | |
96 | // Construct a tracking category => id map |
97 | $catToId = []; |
98 | // id => tracking category or null |
99 | $idToCat = []; |
100 | foreach ( $ids as $id ) { |
101 | $catTitle = Title::newFromText( |
102 | $recordMap[$id]->getTrackingCategoryName( $this->rawConfig ), |
103 | NS_CATEGORY |
104 | ); |
105 | |
106 | if ( $catTitle === null ) { |
107 | $idToCat[$id] = null; |
108 | } else { |
109 | $idToCat[$id] = $catTitle->getDBkey(); |
110 | $catToId[$catTitle->getDBkey()] = $id; |
111 | } |
112 | } |
113 | |
114 | // Do the batch queries |
115 | if ( $catToId ) { |
116 | $summary = $this->getSummaryCounts( $db, $catToId ); |
117 | $media = $this->getUploadedMedia( $db, $catToId ); |
118 | } |
119 | |
120 | // Aggregate results |
121 | $toCache = []; |
122 | foreach ( $idToCat as $id => $category ) { |
123 | if ( $category === null ) { |
124 | $toCache[$id] = null; |
125 | } else { |
126 | $toCache[$id] = [ |
127 | 'trackingCategory' => $category, |
128 | 'uploadedMediaCount' => $summary[$id][0] ?? 0, |
129 | 'contributorsCount' => $summary[$id][1] ?? 0, |
130 | 'uploadedMedia' => $media[$id] ?? [], |
131 | ]; |
132 | } |
133 | } |
134 | |
135 | return $toCache; |
136 | } |
137 | ); |
138 | |
139 | $result = []; |
140 | foreach ( $fromCache as $key => $item ) { |
141 | $result[$keys[$key]] = $item; |
142 | } |
143 | return $result; |
144 | } |
145 | |
146 | /** |
147 | * @param IReadableDatabase $db |
148 | * @param int[] $categories Map: campaign DB key => campaign page ID |
149 | * |
150 | * @return string[][] campaign ID => string[], the strings are filenames |
151 | */ |
152 | private function getUploadedMedia( IReadableDatabase $db, array $categories ): array { |
153 | $result = $db->newSelectQueryBuilder() |
154 | ->table( 'categorylinks' ) |
155 | ->fields( [ 'cl_to', 'page_namespace', 'page_title' ] ) |
156 | ->where( [ |
157 | 'cl_to' => array_keys( $categories ), |
158 | 'cl_type' => 'file', |
159 | ] ) |
160 | ->join( 'page', null, 'cl_from=page_id' ) |
161 | ->orderBy( 'cl_timestamp', 'DESC' ) |
162 | ->useIndex( [ 'categorylinks' => 'cl_timestamp' ] ) |
163 | // Old, arbitrary limit. Seems fine. |
164 | ->limit( 24 ) |
165 | ->fetchResultSet(); |
166 | |
167 | $grouped = []; |
168 | foreach ( $result as $row ) { |
169 | $key = $categories[$row->cl_to]; |
170 | |
171 | if ( array_key_exists( $key, $grouped ) ) { |
172 | $grouped[$key][] = $row->page_title; |
173 | } else { |
174 | $grouped[$key] = [ $row->page_title ]; |
175 | } |
176 | } |
177 | |
178 | return $grouped; |
179 | } |
180 | |
181 | /** |
182 | * @param IReadableDatabase $db |
183 | * @param int[] $categories Map: campaign DB key => campaign page ID |
184 | * |
185 | * @return array Map: campaign ID => [ media count, contributor count ] |
186 | */ |
187 | private function getSummaryCounts( IReadableDatabase $db, array $categories ): array { |
188 | $result = $db->newSelectQueryBuilder() |
189 | ->table( 'categorylinks' ) |
190 | ->field( 'cl_to' ) |
191 | ->field( 'COUNT(DISTINCT img_actor)', 'contributors' ) |
192 | ->field( 'COUNT(cl_from)', 'media' ) |
193 | ->where( [ |
194 | 'cl_to' => array_keys( $categories ), |
195 | 'cl_type' => 'file', |
196 | ] ) |
197 | ->join( 'page', null, 'cl_from=page_id' ) |
198 | ->join( 'image', null, 'page_title=img_name' ) |
199 | ->groupBy( 'cl_to' ) |
200 | ->useIndex( [ 'categorylinks' => 'cl_timestamp' ] ) |
201 | ->fetchResultSet(); |
202 | |
203 | $map = []; |
204 | foreach ( $result as $row ) { |
205 | $map[$categories[$row->cl_to]] = [ |
206 | intval( $row->media ), |
207 | intval( $row->contributors ), |
208 | ]; |
209 | } |
210 | return $map; |
211 | } |
212 | } |