Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 104 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
GlobalUsageQuery | |
0.00% |
0 / 104 |
|
0.00% |
0 / 15 |
1406 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
setOffset | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getOffsetString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isReversed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getContinueString | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
filterLocal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
filterNamespaces | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
filterSites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
272 | |||
getResult | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSingleImageResult | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
hasMore | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
count | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\GlobalUsage; |
4 | |
5 | use MediaWiki\Title\Title; |
6 | use MediaWiki\WikiMap\WikiMap; |
7 | use Wikimedia\Rdbms\IDatabase; |
8 | use Wikimedia\Rdbms\SelectQueryBuilder; |
9 | |
10 | /** |
11 | * A helper class to query the globalimagelinks table |
12 | * |
13 | */ |
14 | class GlobalUsageQuery { |
15 | private $limit = 50; |
16 | private $offset; |
17 | private $hasMore = false; |
18 | private $filterLocal = false; |
19 | private $result; |
20 | private $reversed = false; |
21 | |
22 | /** @var int[] namespace ID(s) desired */ |
23 | private $filterNamespaces; |
24 | |
25 | /** @var string[] sites desired */ |
26 | private $filterSites; |
27 | |
28 | /** |
29 | * @var Title|array |
30 | */ |
31 | private $target; |
32 | |
33 | private $lastRow; |
34 | |
35 | /** |
36 | * @var IDatabase |
37 | */ |
38 | private $db; |
39 | |
40 | /** |
41 | * @param mixed $target Title or array of db keys of target(s). |
42 | * If a title, can be a category or a file |
43 | */ |
44 | public function __construct( $target ) { |
45 | $this->db = GlobalUsage::getGlobalDB( DB_REPLICA ); |
46 | if ( $target instanceof Title ) { |
47 | $this->target = $target; |
48 | } elseif ( is_array( $target ) ) { |
49 | // List of files to query |
50 | $this->target = $target; |
51 | } else { |
52 | $this->target = Title::makeTitleSafe( NS_FILE, $target ); |
53 | } |
54 | $this->offset = []; |
55 | } |
56 | |
57 | /** |
58 | * Set the offset parameter |
59 | * |
60 | * @param string $offset offset |
61 | * @param bool|null $reversed True if this is the upper offset |
62 | * @return bool |
63 | */ |
64 | public function setOffset( $offset, $reversed = null ) { |
65 | if ( $reversed !== null ) { |
66 | $this->reversed = $reversed; |
67 | } |
68 | |
69 | if ( !is_array( $offset ) ) { |
70 | $offset = explode( '|', $offset ); |
71 | } |
72 | |
73 | if ( count( $offset ) == 3 ) { |
74 | $this->offset = $offset; |
75 | return true; |
76 | } else { |
77 | return false; |
78 | } |
79 | } |
80 | |
81 | /** |
82 | * Return the offset set by the user |
83 | * |
84 | * @return string offset |
85 | */ |
86 | public function getOffsetString() { |
87 | return implode( '|', $this->offset ); |
88 | } |
89 | |
90 | /** |
91 | * Is the result reversed |
92 | * |
93 | * @return bool |
94 | */ |
95 | public function isReversed() { |
96 | return $this->reversed; |
97 | } |
98 | |
99 | /** |
100 | * Returns the string used for continuation |
101 | * |
102 | * @return string |
103 | * |
104 | */ |
105 | public function getContinueString() { |
106 | if ( $this->hasMore() ) { |
107 | return "{$this->lastRow->gil_to}|{$this->lastRow->gil_wiki}|{$this->lastRow->gil_page}"; |
108 | } else { |
109 | return ''; |
110 | } |
111 | } |
112 | |
113 | /** |
114 | * Set the maximum amount of items to return. Capped at 500. |
115 | * |
116 | * @param int $limit The limit |
117 | */ |
118 | public function setLimit( $limit ) { |
119 | $this->limit = min( $limit, 500 ); |
120 | } |
121 | |
122 | /** |
123 | * Returns the user set limit |
124 | * @return int |
125 | */ |
126 | public function getLimit() { |
127 | return $this->limit; |
128 | } |
129 | |
130 | /** |
131 | * Set whether to filter out the local usage |
132 | * @param bool $value |
133 | */ |
134 | public function filterLocal( $value = true ) { |
135 | $this->filterLocal = $value; |
136 | } |
137 | |
138 | /** |
139 | * Return results only for these namespaces. |
140 | * @param int[] $namespaces numeric namespace IDs |
141 | */ |
142 | public function filterNamespaces( $namespaces ) { |
143 | $this->filterNamespaces = $namespaces; |
144 | } |
145 | |
146 | /** |
147 | * Return results only for these sites. |
148 | * @param string[] $sites wiki site names |
149 | */ |
150 | public function filterSites( $sites ) { |
151 | $this->filterSites = $sites; |
152 | } |
153 | |
154 | /** |
155 | * Executes the query |
156 | */ |
157 | public function execute() { |
158 | /* Construct the SQL query */ |
159 | $queryBuilder = $this->db->newSelectQueryBuilder() |
160 | ->select( [ |
161 | 'gil_to', |
162 | 'gil_wiki', |
163 | 'gil_page', |
164 | 'gil_page_namespace_id', |
165 | 'gil_page_namespace', |
166 | 'gil_page_title' |
167 | ] ) |
168 | ->from( 'globalimagelinks' ) |
169 | // Select an extra row to check whether we have more rows available |
170 | ->limit( $this->limit + 1 ) |
171 | ->caller( __METHOD__ ); |
172 | |
173 | // Add target image(s) |
174 | if ( is_array( $this->target ) ) { // array of dbkey strings |
175 | $namespace = NS_FILE; |
176 | $queryIn = $this->target; |
177 | } else { // a Title object |
178 | $namespace = $this->target->getNamespace(); |
179 | $queryIn = $this->target->getDbKey(); |
180 | } |
181 | switch ( $namespace ) { |
182 | case NS_FILE: |
183 | $queryBuilder->where( [ 'gil_to' => $queryIn ] ); |
184 | break; |
185 | case NS_CATEGORY: |
186 | $queryBuilder->join( 'categorylinks', null, 'page_id = cl_from' ); |
187 | $queryBuilder->join( 'page', null, 'page_title = gil_to' ); |
188 | $queryBuilder->where( [ |
189 | 'cl_to' => $queryIn, |
190 | 'page_namespace' => NS_FILE, |
191 | ] ); |
192 | break; |
193 | default: |
194 | return; |
195 | } |
196 | |
197 | if ( $this->filterLocal ) { |
198 | // Don't show local file usage |
199 | $queryBuilder->andWhere( $this->db->expr( 'gil_wiki', '!=', WikiMap::getCurrentWikiId() ) ); |
200 | } |
201 | |
202 | if ( $this->filterNamespaces ) { |
203 | $queryBuilder->andWhere( [ 'gil_page_namespace_id' => $this->filterNamespaces ] ); |
204 | } |
205 | |
206 | if ( $this->filterSites ) { |
207 | $queryBuilder->andWhere( [ 'gil_wiki' => $this->filterSites ] ); |
208 | } |
209 | |
210 | // Set the continuation condition |
211 | if ( $this->offset ) { |
212 | // Check which limit we got in order to determine which way to traverse rows |
213 | if ( $this->reversed ) { |
214 | // Reversed traversal; do not include offset row |
215 | $op = '<'; |
216 | $queryBuilder->orderBy( [ 'gil_to', 'gil_wiki', 'gil_page' ], SelectQueryBuilder::SORT_DESC ); |
217 | } else { |
218 | // Normal traversal; include offset row |
219 | $op = '>='; |
220 | } |
221 | |
222 | $queryBuilder->andWhere( $this->db->buildComparison( $op, [ |
223 | 'gil_to' => $this->offset[0], |
224 | 'gil_wiki' => $this->offset[1], |
225 | 'gil_page' => intval( $this->offset[2] ), |
226 | ] ) ); |
227 | } |
228 | |
229 | $res = $queryBuilder->fetchResultSet(); |
230 | |
231 | /* Process result */ |
232 | // Always return the result in the same order; regardless whether reversed was specified |
233 | // reversed is really only used to determine from which direction the offset is |
234 | $rows = []; |
235 | $count = 0; |
236 | $this->hasMore = false; |
237 | foreach ( $res as $row ) { |
238 | $count++; |
239 | if ( $count > $this->limit ) { |
240 | // We've reached the extra row that indicates that there are more rows |
241 | $this->hasMore = true; |
242 | $this->lastRow = $row; |
243 | break; |
244 | } |
245 | $rows[] = $row; |
246 | } |
247 | if ( $this->reversed ) { |
248 | $rows = array_reverse( $rows ); |
249 | } |
250 | |
251 | // Build the result array |
252 | $this->result = []; |
253 | foreach ( $rows as $row ) { |
254 | if ( !isset( $this->result[$row->gil_to] ) ) { |
255 | $this->result[$row->gil_to] = []; |
256 | } |
257 | if ( !isset( $this->result[$row->gil_to][$row->gil_wiki] ) ) { |
258 | $this->result[$row->gil_to][$row->gil_wiki] = []; |
259 | } |
260 | |
261 | $this->result[$row->gil_to][$row->gil_wiki][] = [ |
262 | 'image' => $row->gil_to, |
263 | 'id' => $row->gil_page, |
264 | 'namespace_id' => $row->gil_page_namespace_id, |
265 | 'namespace' => $row->gil_page_namespace, |
266 | 'title' => $row->gil_page_title, |
267 | 'wiki' => $row->gil_wiki, |
268 | ]; |
269 | } |
270 | } |
271 | |
272 | /** |
273 | * Returns the result set. The result is a 4 dimensional array |
274 | * (file, wiki, page), whose items are arrays with keys: |
275 | * - image: File name |
276 | * - id: Page id |
277 | * - namespace: Page namespace text |
278 | * - title: Unprefixed page title |
279 | * - wiki: Wiki id |
280 | * |
281 | * @return array Result set |
282 | */ |
283 | public function getResult() { |
284 | return $this->result; |
285 | } |
286 | |
287 | /** |
288 | * Returns a 3 dimensional array with the result of the first file. Useful |
289 | * if only one image was queried. |
290 | * |
291 | * For further information see documentation of getResult() |
292 | * |
293 | * @return array Result set |
294 | */ |
295 | public function getSingleImageResult() { |
296 | if ( $this->result ) { |
297 | return current( $this->result ); |
298 | } else { |
299 | return []; |
300 | } |
301 | } |
302 | |
303 | /** |
304 | * Returns whether there are more results |
305 | * |
306 | * @return bool |
307 | */ |
308 | public function hasMore() { |
309 | return $this->hasMore; |
310 | } |
311 | |
312 | /** |
313 | * Returns the result length |
314 | * |
315 | * @return int |
316 | */ |
317 | public function count() { |
318 | return count( $this->result ); |
319 | } |
320 | } |