Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.04% covered (success)
96.04%
97 / 101
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileBackendGroup
97.00% covered (success)
97.00%
97 / 100
83.33% covered (warning)
83.33%
5 / 6
27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
4
 register
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 get
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
7.33
 config
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
2
 backendFromPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 guessMimeInternal
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
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
24namespace MediaWiki\FileBackend;
25
26use InvalidArgumentException;
27use LogicException;
28use MediaWiki\Config\ServiceOptions;
29use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
30use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
31use MediaWiki\Logger\LoggerFactory;
32use MediaWiki\MainConfigNames;
33use MediaWiki\Output\StreamFile;
34use MediaWiki\Status\Status;
35use Profiler;
36use Wikimedia\FileBackend\FileBackend;
37use Wikimedia\FileBackend\FileBackendMultiWrite;
38use Wikimedia\FileBackend\FSFileBackend;
39use Wikimedia\Mime\MimeAnalyzer;
40use Wikimedia\ObjectCache\BagOStuff;
41use Wikimedia\ObjectCache\WANObjectCache;
42use Wikimedia\ObjectFactory\ObjectFactory;
43use Wikimedia\Rdbms\ReadOnlyMode;
44
45/**
46 * Class to handle file backend registration
47 *
48 * @ingroup FileBackend
49 * @since 1.19
50 */
51class 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}>
55     */
56    protected $backends = [];
57
58    /** @var ServiceOptions */
59    private $options;
60
61    /** @var BagOStuff */
62    private $srvCache;
63
64    /** @var WANObjectCache */
65    private $wanCache;
66
67    /** @var MimeAnalyzer */
68    private $mimeAnalyzer;
69
70    /** @var LockManagerGroupFactory */
71    private $lmgFactory;
72
73    /** @var TempFSFileFactory */
74    private $tmpFileFactory;
75
76    /** @var ObjectFactory */
77    private $objectFactory;
78
79    /**
80     * @internal For use by ServiceWiring
81     */
82    public const CONSTRUCTOR_OPTIONS = [
83        MainConfigNames::DirectoryMode,
84        MainConfigNames::FileBackends,
85        MainConfigNames::ForeignFileRepos,
86        MainConfigNames::LocalFileRepo,
87        'fallbackWikiId',
88    ];
89
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
99     */
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
109    ) {
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;
117
118        // Register explicitly defined backends
119        $this->register( $options->get( MainConfigNames::FileBackends ), $readOnlyMode->getConfiguredReason() );
120
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)
129            }
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"
148                ],
149                'fileMode' => $info['fileMode'] ?? 0644,
150                'directoryMode' => $options->get( MainConfigNames::DirectoryMode ),
151            ];
152        }
153
154        // Register implicitly defined backends
155        $this->register( $autoBackends, $readOnlyMode->getConfiguredReason() );
156    }
157
158    /**
159     * Register an array of file backend configurations
160     *
161     * @param array[] $configs
162     * @param string|null $readOnlyReason
163     */
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." );
168            }
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." );
174            }
175            $class = $config['class'];
176
177            $config['domainId'] ??= $config['wikiId'] ?? $this->options->get( 'fallbackWikiId' );
178            $config['readOnly'] ??= $readOnlyReason;
179
180            unset( $config['class'] ); // backend won't need this
181            $this->backends[$name] = [
182                'class' => $class,
183                'config' => $config,
184                'instance' => null
185            ];
186        }
187    }
188
189    /**
190     * Get the backend object with a given name
191     *
192     * @param string $name
193     * @return FileBackend
194     */
195    public function get( $name ) {
196        // Lazy-load the actual backend instance
197        if ( !isset( $this->backends[$name]['instance'] ) ) {
198            $config = $this->config( $name );
199
200            $class = $config['class'];
201            // Checking old alias for compatibility with unchanged config
202            if ( $class === FileBackendMultiWrite::class || $class === \FileBackendMultiWrite::class ) {
203                // @todo How can we test this? What's the intended use-case?
204                foreach ( $config['backends'] as $index => $beConfig ) {
205                    if ( isset( $beConfig['template'] ) ) {
206                        // Config is just a modified version of a registered backend's.
207                        // This should only be used when that config is used only by this backend.
208                        $config['backends'][$index] += $this->config( $beConfig['template'] );
209                    }
210                }
211            }
212
213            $this->backends[$name]['instance'] = new $class( $config );
214        }
215
216        return $this->backends[$name]['instance'];
217    }
218
219    /**
220     * Get the config array for a backend object with a given name
221     *
222     * @param string $name
223     * @return array Parameters to FileBackend::__construct()
224     */
225    public function config( $name ) {
226        if ( !isset( $this->backends[$name] ) ) {
227            throw new InvalidArgumentException( "No backend defined with the name '$name'." );
228        }
229
230        $config = $this->backends[$name]['config'];
231
232        return array_merge(
233            // Default backend parameters
234            [
235                'mimeCallback' => [ $this, 'guessMimeInternal' ],
236                'obResetFunc' => 'wfResetOutputBuffers',
237                'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ],
238                'tmpFileFactory' => $this->tmpFileFactory,
239                'statusWrapper' => [ Status::class, 'wrap' ],
240                'wanCache' => $this->wanCache,
241                'srvCache' => $this->srvCache,
242                'logger' => LoggerFactory::getInstance( 'FileOperation' ),
243                'profiler' => static function ( $section ) {
244                    return Profiler::instance()->scopedProfileIn( $section );
245                }
246            ],
247            // Configured backend parameters
248            $config,
249            // Resolved backend parameters
250            [
251                'class' => $this->backends[$name]['class'],
252                'lockManager' =>
253                    $this->lmgFactory->getLockManagerGroup( $config['domainId'] )
254                        ->get( $config['lockManager'] ),
255            ]
256        );
257    }
258
259    /**
260     * Get an appropriate backend object from a storage path
261     *
262     * @param string $storagePath
263     * @return FileBackend|null Backend or null on failure
264     */
265    public function backendFromPath( $storagePath ) {
266        [ $backend, , ] = FileBackend::splitStoragePath( $storagePath );
267        if ( $backend !== null && isset( $this->backends[$backend] ) ) {
268            return $this->get( $backend );
269        }
270
271        return null;
272    }
273
274    /**
275     * @param string $storagePath
276     * @param string|null $content
277     * @param string|null $fsPath
278     * @return string
279     * @since 1.27
280     */
281    public function guessMimeInternal( $storagePath, $content, $fsPath ) {
282        // Trust the extension of the storage path (caller must validate)
283        $ext = FileBackend::extensionFromPath( $storagePath );
284        $type = $this->mimeAnalyzer->getMimeTypeFromExtensionOrNull( $ext );
285        // For files without a valid extension (or one at all), inspect the contents
286        if ( !$type && $fsPath ) {
287            $type = $this->mimeAnalyzer->guessMimeType( $fsPath, false );
288        } elseif ( !$type && $content !== null && $content !== '' ) {
289            $tmpFile = $this->tmpFileFactory->newTempFSFile( 'mime_', '' );
290            file_put_contents( $tmpFile->getPath(), $content );
291            $type = $this->mimeAnalyzer->guessMimeType( $tmpFile->getPath(), false );
292        }
293        return $type ?: 'unknown/unknown';
294    }
295}
296/** @deprecated class alias since 1.43 */
297class_alias( FileBackendGroup::class, 'FileBackendGroup' );