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 Wikimedia\Rdbms\IDatabase;
8use Wikimedia\Rdbms\SelectQueryBuilder;
9
10/**
11 * A helper class to query the globalimagelinks table
12 *
13 */
14class 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}