Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
GlobalUsage
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 11
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 insertLinks
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 getLinksFromPage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 deleteLinksFromPage
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 deleteLinksToFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 copyLocalImagelinks
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
12
 moveTo
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 redirectSpecialPageToSharedRepo
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 onSharedRepo
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getWantedFilesQueryInfo
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
6
 getGlobalDB
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\GlobalUsage;
4
5use MediaWiki\Context\IContextSource;
6use MediaWiki\Deferred\DeferredUpdates;
7use MediaWiki\MediaWikiServices;
8use MediaWiki\Title\Title;
9use MediaWiki\WikiMap\WikiMap;
10use Wikimedia\Rdbms\IDatabase;
11use Wikimedia\Rdbms\IDBAccessObject;
12
13class GlobalUsage {
14    /** @var string */
15    private $interwiki;
16
17    /**
18     * @var IDatabase
19     */
20    private $dbw;
21
22    /**
23     * @var IDatabase
24     */
25    private $dbr;
26
27    /**
28     * Construct a GlobalUsage instance for a certain wiki.
29     *
30     * @param string $interwiki Interwiki prefix of the wiki
31     * @param IDatabase $dbw Database object for write (primary)
32     * @param IDatabase $dbr Database object for read (replica)
33     */
34    public function __construct( $interwiki, IDatabase $dbw, IDatabase $dbr ) {
35        $this->interwiki = $interwiki;
36        $this->dbw = $dbw;
37        $this->dbr = $dbr;
38    }
39
40    /**
41     * Sets the images used by a certain page
42     *
43     * @param Title $title Title of the page
44     * @param string[] $images Array of db keys of images used
45     * @param int $pageIdFlags
46     * @param int|null $ticket
47     */
48    public function insertLinks(
49        Title $title, array $images, $pageIdFlags = IDBAccessObject::READ_LATEST, $ticket = null
50    ) {
51        global $wgUpdateRowsPerQuery;
52
53        $insert = [];
54        foreach ( $images as $name ) {
55            $insert[] = [
56                'gil_wiki' => $this->interwiki,
57                'gil_page' => $title->getArticleID( $pageIdFlags ),
58                'gil_page_namespace_id' => $title->getNamespace(),
59                'gil_page_namespace' => $title->getNsText(),
60                'gil_page_title' => $title->getDBkey(),
61                'gil_to' => $name
62            ];
63        }
64
65        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
66        $ticket = $ticket ?: $lbFactory->getEmptyTransactionTicket( __METHOD__ );
67        $insertBatches = array_chunk( $insert, $wgUpdateRowsPerQuery );
68        foreach ( $insertBatches as $insertBatch ) {
69            $this->dbw->newInsertQueryBuilder()
70                ->insertInto( 'globalimagelinks' )
71                ->ignore()
72                ->rows( $insertBatch )
73                ->caller( __METHOD__ )
74                ->execute();
75            if ( count( $insertBatches ) > 1 ) {
76                $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
77            }
78        }
79    }
80
81    /**
82     * Get all global images from a certain page
83     * @param int $id
84     * @return string[]
85     */
86    public function getLinksFromPage( $id ) {
87        return $this->dbr->newSelectQueryBuilder()
88            ->select( 'gil_to' )
89            ->from( 'globalimagelinks' )
90            ->where( [
91                'gil_wiki' => $this->interwiki,
92                'gil_page' => $id,
93            ] )
94            ->caller( __METHOD__ )
95            ->fetchFieldValues();
96    }
97
98    /**
99     * Deletes all entries from a certain page to certain files
100     *
101     * @param int $id Page id of the page
102     * @param string[]|null $to File name(s)
103     * @param int|null $ticket
104     */
105    public function deleteLinksFromPage( $id, ?array $to = null, $ticket = null ) {
106        global $wgUpdateRowsPerQuery;
107
108        $where = [
109            'gil_wiki' => $this->interwiki,
110            'gil_page' => $id
111        ];
112        if ( $to ) {
113            $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
114            $ticket = $ticket ?: $lbFactory->getEmptyTransactionTicket( __METHOD__ );
115            foreach ( array_chunk( $to, $wgUpdateRowsPerQuery ) as $toBatch ) {
116                $where['gil_to'] = $toBatch;
117                $this->dbw->newDeleteQueryBuilder()
118                    ->deleteFrom( 'globalimagelinks' )
119                    ->where( $where )
120                    ->caller( __METHOD__ )
121                    ->execute();
122                $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
123            }
124        } else {
125            $this->dbw->newDeleteQueryBuilder()
126                ->deleteFrom( 'globalimagelinks' )
127                ->where( $where )
128                ->caller( __METHOD__ )
129                ->execute();
130        }
131    }
132
133    /**
134     * Deletes all entries to a certain image
135     *
136     * @param Title $title Title of the file
137     */
138    public function deleteLinksToFile( $title ) {
139        $this->dbw->newDeleteQueryBuilder()
140            ->deleteFrom( 'globalimagelinks' )
141            ->where( [
142                'gil_wiki' => $this->interwiki,
143                'gil_to' => $title->getDBkey()
144            ] )
145            ->caller( __METHOD__ )
146            ->execute();
147    }
148
149    /**
150     * Copy local links to global table
151     *
152     * @param Title $title Title of the file to copy entries from.
153     * @param IDatabase $localDbr Database object for reading the local links from
154     */
155    public function copyLocalImagelinks( Title $title, IDatabase $localDbr ) {
156        $res = $localDbr->newSelectQueryBuilder()
157            ->select( [ 'il_to', 'page_id', 'page_namespace', 'page_title' ] )
158            ->from( 'imagelinks' )
159            ->join( 'page', null, 'il_from = page_id' )
160            ->where( [ 'il_to' => $title->getDBkey() ] )
161            ->caller( __METHOD__ )
162            ->fetchResultSet();
163
164        if ( !$res->numRows() ) {
165            return;
166        }
167
168        $insert = [];
169        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
170        foreach ( $res as $row ) {
171            $insert[] = [
172                'gil_wiki' => $this->interwiki,
173                'gil_page' => $row->page_id,
174                'gil_page_namespace_id' => $row->page_namespace,
175                'gil_page_namespace' => $contLang->getNsText( $row->page_namespace ),
176                'gil_page_title' => $row->page_title,
177                'gil_to' => $row->il_to,
178            ];
179        }
180
181        $fname = __METHOD__;
182        DeferredUpdates::addCallableUpdate( function () use ( $insert, $fname ) {
183            $this->dbw->newInsertQueryBuilder()
184                ->insertInto( 'globalimagelinks' )
185                ->ignore()
186                ->rows( $insert )
187                ->caller( $fname )
188                ->execute();
189        } );
190    }
191
192    /**
193     * Changes the page title
194     *
195     * @param int $id Page id of the page
196     * @param Title $title New title of the page
197     */
198    public function moveTo( $id, $title ) {
199        $this->dbw->newUpdateQueryBuilder()
200            ->update( 'globalimagelinks' )
201            ->set( [
202                'gil_page_namespace_id' => $title->getNamespace(),
203                'gil_page_namespace' => $title->getNsText(),
204                'gil_page_title' => $title->getDBkey()
205            ] )
206            ->where( [
207                'gil_wiki' => $this->interwiki,
208                'gil_page' => $id
209            ] )
210            ->caller( __METHOD__ )
211            ->execute();
212    }
213
214    /**
215     * Utility function to redirect special pages that are only on the shared repo
216     *
217     * Putting here as this can be useful in multiple special page classes.
218     * This redirects the current page to the same page on the shared repo
219     * wiki, making sure to use the english name of the special page, in case the
220     * current wiki uses something other than english for its content language.
221     *
222     * @param IContextSource $context $this->getContext() from the special page.
223     */
224    public static function redirectSpecialPageToSharedRepo( IContextSource $context ) {
225        global $wgGlobalUsageSharedRepoWiki;
226        // Make sure to get the "canonical" page name, and not a translation.
227        $titleText = $context->getTitle()->getDBkey();
228        $services = MediaWikiServices::getInstance();
229        [ $canonicalName, $subpage ] = $services->getSpecialPageFactory()->resolveAlias( $titleText );
230        $canonicalName = $services->getNamespaceInfo()->getCanonicalName( NS_SPECIAL ) . ':' . $canonicalName;
231        if ( $subpage !== null ) {
232            $canonicalName .= '/' . $subpage;
233        }
234
235        $url = WikiMap::getForeignURL( $wgGlobalUsageSharedRepoWiki, $canonicalName );
236        if ( $url !== false ) {
237            // We have a url
238            $args = $context->getRequest()->getQueryValues();
239            unset( $args['title'] );
240            $url = wfAppendQuery( $url, $args );
241
242            $context->getOutput()->redirect( $url );
243        } else {
244            // WikiMap can't find the url for the shared repo.
245            // Just pretend we don't exist in this case.
246            $context->getOutput()->setStatusCode( 404 );
247            $context->getOutput()->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' );
248        }
249    }
250
251    /**
252     * Are we currently on the shared repo? (Utility function)
253     *
254     * @note This assumes the user has a single shared repo. If the user has
255     *   multiple/nested foreign repos, then its unclear what it means to
256     *   be on the "shared repo". See discussion on bug 23136.
257     * @return bool
258     */
259    public static function onSharedRepo() {
260        global $wgGlobalUsageSharedRepoWiki, $wgGlobalUsageDatabase;
261        if ( !$wgGlobalUsageSharedRepoWiki ) {
262            // backwards compatability with settings from before $wgGlobalUsageSharedRepoWiki
263            // was introduced.
264            return $wgGlobalUsageDatabase === WikiMap::getCurrentWikiId() || !$wgGlobalUsageDatabase;
265        } else {
266            return $wgGlobalUsageSharedRepoWiki === WikiMap::getCurrentWikiId();
267        }
268    }
269
270    /**
271     * Query info for getting wanted files using global image links
272     *
273     * Adding a utility method here, as this same query is used in
274     * two different special page classes.
275     *
276     * @param string|bool $wiki
277     * @return array Query info array, as a QueryPage would expect.
278     */
279    public static function getWantedFilesQueryInfo( $wiki = false ) {
280        $qi = [
281            'tables' => [
282                'globalimagelinks',
283                'page',
284                'redirect',
285                'img1' => 'image',
286                'img2' => 'image',
287            ],
288            'fields' => [
289                'namespace' => NS_FILE,
290                'title' => 'gil_to',
291                'value' => 'COUNT(*)'
292            ],
293            'conds' => [
294                'img1.img_name' => null,
295                // We also need to exclude file redirects
296                'img2.img_name' => null,
297             ],
298            'options' => [ 'GROUP BY' => 'gil_to' ],
299            'join_conds' => [
300                'img1' => [ 'LEFT JOIN',
301                    'gil_to = img1.img_name'
302                ],
303                'page' => [ 'LEFT JOIN', [
304                    'gil_to = page_title',
305                    'page_namespace' => NS_FILE,
306                ] ],
307                'redirect' => [ 'LEFT JOIN', [
308                    'page_id = rd_from',
309                    'rd_namespace' => NS_FILE,
310                    'rd_interwiki' => ''
311                ] ],
312                'img2' => [ 'LEFT JOIN',
313                    'rd_title = img2.img_name'
314                ]
315            ]
316        ];
317        if ( $wiki !== false ) {
318            // Limit to just one wiki.
319            $qi['conds']['gil_wiki'] = $wiki;
320        }
321
322        return $qi;
323    }
324
325    /**
326     * @param int $index DB_PRIMARY/DB_REPLICA
327     * @param array $groups
328     * @return IDatabase
329     */
330    public static function getGlobalDB( $index, $groups = [] ) {
331        global $wgGlobalUsageDatabase;
332
333        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
334        $lb = $lbFactory->getMainLB( $wgGlobalUsageDatabase );
335
336        return $lb->getConnection( $index, [], $wgGlobalUsageDatabase );
337    }
338}