Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.66% covered (warning)
88.66%
86 / 97
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SiteMatrixInterwikiResolver
88.66% covered (warning)
88.66%
86 / 97
25.00% covered (danger)
25.00%
1 / 4
37.89
0.00% covered (danger)
0.00%
0 / 1
 __construct
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
8.30
 accepts
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 loadMatrix
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 siteMatrixLoader
91.89% covered (success)
91.89%
68 / 74
0.00% covered (danger)
0.00%
0 / 1
24.31
1<?php
2
3namespace CirrusSearch;
4
5use BagOStuff;
6use ExtensionRegistry;
7use MediaWiki\Extension\SiteMatrix\SiteMatrix;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\WikiMap\WikiMap;
10use WANObjectCache;
11
12/**
13 * InterwikiResolver suited for WMF context and uses SiteMatrix.
14 */
15class SiteMatrixInterwikiResolver extends BaseInterwikiResolver {
16
17    private const MATRIX_CACHE_TTL = 600;
18
19    /**
20     * @var WANObjectCache
21     */
22    private $cache;
23
24    public function __construct(
25        SearchConfig $config,
26        \MultiHttpClient $client = null,
27        WANObjectCache $wanCache = null,
28        BagOStuff $srvCache = null,
29        ExtensionRegistry $extensionRegistry = null
30    ) {
31        parent::__construct( $config, $client, $wanCache, $srvCache );
32        $this->cache = $wanCache ?: MediaWikiServices::getInstance()->getMainWANObjectCache();
33        if ( $config->getWikiId() !== WikiMap::getCurrentWikiId() ) {
34            throw new \RuntimeException( "This resolver cannot with an external wiki config. (config: " .
35                $config->getWikiId() . ", global: " . WikiMap::getCurrentWikiId() );
36        }
37
38        $extensionRegistry = $extensionRegistry ?: ExtensionRegistry::getInstance();
39        if ( !$extensionRegistry->isLoaded( 'SiteMatrix' ) ) {
40            throw new \RuntimeException( "SiteMatrix is required" );
41        }
42        if ( !$this->config->has( 'SiteMatrixSites' ) ) {
43            throw new \RuntimeException( '$wgSiteMatrixSites must be set.' );
44        }
45    }
46
47    /**
48     * @param SearchConfig $config
49     * @param ExtensionRegistry|null $extensionRegistry
50     * @return bool true if this resolver can run with the specified config
51     */
52    public static function accepts( SearchConfig $config, ExtensionRegistry $extensionRegistry = null ) {
53        $extensionRegistry = $extensionRegistry ?: ExtensionRegistry::getInstance();
54        return $config->getWikiId() === WikiMap::getCurrentWikiId()
55            && $extensionRegistry->isLoaded( 'SiteMatrix' )
56            && $config->has( 'SiteMatrixSites' );
57    }
58
59    protected function loadMatrix() {
60        $cacheKey = $this->cache->makeKey( 'cirrussearch-interwiki-matrix', 'v1' );
61        $matrix = $this->cache->getWithSetCallback(
62            $cacheKey,
63            self::MATRIX_CACHE_TTL,
64            $this->siteMatrixLoader()
65        );
66        if ( !is_array( $matrix ) ) {
67            // Should we log something if we failed?
68            return [];
69        }
70        return $matrix;
71    }
72
73    /**
74     * @return callable
75     */
76    private function siteMatrixLoader() {
77        return function () {
78            global $wgConf;
79
80            $matrix = new SiteMatrix;
81            $iwLookup = MediaWikiServices::getInstance()->getInterwikiLookup();
82            $wikiDBname = $this->config->get( 'DBname' );
83            [ , $myLang ] = $wgConf->siteFromDB( $wikiDBname );
84            $siteConf = $this->config->get( 'SiteMatrixSites' );
85            $prefixOverrides = $this->config->get( 'CirrusSearchInterwikiPrefixOverrides' );
86            $sisterProjects = [];
87            $crossLanguage = [];
88            $prefixesByWiki = [];
89            $languageMap = [];
90            $myProject = null;
91
92            if ( !in_array( $myLang, $matrix->getLangList() ) ) {
93                return [];
94            }
95
96            foreach ( $matrix->getSites() as $site ) {
97                if ( $matrix->getDBName( $myLang, $site ) === $wikiDBname ) {
98                    $myProject = $site;
99                    break;
100                }
101            }
102
103            if ( $myProject === null ) {
104                // This is not a "project"
105                return [];
106            }
107
108            foreach ( $matrix->getSites() as $site ) {
109                if ( $site === $myProject ) {
110                    continue;
111                }
112                if ( !$matrix->exist( $myLang, $site ) ) {
113                    continue;
114                }
115                if ( $matrix->isClosed( $myLang, $site ) ) {
116                    continue;
117                }
118                if ( !isset( $siteConf[$site]['prefix'] ) ) {
119                    continue;
120                }
121                $dbName = $matrix->getDBName( $myLang, $site );
122                $prefix = $siteConf[$site]['prefix'];
123
124                if ( isset( $prefixOverrides[$prefix] ) ) {
125                    $prefix = $prefixOverrides[$prefix];
126                }
127
128                if ( !in_array( $prefix, $this->config->get( 'CirrusSearchCrossProjectSearchBlockList' ) ) ) {
129                    $sisterProjects[$prefix] = $dbName;
130                }
131                $prefixesByWiki[$dbName] = $prefix;
132            }
133
134            foreach ( $matrix->getLangList() as $lang ) {
135                if ( $lang === $myLang ) {
136                    continue;
137                }
138
139                $dbname = $matrix->getDBName( $lang, $myProject );
140                if ( !$matrix->exist( $lang, $myProject ) ) {
141                    continue;
142                }
143                if ( $matrix->isClosed( $lang, $myProject ) ) {
144                    continue;
145                }
146                // Bold assumption that the interwiki prefix is equal
147                // to the language.
148                $iw = $iwLookup->fetch( $lang );
149                // Not a valid interwiki prefix...
150                if ( !$iw ) {
151                    continue;
152                }
153
154                $url = $matrix->getCanonicalUrl( $lang, $myProject );
155                $iwurl = $iw->getURL();
156                if ( strlen( $url ) > strlen( $iwurl ) ) {
157                    continue;
158                }
159                if ( substr_compare( $iwurl, $url, 0, strlen( $url ) ) !== 0 ) {
160                    continue;
161                }
162
163                $crossLanguage[$lang] = $dbname;
164                // In theory it's impossible to override something here
165                // should we log something if the case?
166                $prefixesByWiki[$dbname] = $lang;
167                $wikiLangCode = $wgConf->get( 'wgLanguageCode', $dbname, $myProject,
168                    [ 'lang' => $lang, 'site' => $myProject ] );
169                $languageMap[$wikiLangCode][] = $lang;
170            }
171            // Cleanup unambiguous languages
172            $cleanLanguageMap = [];
173            foreach ( $languageMap as $lang => $dbprefixes ) {
174                if ( array_key_exists( $lang, $dbprefixes )
175                    && ( $dbprefixes === [ $lang ] )
176                ) {
177                    continue;
178                }
179                // if lang is equals to one of the dbprefixes then
180                if ( in_array( $lang, $dbprefixes ) ) {
181                    continue;
182                }
183                if ( count( $dbprefixes ) > 1 ) {
184                    // TODO: Log this ambiguous entry
185                }
186                $cleanLanguageMap[$lang] = reset( $dbprefixes );
187            }
188            return [
189                'sister_projects' => $sisterProjects,
190                'language_map' => $cleanLanguageMap,
191                'cross_language' => $crossLanguage,
192                'prefixes_by_wiki' => $prefixesByWiki,
193            ];
194        };
195    }
196
197}