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