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