Merge "Selenium: replace UserLoginPage with BlankPage where possible"
[lhc/web/wiklou.git] / includes / externalstore / ExternalStoreFactory.php
1 <?php
2 /**
3 * @defgroup ExternalStorage ExternalStorage
4 */
5
6 use MediaWiki\MediaWikiServices;
7 use Psr\Log\LoggerAwareInterface;
8 use Psr\Log\LoggerInterface;
9 use Psr\Log\NullLogger;
10 use Wikimedia\Assert\Assert;
11
12 /**
13 * @ingroup ExternalStorage
14 */
15 class ExternalStoreFactory implements LoggerAwareInterface {
16 /** @var string[] List of storage access protocols */
17 private $protocols;
18 /** @var string[] List of base storage URLs that define locations for writes */
19 private $writeBaseUrls;
20 /** @var string Default database domain to store content under */
21 private $localDomainId;
22 /** @var LoggerInterface */
23 private $logger;
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 $localDomainId,
35 LoggerInterface $logger = null
36 ) {
37 Assert::parameterType( 'string', $localDomainId, '$localDomainId' );
38
39 $this->protocols = array_map( 'strtolower', $externalStores );
40 $this->writeBaseUrls = $defaultStores;
41 $this->localDomainId = $localDomainId;
42 $this->logger = $logger ?: new NullLogger();
43 }
44
45 public function setLogger( LoggerInterface $logger ) {
46 $this->logger = $logger;
47 }
48
49 /**
50 * @return string[] List of active store types/protocols (lowercased), e.g. [ "db" ]
51 * @since 1.34
52 */
53 public function getProtocols() {
54 return $this->protocols;
55 }
56
57 /**
58 * @return string[] List of base URLs for writes, e.g. [ "DB://cluster1" ]
59 * @since 1.34
60 */
61 public function getWriteBaseUrls() {
62 return $this->writeBaseUrls;
63 }
64
65 /**
66 * Get an external store object of the given type, with the given parameters
67 *
68 * The 'domain' field in $params will be set to the local DB domain if it is unset
69 * or false. A special 'isDomainImplicit' flag is set when this happens, which should
70 * only be used to handle legacy DB domain configuration concerns (e.g. T200471).
71 *
72 * @param string $proto Type of external storage, should be a value in $wgExternalStores
73 * @param array $params Map of ExternalStoreMedium::__construct context parameters.
74 * @return ExternalStoreMedium The store class or false on error
75 * @throws ExternalStoreException When $proto is not recognized
76 */
77 public function getStore( $proto, array $params = [] ) {
78 $protoLowercase = strtolower( $proto ); // normalize
79 if ( !$this->protocols || !in_array( $protoLowercase, $this->protocols ) ) {
80 throw new ExternalStoreException( "Protocol '$proto' is not enabled." );
81 }
82
83 $class = 'ExternalStore' . ucfirst( $proto );
84 if ( isset( $params['wiki'] ) ) {
85 $params += [ 'domain' => $params['wiki'] ]; // b/c
86 }
87 if ( !isset( $params['domain'] ) || $params['domain'] === false ) {
88 $params['domain'] = $this->localDomainId; // default
89 $params['isDomainImplicit'] = true; // b/c for ExternalStoreDB
90 }
91 $params['writableLocations'] = [];
92 // Determine the locations for this protocol/store still receiving writes
93 foreach ( $this->writeBaseUrls as $storeUrl ) {
94 list( $storeProto, $storePath ) = self::splitStorageUrl( $storeUrl );
95 if ( $protoLowercase === strtolower( $storeProto ) ) {
96 $params['writableLocations'][] = $storePath;
97 }
98 }
99 // @TODO: ideally, this class should not hardcode what classes need what backend factory
100 // objects. For now, inject the factory instances into __construct() for those that do.
101 if ( $protoLowercase === 'db' ) {
102 $params['lbFactory'] = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
103 } elseif ( $protoLowercase === 'mwstore' ) {
104 $params['fbGroup'] = FileBackendGroup::singleton();
105 }
106 $params['logger'] = $this->logger;
107
108 if ( !class_exists( $class ) ) {
109 throw new ExternalStoreException( "Class '$class' is not defined." );
110 }
111
112 // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
113 return new $class( $params );
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 list( $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 list( , $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 array[] 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 list( $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 }