Merge "Simplify code to avoid interpreting "$" characters in string replacement"
[mediawiki.git] / includes / filebackend / FileBackendGroup.php
bloba5ecfe5014d49668898b890a0888f29f5af8ffd9
1 <?php
2 /**
3 * File backend registration handling.
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.
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.
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
20 * @file
21 * @ingroup FileBackend
24 namespace MediaWiki\FileBackend;
26 use FileBackendMultiWrite;
27 use FSFileBackend;
28 use InvalidArgumentException;
29 use LogicException;
30 use MediaWiki\Config\ServiceOptions;
31 use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
32 use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
33 use MediaWiki\Logger\LoggerFactory;
34 use MediaWiki\MainConfigNames;
35 use MediaWiki\Output\StreamFile;
36 use MediaWiki\Status\Status;
37 use Profiler;
38 use Wikimedia\FileBackend\FileBackend;
39 use Wikimedia\Mime\MimeAnalyzer;
40 use Wikimedia\ObjectCache\BagOStuff;
41 use Wikimedia\ObjectCache\WANObjectCache;
42 use Wikimedia\ObjectFactory\ObjectFactory;
43 use Wikimedia\Rdbms\ReadOnlyMode;
45 /**
46 * Class to handle file backend registration
48 * @ingroup FileBackend
49 * @since 1.19
51 class FileBackendGroup {
52 /**
53 * @var array[] (name => ('class' => string, 'config' => array, 'instance' => object))
54 * @phan-var array<string,array{class:class-string,config:array,instance:object}>
56 protected $backends = [];
58 /** @var ServiceOptions */
59 private $options;
61 /** @var BagOStuff */
62 private $srvCache;
64 /** @var WANObjectCache */
65 private $wanCache;
67 /** @var MimeAnalyzer */
68 private $mimeAnalyzer;
70 /** @var LockManagerGroupFactory */
71 private $lmgFactory;
73 /** @var TempFSFileFactory */
74 private $tmpFileFactory;
76 /** @var ObjectFactory */
77 private $objectFactory;
79 /**
80 * @internal For use by ServiceWiring
82 public const CONSTRUCTOR_OPTIONS = [
83 MainConfigNames::DirectoryMode,
84 MainConfigNames::FileBackends,
85 MainConfigNames::ForeignFileRepos,
86 MainConfigNames::LocalFileRepo,
87 'fallbackWikiId',
90 /**
91 * @param ServiceOptions $options
92 * @param ReadOnlyMode $readOnlyMode
93 * @param BagOStuff $srvCache
94 * @param WANObjectCache $wanCache
95 * @param MimeAnalyzer $mimeAnalyzer
96 * @param LockManagerGroupFactory $lmgFactory
97 * @param TempFSFileFactory $tmpFileFactory
98 * @param ObjectFactory $objectFactory
100 public function __construct(
101 ServiceOptions $options,
102 ReadOnlyMode $readOnlyMode,
103 BagOStuff $srvCache,
104 WANObjectCache $wanCache,
105 MimeAnalyzer $mimeAnalyzer,
106 LockManagerGroupFactory $lmgFactory,
107 TempFSFileFactory $tmpFileFactory,
108 ObjectFactory $objectFactory
110 $this->options = $options;
111 $this->srvCache = $srvCache;
112 $this->wanCache = $wanCache;
113 $this->mimeAnalyzer = $mimeAnalyzer;
114 $this->lmgFactory = $lmgFactory;
115 $this->tmpFileFactory = $tmpFileFactory;
116 $this->objectFactory = $objectFactory;
118 // Register explicitly defined backends
119 $this->register( $options->get( MainConfigNames::FileBackends ), $readOnlyMode->getConfiguredReason() );
121 $autoBackends = [];
122 // Automatically create b/c backends for file repos...
123 $repos = array_merge(
124 $options->get( MainConfigNames::ForeignFileRepos ), [ $options->get( MainConfigNames::LocalFileRepo ) ] );
125 foreach ( $repos as $info ) {
126 $backendName = $info['backend'];
127 if ( is_object( $backendName ) || isset( $this->backends[$backendName] ) ) {
128 continue; // already defined (or set to the object for some reason)
130 $repoName = $info['name'];
131 // Local vars that used to be FSRepo members...
132 $directory = $info['directory'];
133 $deletedDir = $info['deletedDir'] ?? false; // deletion disabled
134 $thumbDir = $info['thumbDir'] ?? "{$directory}/thumb";
135 $transcodedDir = $info['transcodedDir'] ?? "{$directory}/transcoded";
136 $lockManager = $info['lockManager'] ?? 'fsLockManager';
137 // Get the FS backend configuration
138 $autoBackends[] = [
139 'name' => $backendName,
140 'class' => FSFileBackend::class,
141 'lockManager' => $lockManager,
142 'containerPaths' => [
143 "{$repoName}-public" => "{$directory}",
144 "{$repoName}-thumb" => $thumbDir,
145 "{$repoName}-transcoded" => $transcodedDir,
146 "{$repoName}-deleted" => $deletedDir,
147 "{$repoName}-temp" => "{$directory}/temp"
149 'fileMode' => $info['fileMode'] ?? 0644,
150 'directoryMode' => $options->get( MainConfigNames::DirectoryMode ),
154 // Register implicitly defined backends
155 $this->register( $autoBackends, $readOnlyMode->getConfiguredReason() );
159 * Register an array of file backend configurations
161 * @param array[] $configs
162 * @param string|null $readOnlyReason
164 protected function register( array $configs, $readOnlyReason = null ) {
165 foreach ( $configs as $config ) {
166 if ( !isset( $config['name'] ) ) {
167 throw new InvalidArgumentException( "Cannot register a backend with no name." );
169 $name = $config['name'];
170 if ( isset( $this->backends[$name] ) ) {
171 throw new LogicException( "Backend with name '$name' already registered." );
172 } elseif ( !isset( $config['class'] ) ) {
173 throw new InvalidArgumentException( "Backend with name '$name' has no class." );
175 $class = $config['class'];
177 $config['domainId'] ??= $config['wikiId'] ?? $this->options->get( 'fallbackWikiId' );
178 $config['readOnly'] ??= $readOnlyReason;
180 unset( $config['class'] ); // backend won't need this
181 $this->backends[$name] = [
182 'class' => $class,
183 'config' => $config,
184 'instance' => null
190 * Get the backend object with a given name
192 * @param string $name
193 * @return FileBackend
195 public function get( $name ) {
196 // Lazy-load the actual backend instance
197 if ( !isset( $this->backends[$name]['instance'] ) ) {
198 $config = $this->config( $name );
200 $class = $config['class'];
201 if ( $class === FileBackendMultiWrite::class ) {
202 // @todo How can we test this? What's the intended use-case?
203 foreach ( $config['backends'] as $index => $beConfig ) {
204 if ( isset( $beConfig['template'] ) ) {
205 // Config is just a modified version of a registered backend's.
206 // This should only be used when that config is used only by this backend.
207 $config['backends'][$index] += $this->config( $beConfig['template'] );
212 $this->backends[$name]['instance'] = new $class( $config );
215 return $this->backends[$name]['instance'];
219 * Get the config array for a backend object with a given name
221 * @param string $name
222 * @return array Parameters to FileBackend::__construct()
224 public function config( $name ) {
225 if ( !isset( $this->backends[$name] ) ) {
226 throw new InvalidArgumentException( "No backend defined with the name '$name'." );
229 $config = $this->backends[$name]['config'];
231 return array_merge(
232 // Default backend parameters
234 'mimeCallback' => [ $this, 'guessMimeInternal' ],
235 'obResetFunc' => 'wfResetOutputBuffers',
236 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ],
237 'tmpFileFactory' => $this->tmpFileFactory,
238 'statusWrapper' => [ Status::class, 'wrap' ],
239 'wanCache' => $this->wanCache,
240 'srvCache' => $this->srvCache,
241 'logger' => LoggerFactory::getInstance( 'FileOperation' ),
242 'profiler' => static function ( $section ) {
243 return Profiler::instance()->scopedProfileIn( $section );
246 // Configured backend parameters
247 $config,
248 // Resolved backend parameters
250 'class' => $this->backends[$name]['class'],
251 'lockManager' =>
252 $this->lmgFactory->getLockManagerGroup( $config['domainId'] )
253 ->get( $config['lockManager'] ),
259 * Get an appropriate backend object from a storage path
261 * @param string $storagePath
262 * @return FileBackend|null Backend or null on failure
264 public function backendFromPath( $storagePath ) {
265 [ $backend, , ] = FileBackend::splitStoragePath( $storagePath );
266 if ( $backend !== null && isset( $this->backends[$backend] ) ) {
267 return $this->get( $backend );
270 return null;
274 * @param string $storagePath
275 * @param string|null $content
276 * @param string|null $fsPath
277 * @return string
278 * @since 1.27
280 public function guessMimeInternal( $storagePath, $content, $fsPath ) {
281 // Trust the extension of the storage path (caller must validate)
282 $ext = FileBackend::extensionFromPath( $storagePath );
283 $type = $this->mimeAnalyzer->getMimeTypeFromExtensionOrNull( $ext );
284 // For files without a valid extension (or one at all), inspect the contents
285 if ( !$type && $fsPath ) {
286 $type = $this->mimeAnalyzer->guessMimeType( $fsPath, false );
287 } elseif ( !$type && $content !== null && $content !== '' ) {
288 $tmpFile = $this->tmpFileFactory->newTempFSFile( 'mime_', '' );
289 file_put_contents( $tmpFile->getPath(), $content );
290 $type = $this->mimeAnalyzer->guessMimeType( $tmpFile->getPath(), false );
292 return $type ?: 'unknown/unknown';
295 /** @deprecated class alias since 1.43 */
296 class_alias( FileBackendGroup::class, 'FileBackendGroup' );