Merge "Make DBAccessBase use DBConnRef, rename $wiki, and hide getLoadBalancer()"
[lhc/web/wiklou.git] / includes / filebackend / FileBackendGroup.php
1 <?php
2 /**
3 * File backend registration handling.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup FileBackend
22 */
23
24 use MediaWiki\Logger\LoggerFactory;
25 use MediaWiki\MediaWikiServices;
26
27 /**
28 * Class to handle file backend registration
29 *
30 * @ingroup FileBackend
31 * @since 1.19
32 */
33 class FileBackendGroup {
34 /** @var FileBackendGroup */
35 protected static $instance = null;
36
37 /** @var array (name => ('class' => string, 'config' => array, 'instance' => object)) */
38 protected $backends = [];
39
40 protected function __construct() {
41 }
42
43 /**
44 * @return FileBackendGroup
45 */
46 public static function singleton() {
47 if ( self::$instance == null ) {
48 self::$instance = new self();
49 self::$instance->initFromGlobals();
50 }
51
52 return self::$instance;
53 }
54
55 /**
56 * Destroy the singleton instance
57 */
58 public static function destroySingleton() {
59 self::$instance = null;
60 }
61
62 /**
63 * Register file backends from the global variables
64 */
65 protected function initFromGlobals() {
66 global $wgLocalFileRepo, $wgForeignFileRepos, $wgFileBackends, $wgDirectoryMode;
67
68 // Register explicitly defined backends
69 $this->register( $wgFileBackends, wfConfiguredReadOnlyReason() );
70
71 $autoBackends = [];
72 // Automatically create b/c backends for file repos...
73 $repos = array_merge( $wgForeignFileRepos, [ $wgLocalFileRepo ] );
74 foreach ( $repos as $info ) {
75 $backendName = $info['backend'];
76 if ( is_object( $backendName ) || isset( $this->backends[$backendName] ) ) {
77 continue; // already defined (or set to the object for some reason)
78 }
79 $repoName = $info['name'];
80 // Local vars that used to be FSRepo members...
81 $directory = $info['directory'];
82 $deletedDir = $info['deletedDir'] ?? false; // deletion disabled
83 $thumbDir = $info['thumbDir'] ?? "{$directory}/thumb";
84 $transcodedDir = $info['transcodedDir'] ?? "{$directory}/transcoded";
85 // Get the FS backend configuration
86 $autoBackends[] = [
87 'name' => $backendName,
88 'class' => FSFileBackend::class,
89 'lockManager' => 'fsLockManager',
90 'containerPaths' => [
91 "{$repoName}-public" => "{$directory}",
92 "{$repoName}-thumb" => $thumbDir,
93 "{$repoName}-transcoded" => $transcodedDir,
94 "{$repoName}-deleted" => $deletedDir,
95 "{$repoName}-temp" => "{$directory}/temp"
96 ],
97 'fileMode' => $info['fileMode'] ?? 0644,
98 'directoryMode' => $wgDirectoryMode,
99 ];
100 }
101
102 // Register implicitly defined backends
103 $this->register( $autoBackends, wfConfiguredReadOnlyReason() );
104 }
105
106 /**
107 * Register an array of file backend configurations
108 *
109 * @param array[] $configs
110 * @param string|null $readOnlyReason
111 * @throws InvalidArgumentException
112 */
113 protected function register( array $configs, $readOnlyReason = null ) {
114 foreach ( $configs as $config ) {
115 if ( !isset( $config['name'] ) ) {
116 throw new InvalidArgumentException( "Cannot register a backend with no name." );
117 }
118 $name = $config['name'];
119 if ( isset( $this->backends[$name] ) ) {
120 throw new LogicException( "Backend with name '$name' already registered." );
121 } elseif ( !isset( $config['class'] ) ) {
122 throw new InvalidArgumentException( "Backend with name '$name' has no class." );
123 }
124 $class = $config['class'];
125
126 // @FIXME: ideally this would default to the DB domain (which includes the schema)
127 $config['domainId'] = $config['domainId'] ?? ( $config['wikiId'] ?? wfWikiID() );
128 $config['readOnly'] = $config['readOnly'] ?? $readOnlyReason;
129
130 unset( $config['class'] ); // backend won't need this
131 $this->backends[$name] = [
132 'class' => $class,
133 'config' => $config,
134 'instance' => null
135 ];
136 }
137 }
138
139 /**
140 * Get the backend object with a given name
141 *
142 * @param string $name
143 * @return FileBackend
144 * @throws InvalidArgumentException
145 */
146 public function get( $name ) {
147 // Lazy-load the actual backend instance
148 if ( !isset( $this->backends[$name]['instance'] ) ) {
149 $config = $this->config( $name );
150
151 $class = $config['class'];
152 if ( $class === FileBackendMultiWrite::class ) {
153 // @todo How can we test this? What's the intended use-case?
154 foreach ( $config['backends'] as $index => $beConfig ) {
155 if ( isset( $beConfig['template'] ) ) {
156 // Config is just a modified version of a registered backend's.
157 // This should only be used when that config is used only by this backend.
158 $config['backends'][$index] += $this->config( $beConfig['template'] );
159 }
160 }
161 }
162
163 $this->backends[$name]['instance'] = new $class( $config );
164 }
165
166 return $this->backends[$name]['instance'];
167 }
168
169 /**
170 * Get the config array for a backend object with a given name
171 *
172 * @param string $name
173 * @return array Parameters to FileBackend::__construct()
174 * @throws InvalidArgumentException
175 */
176 public function config( $name ) {
177 if ( !isset( $this->backends[$name] ) ) {
178 throw new InvalidArgumentException( "No backend defined with the name '$name'." );
179 }
180
181 $config = $this->backends[$name]['config'];
182 $services = MediaWikiServices::getInstance();
183
184 return array_merge(
185 // Default backend parameters
186 [
187 'mimeCallback' => [ $this, 'guessMimeInternal' ],
188 'obResetFunc' => 'wfResetOutputBuffers',
189 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ],
190 'tmpFileFactory' => $services->getTempFSFileFactory(),
191 'statusWrapper' => [ Status::class, 'wrap' ],
192 'wanCache' => $services->getMainWANObjectCache(),
193 'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ),
194 'logger' => LoggerFactory::getInstance( 'FileOperation' ),
195 'profiler' => function ( $section ) {
196 return Profiler::instance()->scopedProfileIn( $section );
197 }
198 ],
199 // Configured backend parameters
200 $config,
201 // Resolved backend parameters
202 [
203 'class' => $this->backends[$name]['class'],
204 'lockManager' =>
205 LockManagerGroup::singleton( $config['domainId'] )
206 ->get( $config['lockManager'] ),
207 'fileJournal' => isset( $config['fileJournal'] )
208 ? FileJournal::factory( $config['fileJournal'], $name )
209 : FileJournal::factory( [ 'class' => NullFileJournal::class ], $name )
210 ]
211 );
212 }
213
214 /**
215 * Get an appropriate backend object from a storage path
216 *
217 * @param string $storagePath
218 * @return FileBackend|null Backend or null on failure
219 */
220 public function backendFromPath( $storagePath ) {
221 list( $backend, , ) = FileBackend::splitStoragePath( $storagePath );
222 if ( $backend !== null && isset( $this->backends[$backend] ) ) {
223 return $this->get( $backend );
224 }
225
226 return null;
227 }
228
229 /**
230 * @param string $storagePath
231 * @param string|null $content
232 * @param string|null $fsPath
233 * @return string
234 * @since 1.27
235 */
236 public function guessMimeInternal( $storagePath, $content, $fsPath ) {
237 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
238 // Trust the extension of the storage path (caller must validate)
239 $ext = FileBackend::extensionFromPath( $storagePath );
240 $type = $magic->guessTypesForExtension( $ext );
241 // For files without a valid extension (or one at all), inspect the contents
242 if ( !$type && $fsPath ) {
243 $type = $magic->guessMimeType( $fsPath, false );
244 } elseif ( !$type && strlen( $content ) ) {
245 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
246 ->newTempFSFile( 'mime_', '' );
247 file_put_contents( $tmpFile->getPath(), $content );
248 $type = $magic->guessMimeType( $tmpFile->getPath(), false );
249 }
250 return $type ?: 'unknown/unknown';
251 }
252 }