Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.00% covered (success)
97.00%
97 / 100
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
26
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
5.93
 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
24use MediaWiki\Config\ServiceOptions;
25use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
26use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
27use MediaWiki\Logger\LoggerFactory;
28use MediaWiki\MainConfigNames;
29use MediaWiki\Output\StreamFile;
30use MediaWiki\Status\Status;
31use Wikimedia\ObjectFactory\ObjectFactory;
32use Wikimedia\Rdbms\ReadOnlyMode;
33
34/**
35 * Class to handle file backend registration
36 *
37 * @ingroup FileBackend
38 * @since 1.19
39 */
40class FileBackendGroup {
41    /**
42     * @var array[] (name => ('class' => string, 'config' => array, 'instance' => object))
43     * @phan-var array<string,array{class:class-string,config:array,instance:object}>
44     */
45    protected $backends = [];
46
47    /** @var ServiceOptions */
48    private $options;
49
50    /** @var BagOStuff */
51    private $srvCache;
52
53    /** @var WANObjectCache */
54    private $wanCache;
55
56    /** @var MimeAnalyzer */
57    private $mimeAnalyzer;
58
59    /** @var LockManagerGroupFactory */
60    private $lmgFactory;
61
62    /** @var TempFSFileFactory */
63    private $tmpFileFactory;
64
65    /** @var ObjectFactory */
66    private $objectFactory;
67
68    /**
69     * @internal For use by ServiceWiring
70     */
71    public const CONSTRUCTOR_OPTIONS = [
72        MainConfigNames::DirectoryMode,
73        MainConfigNames::FileBackends,
74        MainConfigNames::ForeignFileRepos,
75        MainConfigNames::LocalFileRepo,
76        'fallbackWikiId',
77    ];
78
79    /**
80     * @param ServiceOptions $options
81     * @param ReadOnlyMode $readOnlyMode
82     * @param BagOStuff $srvCache
83     * @param WANObjectCache $wanCache
84     * @param MimeAnalyzer $mimeAnalyzer
85     * @param LockManagerGroupFactory $lmgFactory
86     * @param TempFSFileFactory $tmpFileFactory
87     * @param ObjectFactory $objectFactory
88     */
89    public function __construct(
90        ServiceOptions $options,
91        ReadOnlyMode $readOnlyMode,
92        BagOStuff $srvCache,
93        WANObjectCache $wanCache,
94        MimeAnalyzer $mimeAnalyzer,
95        LockManagerGroupFactory $lmgFactory,
96        TempFSFileFactory $tmpFileFactory,
97        ObjectFactory $objectFactory
98    ) {
99        $this->options = $options;
100        $this->srvCache = $srvCache;
101        $this->wanCache = $wanCache;
102        $this->mimeAnalyzer = $mimeAnalyzer;
103        $this->lmgFactory = $lmgFactory;
104        $this->tmpFileFactory = $tmpFileFactory;
105        $this->objectFactory = $objectFactory;
106
107        // Register explicitly defined backends
108        $this->register( $options->get( MainConfigNames::FileBackends ), $readOnlyMode->getConfiguredReason() );
109
110        $autoBackends = [];
111        // Automatically create b/c backends for file repos...
112        $repos = array_merge(
113            $options->get( MainConfigNames::ForeignFileRepos ), [ $options->get( MainConfigNames::LocalFileRepo ) ] );
114        foreach ( $repos as $info ) {
115            $backendName = $info['backend'];
116            if ( is_object( $backendName ) || isset( $this->backends[$backendName] ) ) {
117                continue; // already defined (or set to the object for some reason)
118            }
119            $repoName = $info['name'];
120            // Local vars that used to be FSRepo members...
121            $directory = $info['directory'];
122            $deletedDir = $info['deletedDir'] ?? false; // deletion disabled
123            $thumbDir = $info['thumbDir'] ?? "{$directory}/thumb";
124            $transcodedDir = $info['transcodedDir'] ?? "{$directory}/transcoded";
125            $lockManager = $info['lockManager'] ?? 'fsLockManager';
126            // Get the FS backend configuration
127            $autoBackends[] = [
128                'name' => $backendName,
129                'class' => FSFileBackend::class,
130                'lockManager' => $lockManager,
131                'containerPaths' => [
132                    "{$repoName}-public" => "{$directory}",
133                    "{$repoName}-thumb" => $thumbDir,
134                    "{$repoName}-transcoded" => $transcodedDir,
135                    "{$repoName}-deleted" => $deletedDir,
136                    "{$repoName}-temp" => "{$directory}/temp"
137                ],
138                'fileMode' => $info['fileMode'] ?? 0644,
139                'directoryMode' => $options->get( MainConfigNames::DirectoryMode ),
140            ];
141        }
142
143        // Register implicitly defined backends
144        $this->register( $autoBackends, $readOnlyMode->getConfiguredReason() );
145    }
146
147    /**
148     * Register an array of file backend configurations
149     *
150     * @param array[] $configs
151     * @param string|null $readOnlyReason
152     * @throws InvalidArgumentException
153     */
154    protected function register( array $configs, $readOnlyReason = null ) {
155        foreach ( $configs as $config ) {
156            if ( !isset( $config['name'] ) ) {
157                throw new InvalidArgumentException( "Cannot register a backend with no name." );
158            }
159            $name = $config['name'];
160            if ( isset( $this->backends[$name] ) ) {
161                throw new LogicException( "Backend with name '$name' already registered." );
162            } elseif ( !isset( $config['class'] ) ) {
163                throw new InvalidArgumentException( "Backend with name '$name' has no class." );
164            }
165            $class = $config['class'];
166
167            $config['domainId'] ??= $config['wikiId'] ?? $this->options->get( 'fallbackWikiId' );
168            $config['readOnly'] ??= $readOnlyReason;
169
170            unset( $config['class'] ); // backend won't need this
171            $this->backends[$name] = [
172                'class' => $class,
173                'config' => $config,
174                'instance' => null
175            ];
176        }
177    }
178
179    /**
180     * Get the backend object with a given name
181     *
182     * @param string $name
183     * @return FileBackend
184     * @throws InvalidArgumentException
185     */
186    public function get( $name ) {
187        // Lazy-load the actual backend instance
188        if ( !isset( $this->backends[$name]['instance'] ) ) {
189            $config = $this->config( $name );
190
191            $class = $config['class'];
192            if ( $class === FileBackendMultiWrite::class ) {
193                // @todo How can we test this? What's the intended use-case?
194                foreach ( $config['backends'] as $index => $beConfig ) {
195                    if ( isset( $beConfig['template'] ) ) {
196                        // Config is just a modified version of a registered backend's.
197                        // This should only be used when that config is used only by this backend.
198                        $config['backends'][$index] += $this->config( $beConfig['template'] );
199                    }
200                }
201            }
202
203            $this->backends[$name]['instance'] = new $class( $config );
204        }
205
206        return $this->backends[$name]['instance'];
207    }
208
209    /**
210     * Get the config array for a backend object with a given name
211     *
212     * @param string $name
213     * @return array Parameters to FileBackend::__construct()
214     * @throws InvalidArgumentException
215     */
216    public function config( $name ) {
217        if ( !isset( $this->backends[$name] ) ) {
218            throw new InvalidArgumentException( "No backend defined with the name '$name'." );
219        }
220
221        $config = $this->backends[$name]['config'];
222
223        return array_merge(
224            // Default backend parameters
225            [
226                'mimeCallback' => [ $this, 'guessMimeInternal' ],
227                'obResetFunc' => 'wfResetOutputBuffers',
228                'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ],
229                'tmpFileFactory' => $this->tmpFileFactory,
230                'statusWrapper' => [ Status::class, 'wrap' ],
231                'wanCache' => $this->wanCache,
232                'srvCache' => $this->srvCache,
233                'logger' => LoggerFactory::getInstance( 'FileOperation' ),
234                'profiler' => static function ( $section ) {
235                    return Profiler::instance()->scopedProfileIn( $section );
236                }
237            ],
238            // Configured backend parameters
239            $config,
240            // Resolved backend parameters
241            [
242                'class' => $this->backends[$name]['class'],
243                'lockManager' =>
244                    $this->lmgFactory->getLockManagerGroup( $config['domainId'] )
245                        ->get( $config['lockManager'] ),
246            ]
247        );
248    }
249
250    /**
251     * Get an appropriate backend object from a storage path
252     *
253     * @param string $storagePath
254     * @return FileBackend|null Backend or null on failure
255     */
256    public function backendFromPath( $storagePath ) {
257        [ $backend, , ] = FileBackend::splitStoragePath( $storagePath );
258        if ( $backend !== null && isset( $this->backends[$backend] ) ) {
259            return $this->get( $backend );
260        }
261
262        return null;
263    }
264
265    /**
266     * @param string $storagePath
267     * @param string|null $content
268     * @param string|null $fsPath
269     * @return string
270     * @since 1.27
271     */
272    public function guessMimeInternal( $storagePath, $content, $fsPath ) {
273        // Trust the extension of the storage path (caller must validate)
274        $ext = FileBackend::extensionFromPath( $storagePath );
275        $type = $this->mimeAnalyzer->getMimeTypeFromExtensionOrNull( $ext );
276        // For files without a valid extension (or one at all), inspect the contents
277        if ( !$type && $fsPath ) {
278            $type = $this->mimeAnalyzer->guessMimeType( $fsPath, false );
279        } elseif ( !$type && $content !== null && $content !== '' ) {
280            $tmpFile = $this->tmpFileFactory->newTempFSFile( 'mime_', '' );
281            file_put_contents( $tmpFile->getPath(), $content );
282            $type = $this->mimeAnalyzer->guessMimeType( $tmpFile->getPath(), false );
283        }
284        return $type ?: 'unknown/unknown';
285    }
286}