Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.98% covered (warning)
82.98%
39 / 47
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExternalStoreFactory
82.98% covered (warning)
82.98%
39 / 47
44.44% covered (danger)
44.44%
4 / 9
29.33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProtocols
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWriteBaseUrls
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStore
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
11.64
 getStoreForUrl
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getStoreLocationFromUrl
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getUrlsByProtocol
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 splitStorageUrl
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
1<?php
2
3use MediaWiki\MediaWikiServices;
4use Psr\Log\LoggerAwareInterface;
5use Psr\Log\LoggerInterface;
6use Psr\Log\NullLogger;
7
8/**
9 * @see ExternalStoreAccess
10 * @internal Use the ExternalStoreAccess service instead.
11 * @since 1.31
12 * @ingroup ExternalStorage
13 */
14class ExternalStoreFactory implements LoggerAwareInterface {
15    /** @var string[] List of storage access protocols */
16    private $protocols;
17    /** @var string[] List of base storage URLs that define locations for writes */
18    private $writeBaseUrls;
19    /** @var string Default database domain to store content under */
20    private $localDomainId;
21    /** @var LoggerInterface */
22    private $logger;
23    /** @var ExternalStoreMedium[] */
24    private $stores = [];
25
26    /**
27     * @param string[] $externalStores See $wgExternalStores
28     * @param string[] $defaultStores See $wgDefaultExternalStore
29     * @param string $localDomainId Local database/wiki ID
30     * @param LoggerInterface|null $logger
31     */
32    public function __construct(
33        array $externalStores,
34        array $defaultStores,
35        string $localDomainId,
36        ?LoggerInterface $logger = null
37    ) {
38        $this->protocols = array_map( 'strtolower', $externalStores );
39        $this->writeBaseUrls = $defaultStores;
40        $this->localDomainId = $localDomainId;
41        $this->logger = $logger ?: new NullLogger();
42    }
43
44    public function setLogger( LoggerInterface $logger ) {
45        $this->logger = $logger;
46    }
47
48    /**
49     * @return string[] List of active store types/protocols (lowercased), e.g. [ "db" ]
50     * @since 1.34
51     */
52    public function getProtocols() {
53        return $this->protocols;
54    }
55
56    /**
57     * @return string[] List of default base URLs for writes, e.g. [ "DB://cluster1" ]
58     * @since 1.34
59     */
60    public function getWriteBaseUrls() {
61        return $this->writeBaseUrls;
62    }
63
64    /**
65     * Get an external store object of the given type, with the given parameters
66     *
67     * The 'domain' field in $params will be set to the local DB domain if it is unset
68     * or false. A special 'isDomainImplicit' flag is set when this happens, which should
69     * only be used to handle legacy DB domain configuration concerns (e.g. T200471).
70     *
71     * @param string $proto Type of external storage, should be a value in $wgExternalStores
72     * @param array $params Map of ExternalStoreMedium::__construct context parameters.
73     * @return ExternalStoreMedium The store class or false on error
74     * @throws ExternalStoreException When $proto is not recognized
75     */
76    public function getStore( $proto, array $params = [] ) {
77        $cacheKey = $proto . ':' . json_encode( $params );
78        if ( isset( $this->stores[$cacheKey] ) ) {
79            return $this->stores[$cacheKey];
80        }
81        $protoLowercase = strtolower( $proto ); // normalize
82        if ( !$this->protocols || !in_array( $protoLowercase, $this->protocols ) ) {
83            throw new ExternalStoreException( "Protocol '$proto' is not enabled." );
84        }
85
86        if ( $protoLowercase === 'db' ) {
87            $class = 'ExternalStoreDB';
88        } else {
89            $class = 'ExternalStore' . ucfirst( $proto );
90        }
91        if ( isset( $params['wiki'] ) ) {
92            $params += [ 'domain' => $params['wiki'] ]; // b/c
93        }
94        if ( !isset( $params['domain'] ) || $params['domain'] === false ) {
95            $params['domain'] = $this->localDomainId; // default
96            $params['isDomainImplicit'] = true; // b/c for ExternalStoreDB
97        }
98        // @TODO: ideally, this class should not hardcode what classes need what backend factory
99        // objects. For now, inject the factory instances into __construct() for those that do.
100        if ( $protoLowercase === 'db' ) {
101            $params['lbFactory'] = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
102        } elseif ( $protoLowercase === 'mwstore' ) {
103            $params['fbGroup'] = MediaWikiServices::getInstance()->getFileBackendGroup();
104        }
105        $params['logger'] = $this->logger;
106
107        if ( !class_exists( $class ) ) {
108            throw new ExternalStoreException( "Class '$class' is not defined." );
109        }
110
111        // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
112        $this->stores[$cacheKey] = new $class( $params );
113        return $this->stores[$cacheKey];
114    }
115
116    /**
117     * Get the ExternalStoreMedium for a given URL
118     *
119     * $url is either of the form:
120     *   - a) "<proto>://<location>/<path>", for retrieval, or
121     *   - b) "<proto>://<location>", for storage
122     *
123     * @param string $url
124     * @param array $params Map of ExternalStoreMedium::__construct context parameters
125     * @return ExternalStoreMedium
126     * @throws ExternalStoreException When the protocol is missing or not recognized
127     * @since 1.34
128     */
129    public function getStoreForUrl( $url, array $params = [] ) {
130        [ $proto, $path ] = self::splitStorageUrl( $url );
131        if ( $path == '' ) { // bad URL
132            throw new ExternalStoreException( "Invalid URL '$url'" );
133        }
134
135        return $this->getStore( $proto, $params );
136    }
137
138    /**
139     * Get the location within the appropriate store for a given a URL
140     *
141     * @param string $url
142     * @return string
143     * @throws ExternalStoreException
144     * @since 1.34
145     */
146    public function getStoreLocationFromUrl( $url ) {
147        [ , $location ] = self::splitStorageUrl( $url );
148        if ( $location == '' ) { // bad URL
149            throw new ExternalStoreException( "Invalid URL '$url'" );
150        }
151
152        return $location;
153    }
154
155    /**
156     * @param string[] $urls
157     * @return string[][] Map of (protocol => list of URLs)
158     * @throws ExternalStoreException
159     * @since 1.34
160     */
161    public function getUrlsByProtocol( array $urls ) {
162        $urlsByProtocol = [];
163        foreach ( $urls as $url ) {
164            [ $proto, ] = self::splitStorageUrl( $url );
165            $urlsByProtocol[$proto][] = $url;
166        }
167
168        return $urlsByProtocol;
169    }
170
171    /**
172     * @param string $storeUrl
173     * @return string[] (protocol, store location or location-qualified path)
174     * @throws ExternalStoreException
175     */
176    private static function splitStorageUrl( $storeUrl ) {
177        $parts = explode( '://', $storeUrl );
178        if ( count( $parts ) != 2 || $parts[0] === '' || $parts[1] === '' ) {
179            throw new ExternalStoreException( "Invalid storage URL '$storeUrl'" );
180        }
181
182        return $parts;
183    }
184}