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