Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.39% covered (success)
92.39%
85 / 92
85.71% covered (warning)
85.71%
12 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikiMap
93.41% covered (success)
93.41%
85 / 91
85.71% covered (warning)
85.71%
12 / 14
40.46
0.00% covered (danger)
0.00%
0 / 1
 getWiki
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getWikiReferenceFromWgConf
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 getWikiWikiReferenceFromSites
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
7.39
 getWikiName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 foreignUserLink
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeForeignLink
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getForeignURL
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getCanonicalServerInfoForAllWikis
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 getWikiFromUrl
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
6.29
 getWikiIdFromDbDomain
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getCurrentWikiDbDomain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCurrentWikiDbDomain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCurrentWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\WikiMap;
22
23use MediaWiki\Linker\Linker;
24use MediaWiki\MediaWikiServices;
25use MediaWiki\Site\MediaWikiSite;
26use Wikimedia\Rdbms\DatabaseDomain;
27
28/**
29 * Tools for dealing with other locally-hosted wikis.
30 */
31class WikiMap {
32
33    /**
34     * Get a WikiReference object for $wikiID
35     *
36     * @param string $wikiID Wiki'd id (generally database name)
37     * @return WikiReference|null WikiReference object or null if the wiki was not found
38     */
39    public static function getWiki( $wikiID ) {
40        $wikiReference = self::getWikiReferenceFromWgConf( $wikiID );
41        if ( $wikiReference ) {
42            return $wikiReference;
43        }
44
45        // Try sites, if $wgConf failed
46        return self::getWikiWikiReferenceFromSites( $wikiID );
47    }
48
49    /**
50     * @param string $wikiID
51     * @return WikiReference|null WikiReference object or null if the wiki was not found
52     */
53    private static function getWikiReferenceFromWgConf( $wikiID ) {
54        global $wgConf;
55        '@phan-var \MediaWiki\Config\SiteConfiguration $wgConf';
56
57        $wgConf->loadFullData();
58
59        [ $major, $minor ] = $wgConf->siteFromDB( $wikiID );
60        if ( $major === null ) {
61            return null;
62        }
63        $server = $wgConf->get( 'wgServer', $wikiID, $major,
64            [ 'lang' => $minor, 'site' => $major ] );
65
66        $canonicalServer = $wgConf->get( 'wgCanonicalServer', $wikiID, $major,
67            [ 'lang' => $minor, 'site' => $major ] );
68        if ( $canonicalServer === false || $canonicalServer === null ) {
69            $canonicalServer = $server;
70        }
71
72        $path = $wgConf->get( 'wgArticlePath', $wikiID, $major,
73            [ 'lang' => $minor, 'site' => $major ] );
74
75        // If we don't have a canonical server or a path containing $1, the
76        // WikiReference isn't going to function properly. Just return null in
77        // that case.
78        if ( !is_string( $canonicalServer ) || !is_string( $path ) || strpos( $path, '$1' ) === false ) {
79            return null;
80        }
81
82        return new WikiReference( $canonicalServer, $path, $server );
83    }
84
85    /**
86     * @param string $wikiID
87     * @return WikiReference|null WikiReference object or null if the wiki was not found
88     */
89    private static function getWikiWikiReferenceFromSites( $wikiID ) {
90        $siteLookup = MediaWikiServices::getInstance()->getSiteLookup();
91        $site = $siteLookup->getSite( $wikiID );
92
93        if ( !$site instanceof MediaWikiSite ) {
94            // Abort if not a MediaWikiSite, as this is about Wikis
95            return null;
96        }
97
98        $urlParts = wfParseUrl( $site->getPageUrl() );
99        if ( $urlParts === false || !isset( $urlParts['path'] ) || !isset( $urlParts['host'] ) ) {
100            // We can't create a meaningful WikiReference without URLs
101            return null;
102        }
103
104        // XXX: Check whether path contains a $1?
105        $path = $urlParts['path'];
106        if ( isset( $urlParts['query'] ) ) {
107            $path .= '?' . $urlParts['query'];
108        }
109
110        $canonicalServer = $urlParts['scheme'] ?? 'http';
111        $canonicalServer .= '://' . $urlParts['host'];
112        if ( isset( $urlParts['port'] ) ) {
113            $canonicalServer .= ':' . $urlParts['port'];
114        }
115
116        return new WikiReference( $canonicalServer, $path );
117    }
118
119    /**
120     * Convenience to get the wiki's display name
121     *
122     * @todo We can give more info than just the wiki id!
123     * @param string $wikiID Wiki'd id (generally database name)
124     * @return string Wiki's name or $wiki_id if the wiki was not found
125     */
126    public static function getWikiName( $wikiID ) {
127        $wiki = self::getWiki( $wikiID );
128        return $wiki ? $wiki->getDisplayName() : $wikiID;
129    }
130
131    /**
132     * Convenience to get a link to a user page on a foreign wiki
133     *
134     * @param string $wikiID Wiki'd id (generally database name)
135     * @param string $user User name (must be normalised before calling this function!)
136     * @param string|null $text Link's text; optional, default to "User:$user"
137     * @return string HTML link or false if the wiki was not found
138     */
139    public static function foreignUserLink( $wikiID, $user, $text = null ) {
140        return self::makeForeignLink( $wikiID, "User:$user", $text );
141    }
142
143    /**
144     * Convenience to get a link to a page on a foreign wiki
145     *
146     * @param string $wikiID Wiki'd id (generally database name)
147     * @param string $page Page name (must be normalised before calling this function!)
148     * @param string|null $text Link's text; optional, default to $page
149     * @return string|false HTML link or false if the wiki was not found
150     */
151    public static function makeForeignLink( $wikiID, $page, $text = null ) {
152        if ( !$text ) {
153            $text = $page;
154        }
155
156        $url = self::getForeignURL( $wikiID, $page );
157        if ( $url === false ) {
158            return false;
159        }
160
161        return Linker::makeExternalLink( $url, $text );
162    }
163
164    /**
165     * Convenience to get a url to a page on a foreign wiki
166     *
167     * @param string $wikiID Wiki'd id (generally database name)
168     * @param string $page Page name (must be normalised before calling this function!)
169     * @param string|null $fragmentId
170     *
171     * @return string|false URL or false if the wiki was not found
172     */
173    public static function getForeignURL( $wikiID, $page, $fragmentId = null ) {
174        $wiki = self::getWiki( $wikiID );
175
176        if ( $wiki ) {
177            return $wiki->getFullUrl( $page, $fragmentId );
178        }
179
180        return false;
181    }
182
183    /**
184     * Get canonical server info for all local wikis in the map that have one
185     *
186     * @return array[] Map of (local wiki ID => map of (url,parts))
187     * @phan-return array<string,array{url:string,parts:string[]|bool}>
188     * @since 1.30
189     */
190    public static function getCanonicalServerInfoForAllWikis() {
191        $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
192
193        return $cache->getWithSetCallback(
194            $cache->makeGlobalKey( 'wikimap-canonical-urls' ),
195            $cache::TTL_DAY,
196            static function () {
197                global $wgLocalDatabases, $wgCanonicalServer;
198
199                $infoMap = [];
200                // Make sure at least the current wiki is set, for simple configurations.
201                // This also makes it the first in the map, which is useful for common cases.
202                $wikiId = self::getCurrentWikiId();
203                $infoMap[$wikiId] = [
204                    'url' => $wgCanonicalServer,
205                    'parts' => wfParseUrl( $wgCanonicalServer )
206                ];
207
208                foreach ( $wgLocalDatabases as $wikiId ) {
209                    $wikiReference = self::getWiki( $wikiId );
210                    if ( $wikiReference ) {
211                        $url = $wikiReference->getCanonicalServer();
212                        $infoMap[$wikiId] = [ 'url' => $url, 'parts' => wfParseUrl( $url ) ];
213                    }
214                }
215
216                return $infoMap;
217            }
218        );
219    }
220
221    /**
222     * @param string $url
223     * @return string|false Wiki ID or false
224     * @since 1.30
225     */
226    public static function getWikiFromUrl( $url ) {
227        global $wgCanonicalServer;
228
229        if ( strpos( $url, "$wgCanonicalServer/" ) === 0 ) {
230            // Optimisation: Handle the common case.
231            // (Duplicates self::getCanonicalServerInfoForAllWikis)
232            return self::getCurrentWikiId();
233        }
234
235        $urlPartsCheck = wfParseUrl( $url );
236        if ( $urlPartsCheck === false ) {
237            return false;
238        }
239
240        static $relevantKeys = [ 'host' => 1, 'port' => 1 ];
241        $urlPartsCheck = array_intersect_key( $urlPartsCheck, $relevantKeys );
242
243        foreach ( self::getCanonicalServerInfoForAllWikis() as $wikiId => $info ) {
244            $urlParts = $info['parts'];
245            if ( $urlParts === false ) {
246                continue;
247            }
248
249            $urlParts = array_intersect_key( $urlParts, $relevantKeys );
250            if ( $urlParts == $urlPartsCheck ) {
251                return $wikiId;
252            }
253        }
254
255        return false;
256    }
257
258    /**
259     * Get the wiki ID of a database domain
260     *
261     * This is like DatabaseDomain::getId() without encoding (for legacy reasons) and
262     * without the schema if it is the generic installer default of "mediawiki"
263     *
264     * @see $wgDBmwschema
265     * @see PostgresInstaller
266     *
267     * @param string|DatabaseDomain $domain
268     * @return string
269     * @since 1.31
270     */
271    public static function getWikiIdFromDbDomain( $domain ) {
272        $domain = DatabaseDomain::newFromId( $domain );
273        // Since the schema was not always part of the wiki ID, try to maintain backwards
274        // compatibility with some common cases. Assume that if the DB domain schema is just
275        // the installer default then it is probably the case that the schema is the same for
276        // all wikis in the farm. Historically, any wiki farm had to make the database/prefix
277        // combination unique per wiki. Omit the schema if it does not seem wiki specific.
278        if ( !in_array( $domain->getSchema(), [ null, 'mediawiki' ], true ) ) {
279            // This means a site admin may have specifically tailored the schemas.
280            // Domain IDs might use the form <DB>-<project>- or <DB>-<project>-<language>_,
281            // meaning that the schema portion must be accounted for to disambiguate wikis.
282            return "{$domain->getDatabase()}-{$domain->getSchema()}-{$domain->getTablePrefix()}";
283        }
284        // Note that if this wiki ID is passed as a domain ID to LoadBalancer, then it can
285        // handle the schema by assuming the generic "mediawiki" schema if needed.
286        return strlen( $domain->getTablePrefix() )
287            ? "{$domain->getDatabase()}-{$domain->getTablePrefix()}"
288            : (string)$domain->getDatabase();
289    }
290
291    /**
292     * @return DatabaseDomain Database domain of the current wiki
293     * @since 1.33
294     */
295    public static function getCurrentWikiDbDomain() {
296        global $wgDBname, $wgDBmwschema, $wgDBprefix;
297        // Avoid invoking LBFactory to avoid any chance of recursion
298        return new DatabaseDomain( $wgDBname, $wgDBmwschema, (string)$wgDBprefix );
299    }
300
301    /**
302     * @since 1.35
303     * @return string
304     */
305    public static function getCurrentWikiId() {
306        return self::getWikiIdFromDbDomain( self::getCurrentWikiDbDomain() );
307    }
308
309    /**
310     * @param DatabaseDomain|string $domain
311     * @return bool Whether $domain matches the DB domain of the current wiki
312     * @since 1.33
313     */
314    public static function isCurrentWikiDbDomain( $domain ) {
315        return self::getCurrentWikiDbDomain()->equals( $domain );
316    }
317
318    /**
319     * @param string $wikiId
320     * @return bool Whether $wikiId matches the wiki ID of the current wiki
321     * @since 1.33
322     */
323    public static function isCurrentWikiId( $wikiId ) {
324        return ( self::getCurrentWikiId() === $wikiId );
325    }
326}
327
328/** @deprecated class alias since 1.40 */
329class_alias( WikiMap::class, 'WikiMap' );