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