Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.67% covered (success)
96.67%
174 / 180
94.55% covered (success)
94.55%
52 / 55
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileBackend
97.21% covered (success)
97.21%
174 / 179
94.55% covered (success)
94.55%
52 / 55
113
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
10
 header
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 callNowOrLater
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 resetOutputBuffer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDomainId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isReadOnly
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReadOnlyReason
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getFeatures
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasFeatures
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doOperations
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 doOperationsInternal
n/a
0 / 0
n/a
0 / 0
0
 doOperation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 store
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 move
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 describe
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doQuickOperations
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 doQuickOperationsInternal
n/a
0 / 0
n/a
0 / 0
0
 doQuickOperation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickCreate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickStore
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickCopy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickMove
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickDescribe
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 concatenate
n/a
0 / 0
n/a
0 / 0
0
 prepare
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 doPrepare
n/a
0 / 0
n/a
0 / 0
0
 secure
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 doSecure
n/a
0 / 0
n/a
0 / 0
0
 publish
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 doPublish
n/a
0 / 0
n/a
0 / 0
0
 clean
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 doClean
n/a
0 / 0
n/a
0 / 0
0
 fileExists
n/a
0 / 0
n/a
0 / 0
0
 getFileTimestamp
n/a
0 / 0
n/a
0 / 0
0
 getFileContents
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFileContentsMulti
n/a
0 / 0
n/a
0 / 0
0
 getFileXAttributes
n/a
0 / 0
n/a
0 / 0
0
 getFileSize
n/a
0 / 0
n/a
0 / 0
0
 getFileStat
n/a
0 / 0
n/a
0 / 0
0
 getFileSha1Base36
n/a
0 / 0
n/a
0 / 0
0
 getFileProps
n/a
0 / 0
n/a
0 / 0
0
 streamFile
n/a
0 / 0
n/a
0 / 0
0
 getLocalReference
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLocalReferenceMulti
n/a
0 / 0
n/a
0 / 0
0
 getLocalCopy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLocalCopyMulti
n/a
0 / 0
n/a
0 / 0
0
 getFileHttpUrl
n/a
0 / 0
n/a
0 / 0
0
 addShellboxInputFile
n/a
0 / 0
n/a
0 / 0
0
 directoryExists
n/a
0 / 0
n/a
0 / 0
0
 getDirectoryList
n/a
0 / 0
n/a
0 / 0
0
 getTopDirectoryList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFileList
n/a
0 / 0
n/a
0 / 0
0
 getTopFileList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 preloadCache
n/a
0 / 0
n/a
0 / 0
0
 clearCache
n/a
0 / 0
n/a
0 / 0
0
 preloadFileStat
n/a
0 / 0
n/a
0 / 0
0
 lockFiles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 unlockFiles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getScopedFileLocks
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getScopedLocksForOps
n/a
0 / 0
n/a
0 / 0
0
 getRootStoragePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContainerStoragePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveFSFileObjects
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isStoragePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 splitStoragePath
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 normalizeStoragePath
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 parentStoragePath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 extensionFromPath
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 isPathTraversalFree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeContentDisposition
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 normalizeContainerPath
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 newStatus
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 wrapStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 scopedProfileSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 resetOutputBufferTheDefaultWay
n/a
0 / 0
n/a
0 / 0
3
 getStreamerOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @defgroup FileBackend File backend
4 *
5 * File backend is used to interact with file storage systems,
6 * such as the local file system, NFS, or cloud storage systems.
7 * See [the architecture doc](@ref filebackendarch) for more information.
8 */
9
10/**
11 * Base class for all file backends.
12 *
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License along
24 * with this program; if not, write to the Free Software Foundation, Inc.,
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
26 * http://www.gnu.org/copyleft/gpl.html
27 *
28 * @file
29 * @ingroup FileBackend
30 */
31
32namespace Wikimedia\FileBackend;
33
34use InvalidArgumentException;
35use LockManager;
36use NullLockManager;
37use Psr\Log\LoggerAwareInterface;
38use Psr\Log\LoggerInterface;
39use Psr\Log\NullLogger;
40use ScopedLock;
41use Shellbox\Command\BoxedCommand;
42use StatusValue;
43use Wikimedia\FileBackend\FSFile\FSFile;
44use Wikimedia\FileBackend\FSFile\TempFSFile;
45use Wikimedia\FileBackend\FSFile\TempFSFileFactory;
46use Wikimedia\Message\MessageParam;
47use Wikimedia\Message\MessageSpecifier;
48use Wikimedia\ScopedCallback;
49
50/**
51 * @brief Base class for all file backend classes (including multi-write backends).
52 *
53 * This class defines the methods as abstract that subclasses must implement.
54 * Outside callers can assume that all backends will have these functions.
55 *
56 * All "storage paths" are of the format "mwstore://<backend>/<container>/<path>".
57 * The "backend" portion is unique name for the application to refer to a backend, while
58 * the "container" portion is a top-level directory of the backend. The "path" portion
59 * is a relative path that uses UNIX file system (FS) notation, though any particular
60 * backend may not actually be using a local filesystem. Therefore, the relative paths
61 * are only virtual.
62 *
63 * Backend contents are stored under "domain"-specific container names by default.
64 * A domain is simply a logical umbrella for entities, such as those belonging to a certain
65 * application or portion of a website, for example. A domain can be local or global.
66 * Global (qualified) backends are achieved by configuring the "domain ID" to a constant.
67 * Global domains are simpler, but local domains can be used by choosing a domain ID based on
68 * the current context, such as which language of a website is being used.
69 *
70 * For legacy reasons, the FSFileBackend class allows manually setting the paths of
71 * containers to ones that do not respect the "domain ID".
72 *
73 * In key/value (object) stores, containers are the only hierarchy (the rest is emulated).
74 * FS-based backends are somewhat more restrictive due to the existence of real
75 * directory files; a regular file cannot have the same name as a directory. Other
76 * backends with virtual directories may not have this limitation. Callers should
77 * store files in such a way that no files and directories are under the same path.
78 *
79 * In general, this class allows for callers to access storage through the same
80 * interface, without regard to the underlying storage system. However, calling code
81 * must follow certain patterns and be aware of certain things to ensure compatibility:
82 *   - a) Always call prepare() on the parent directory before trying to put a file there;
83 *        key/value stores only need the container to exist first, but filesystems need
84 *        all the parent directories to exist first (prepare() is aware of all this)
85 *   - b) Always call clean() on a directory when it might become empty to avoid empty
86 *        directory buildup on filesystems; key/value stores never have empty directories,
87 *        so doing this helps preserve consistency in both cases
88 *   - c) Likewise, do not rely on the existence of empty directories for anything;
89 *        calling directoryExists() on a path that prepare() was previously called on
90 *        will return false for key/value stores if there are no files under that path
91 *   - d) Never alter the resulting FSFile returned from getLocalReference(), as it could
92 *        either be a copy of the source file in /tmp or the original source file itself
93 *   - e) Use a file layout that results in never attempting to store files over directories
94 *        or directories over files; key/value stores allow this but filesystems do not
95 *   - f) Use ASCII file names (e.g. base32, IDs, hashes) to avoid Unicode issues in Windows
96 *   - g) Do not assume that move operations are atomic (difficult with key/value stores)
97 *   - h) Do not assume that file stat or read operations always have immediate consistency;
98 *        various methods have a "latest" flag that should always be used if up-to-date
99 *        information is required (this trades performance for correctness as needed)
100 *   - i) Do not assume that directory listings have immediate consistency
101 *
102 * Methods of subclasses should avoid throwing exceptions at all costs.
103 * As a corollary, external dependencies should be kept to a minimum.
104 *
105 * See [the architecture doc](@ref filebackendarch) for more information.
106 *
107 * @stable to extend
108 *
109 * @ingroup FileBackend
110 * @since 1.19
111 */
112abstract class FileBackend implements LoggerAwareInterface {
113    /** @var string Unique backend name */
114    protected $name;
115
116    /** @var string Unique domain name */
117    protected $domainId;
118
119    /** @var string Read-only explanation message */
120    protected $readOnly;
121
122    /** @var string When to do operations in parallel */
123    protected $parallelize;
124
125    /** @var int How many operations can be done in parallel */
126    protected $concurrency;
127
128    /** @var TempFSFileFactory */
129    protected $tmpFileFactory;
130
131    /** @var LockManager */
132    protected $lockManager;
133    /** @var LoggerInterface */
134    protected $logger;
135    /** @var callable|null */
136    protected $profiler;
137
138    /** @var callable */
139    private $obResetFunc;
140    /** @var callable */
141    private $headerFunc;
142    /** @var callable */
143    private $asyncHandler;
144    /** @var array Option map for use with HTTPFileStreamer */
145    protected $streamerOptions;
146    /** @var callable|null */
147    protected $statusWrapper;
148
149    /** Bitfield flags for supported features */
150    public const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers
151    public const ATTR_METADATA = 2; // files can be stored with metadata key/values
152    public const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
153
154    /** @var false Idiom for "no info; non-existant file" (since 1.34) */
155    protected const STAT_ABSENT = false;
156
157    /** @var null Idiom for "no info; I/O errors" (since 1.34) */
158    public const STAT_ERROR = null;
159    /** @var null Idiom for "no file/directory list; I/O errors" (since 1.34) */
160    public const LIST_ERROR = null;
161    /** @var null Idiom for "no temp URL; not supported or I/O errors" (since 1.34) */
162    public const TEMPURL_ERROR = null;
163    /** @var null Idiom for "existence unknown; I/O errors" (since 1.34) */
164    public const EXISTENCE_ERROR = null;
165
166    /** @var false Idiom for "no timestamp; missing file or I/O errors" (since 1.34) */
167    public const TIMESTAMP_FAIL = false;
168    /** @var false Idiom for "no content; missing file or I/O errors" (since 1.34) */
169    public const CONTENT_FAIL = false;
170    /** @var false Idiom for "no metadata; missing file or I/O errors" (since 1.34) */
171    public const XATTRS_FAIL = false;
172    /** @var false Idiom for "no size; missing file or I/O errors" (since 1.34) */
173    public const SIZE_FAIL = false;
174    /** @var false Idiom for "no SHA1 hash; missing file or I/O errors" (since 1.34) */
175    public const SHA1_FAIL = false;
176
177    /**
178     * Create a new backend instance from configuration.
179     * This should only be called from within FileBackendGroup.
180     * @stable to call
181     *
182     * @param array $config Parameters include:
183     *   - name : The unique name of this backend.
184     *      This should consist of alphanumberic, '-', and '_' characters.
185     *      This name should not be changed after use.
186     *      Note that the name is *not* used in actual container names.
187     *   - domainId : Prefix to container names that is unique to this backend.
188     *      It should only consist of alphanumberic, '-', and '_' characters.
189     *      This ID is what avoids collisions if multiple logical backends
190     *      use the same storage system, so this should be set carefully.
191     *   - lockManager : LockManager object to use for any file locking.
192     *      If not provided, then no file locking will be enforced.
193     *   - readOnly : Write operations are disallowed if this is a non-empty string.
194     *      It should be an explanation for the backend being read-only.
195     *   - parallelize : When to do file operations in parallel (when possible).
196     *      Allowed values are "implicit", "explicit" and "off".
197     *   - concurrency : How many file operations can be done in parallel.
198     *   - tmpDirectory : Directory to use for temporary files.
199     *   - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if
200     *      tmpDirectory is not set. If both are unset or null, then the backend will
201     *      try to discover a usable temporary directory.
202     *   - obResetFunc : alternative callback to clear the output buffer
203     *   - streamMimeFunc : alternative method to determine the content type from the path
204     *   - headerFunc : alternative callback for sending response headers
205     *   - asyncHandler : callback for scheduling deferred updated
206     *   - logger : Optional PSR logger object.
207     *   - profiler : Optional callback that takes a section name argument and returns
208     *      a ScopedCallback instance that ends the profile section in its destructor.
209     *   - statusWrapper : Optional callback that is used to wrap returned StatusValues
210     */
211    public function __construct( array $config ) {
212        if ( !array_key_exists( 'name', $config ) ) {
213            throw new InvalidArgumentException( 'Backend name not specified.' );
214        }
215        $this->name = $config['name'];
216        $this->domainId = $config['domainId'] // e.g. "my_wiki-en_"
217            ?? $config['wikiId'] // b/c alias
218            ?? null;
219        if ( !is_string( $this->name ) || !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
220            throw new InvalidArgumentException( "Backend name '{$this->name}' is invalid." );
221        }
222        if ( !is_string( $this->domainId ) ) {
223            throw new InvalidArgumentException(
224                "Backend domain ID not provided for '{$this->name}'." );
225        }
226        $this->lockManager = $config['lockManager'] ?? new NullLockManager( [] );
227        $this->readOnly = isset( $config['readOnly'] )
228            ? (string)$config['readOnly']
229            : '';
230        $this->parallelize = isset( $config['parallelize'] )
231            ? (string)$config['parallelize']
232            : 'off';
233        $this->concurrency = isset( $config['concurrency'] )
234            ? (int)$config['concurrency']
235            : 50;
236        $this->obResetFunc = $config['obResetFunc']
237            ?? [ self::class, 'resetOutputBufferTheDefaultWay' ];
238        $this->headerFunc = $config['headerFunc'] ?? 'header';
239        $this->asyncHandler = $config['asyncHandler'] ?? null;
240        $this->streamerOptions = [
241            'obResetFunc' => $this->obResetFunc,
242            'headerFunc' => $this->headerFunc,
243            'streamMimeFunc' => $config['streamMimeFunc'] ?? null,
244        ];
245
246        $this->profiler = $config['profiler'] ?? null;
247        if ( !is_callable( $this->profiler ) ) {
248            $this->profiler = null;
249        }
250        $this->logger = $config['logger'] ?? new NullLogger();
251        $this->statusWrapper = $config['statusWrapper'] ?? null;
252        // tmpDirectory gets precedence for backward compatibility
253        if ( isset( $config['tmpDirectory'] ) ) {
254            $this->tmpFileFactory = new TempFSFileFactory( $config['tmpDirectory'] );
255        } else {
256            $this->tmpFileFactory = $config['tmpFileFactory'] ?? new TempFSFileFactory();
257        }
258    }
259
260    protected function header( $header ) {
261        ( $this->headerFunc )( $header );
262    }
263
264    /**
265     * @param callable $update
266     *
267     * @return void
268     */
269    protected function callNowOrLater( callable $update ) {
270        if ( $this->asyncHandler ) {
271            ( $this->asyncHandler )( $update );
272        } else {
273            $update();
274        }
275    }
276
277    protected function resetOutputBuffer() {
278        // By default, this ends up calling $this->defaultOutputBufferReset
279        ( $this->obResetFunc )();
280    }
281
282    public function setLogger( LoggerInterface $logger ) {
283        $this->logger = $logger;
284    }
285
286    /**
287     * Get the unique backend name
288     *
289     * We may have multiple different backends of the same type.
290     * For example, we can have two Swift backends using different proxies.
291     *
292     * @return string
293     */
294    final public function getName() {
295        return $this->name;
296    }
297
298    /**
299     * Get the domain identifier used for this backend (possibly empty).
300     *
301     * @return string
302     * @since 1.28
303     */
304    final public function getDomainId() {
305        return $this->domainId;
306    }
307
308    /**
309     * Alias to getDomainId()
310     *
311     * @return string
312     * @since 1.20
313     * @deprecated Since 1.34 Use getDomainId()
314     */
315    final public function getWikiId() {
316        return $this->getDomainId();
317    }
318
319    /**
320     * Check if this backend is read-only
321     *
322     * @return bool
323     */
324    final public function isReadOnly() {
325        return ( $this->readOnly != '' );
326    }
327
328    /**
329     * Get an explanatory message if this backend is read-only
330     *
331     * @return string|bool Returns false if the backend is not read-only
332     */
333    final public function getReadOnlyReason() {
334        return ( $this->readOnly != '' ) ? $this->readOnly : false;
335    }
336
337    /**
338     * Get the a bitfield of extra features supported by the backend medium
339     * @stable to override
340     *
341     * @return int Bitfield of FileBackend::ATTR_* flags
342     * @since 1.23
343     */
344    public function getFeatures() {
345        return self::ATTR_UNICODE_PATHS;
346    }
347
348    /**
349     * Check if the backend medium supports a field of extra features
350     *
351     * @param int $bitfield Bitfield of FileBackend::ATTR_* flags
352     * @return bool
353     * @since 1.23
354     */
355    final public function hasFeatures( $bitfield ) {
356        return ( $this->getFeatures() & $bitfield ) === $bitfield;
357    }
358
359    /**
360     * This is the main entry point into the backend for write operations.
361     * Callers supply an ordered list of operations to perform as a transaction.
362     * Files will be locked, the stat cache cleared, and then the operations attempted.
363     * If any serious errors occur, all attempted operations will be rolled back.
364     *
365     * $ops is an array of arrays. The outer array holds a list of operations.
366     * Each inner array is a set of key value pairs that specify an operation.
367     *
368     * Supported operations and their parameters. The supported actions are:
369     *  - create
370     *  - store
371     *  - copy
372     *  - move
373     *  - delete
374     *  - describe (since 1.21)
375     *  - null
376     *
377     * FSFile/TempFSFile object support was added in 1.27.
378     *
379     * a) Create a new file in storage with the contents of a string
380     * @code
381     *     [
382     *         'op'                  => 'create',
383     *         'dst'                 => <storage path>,
384     *         'content'             => <string of new file contents>,
385     *         'overwrite'           => <boolean>,
386     *         'overwriteSame'       => <boolean>,
387     *         'headers'             => <HTTP header name/value map> # since 1.21
388     *     ]
389     * @endcode
390     *
391     * b) Copy a file system file into storage
392     * @code
393     *     [
394     *         'op'                  => 'store',
395     *         'src'                 => <file system path, FSFile, or TempFSFile>,
396     *         'dst'                 => <storage path>,
397     *         'overwrite'           => <boolean>,
398     *         'overwriteSame'       => <boolean>,
399     *         'headers'             => <HTTP header name/value map> # since 1.21
400     *     ]
401     * @endcode
402     *
403     * c) Copy a file within storage
404     * @code
405     *     [
406     *         'op'                  => 'copy',
407     *         'src'                 => <storage path>,
408     *         'dst'                 => <storage path>,
409     *         'overwrite'           => <boolean>,
410     *         'overwriteSame'       => <boolean>,
411     *         'ignoreMissingSource' => <boolean>, # since 1.21
412     *         'headers'             => <HTTP header name/value map> # since 1.21
413     *     ]
414     * @endcode
415     *
416     * d) Move a file within storage
417     * @code
418     *     [
419     *         'op'                  => 'move',
420     *         'src'                 => <storage path>,
421     *         'dst'                 => <storage path>,
422     *         'overwrite'           => <boolean>,
423     *         'overwriteSame'       => <boolean>,
424     *         'ignoreMissingSource' => <boolean>, # since 1.21
425     *         'headers'             => <HTTP header name/value map> # since 1.21
426     *     ]
427     * @endcode
428     *
429     * e) Delete a file within storage
430     * @code
431     *     [
432     *         'op'                  => 'delete',
433     *         'src'                 => <storage path>,
434     *         'ignoreMissingSource' => <boolean>
435     *     ]
436     * @endcode
437     *
438     * f) Update metadata for a file within storage
439     * @code
440     *     [
441     *         'op'                  => 'describe',
442     *         'src'                 => <storage path>,
443     *         'headers'             => <HTTP header name/value map>
444     *     ]
445     * @endcode
446     *
447     * g) Do nothing (no-op)
448     * @code
449     *     [
450     *         'op'                  => 'null',
451     *     ]
452     * @endcode
453     *
454     * Boolean flags for operations (operation-specific):
455     *   - ignoreMissingSource : The operation will simply succeed and do
456     *                           nothing if the source file does not exist.
457     *   - overwrite           : Any destination file will be overwritten.
458     *   - overwriteSame       : If a file already exists at the destination with the
459     *                           same contents, then do nothing to the destination file
460     *                           instead of giving an error. This does not compare headers.
461     *                           This option is ignored if 'overwrite' is already provided.
462     *   - headers             : If supplied, the result of merging these headers with any
463     *                           existing source file headers (replacing conflicting ones)
464     *                           will be set as the destination file headers. Headers are
465     *                           deleted if their value is set to the empty string. When a
466     *                           file has headers they are included in responses to GET and
467     *                           HEAD requests to the backing store for that file.
468     *                           Header values should be no larger than 255 bytes, except for
469     *                           Content-Disposition. The system might ignore or truncate any
470     *                           headers that are too long to store (exact limits will vary).
471     *                           Backends that don't support metadata ignore this. (since 1.21)
472     *
473     * $opts is an associative of boolean flags, including:
474     *   - force               : Operation precondition errors no longer trigger an abort.
475     *                           Any remaining operations are still attempted. Unexpected
476     *                           failures may still cause remaining operations to be aborted.
477     *   - nonLocking          : No locks are acquired for the operations.
478     *                           This can increase performance for non-critical writes.
479     *                           This has no effect unless the 'force' flag is set.
480     *   - parallelize         : Try to do operations in parallel when possible.
481     *   - bypassReadOnly      : Allow writes in read-only mode. (since 1.20)
482     *   - preserveCache       : Don't clear the process cache before checking files.
483     *                           This should only be used if all entries in the process
484     *                           cache were added after the files were already locked. (since 1.20)
485     *
486     * @note Remarks on locking:
487     * File system paths given to operations should refer to files that are
488     * already locked or otherwise safe from modification from other processes.
489     * Normally these files will be new temp files, which should be adequate.
490     *
491     * @par Return value:
492     *
493     * This returns a Status, which contains all warnings and fatals that occurred
494     * during the operation. The 'failCount', 'successCount', and 'success' members
495     * will reflect each operation attempted.
496     *
497     * The StatusValue will be "OK" unless:
498     *   - a) unexpected operation errors occurred (network partitions, disk full...)
499     *   - b) predicted operation errors occurred and 'force' was not set
500     *
501     * @param array[] $ops List of operations to execute in order
502     * @phan-param array<int,array{ignoreMissingSource?:bool,overwrite?:bool,overwriteSame?:bool,headers?:bool}> $ops
503     * @param array $opts Batch operation options
504     * @phpcs:ignore Generic.Files.LineLength
505     * @phan-param array{force?:bool,nonLocking?:bool,parallelize?:bool,bypassReadOnly?:bool,preserveCache?:bool} $opts
506     * @return StatusValue
507     */
508    final public function doOperations( array $ops, array $opts = [] ) {
509        if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
510            return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
511        }
512        if ( $ops === [] ) {
513            return $this->newStatus(); // nothing to do
514        }
515
516        $ops = $this->resolveFSFileObjects( $ops );
517        if ( empty( $opts['force'] ) ) {
518            unset( $opts['nonLocking'] );
519        }
520
521        /** @noinspection PhpUnusedLocalVariableInspection */
522        $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
523
524        return $this->doOperationsInternal( $ops, $opts );
525    }
526
527    /**
528     * @see FileBackend::doOperations()
529     * @param array $ops
530     * @param array $opts
531     * @return StatusValue
532     */
533    abstract protected function doOperationsInternal( array $ops, array $opts );
534
535    /**
536     * Same as doOperations() except it takes a single operation.
537     * If you are doing a batch of operations that should either
538     * all succeed or all fail, then use that function instead.
539     *
540     * @see FileBackend::doOperations()
541     *
542     * @param array $op Operation
543     * @param array $opts Operation options
544     * @return StatusValue
545     */
546    final public function doOperation( array $op, array $opts = [] ) {
547        return $this->doOperations( [ $op ], $opts );
548    }
549
550    /**
551     * Performs a single create operation.
552     * This sets $params['op'] to 'create' and passes it to doOperation().
553     *
554     * @see FileBackend::doOperation()
555     *
556     * @param array $params Operation parameters
557     * @param array $opts Operation options
558     * @return StatusValue
559     */
560    final public function create( array $params, array $opts = [] ) {
561        return $this->doOperation( [ 'op' => 'create' ] + $params, $opts );
562    }
563
564    /**
565     * Performs a single store operation.
566     * This sets $params['op'] to 'store' and passes it to doOperation().
567     *
568     * @see FileBackend::doOperation()
569     *
570     * @param array $params Operation parameters
571     * @param array $opts Operation options
572     * @return StatusValue
573     */
574    final public function store( array $params, array $opts = [] ) {
575        return $this->doOperation( [ 'op' => 'store' ] + $params, $opts );
576    }
577
578    /**
579     * Performs a single copy operation.
580     * This sets $params['op'] to 'copy' and passes it to doOperation().
581     *
582     * @see FileBackend::doOperation()
583     *
584     * @param array $params Operation parameters
585     * @param array $opts Operation options
586     * @return StatusValue
587     */
588    final public function copy( array $params, array $opts = [] ) {
589        return $this->doOperation( [ 'op' => 'copy' ] + $params, $opts );
590    }
591
592    /**
593     * Performs a single move operation.
594     * This sets $params['op'] to 'move' and passes it to doOperation().
595     *
596     * @see FileBackend::doOperation()
597     *
598     * @param array $params Operation parameters
599     * @param array $opts Operation options
600     * @return StatusValue
601     */
602    final public function move( array $params, array $opts = [] ) {
603        return $this->doOperation( [ 'op' => 'move' ] + $params, $opts );
604    }
605
606    /**
607     * Performs a single delete operation.
608     * This sets $params['op'] to 'delete' and passes it to doOperation().
609     *
610     * @see FileBackend::doOperation()
611     *
612     * @param array $params Operation parameters
613     * @param array $opts Operation options
614     * @return StatusValue
615     */
616    final public function delete( array $params, array $opts = [] ) {
617        return $this->doOperation( [ 'op' => 'delete' ] + $params, $opts );
618    }
619
620    /**
621     * Performs a single describe operation.
622     * This sets $params['op'] to 'describe' and passes it to doOperation().
623     *
624     * @see FileBackend::doOperation()
625     *
626     * @param array $params Operation parameters
627     * @param array $opts Operation options
628     * @return StatusValue
629     * @since 1.21
630     */
631    final public function describe( array $params, array $opts = [] ) {
632        return $this->doOperation( [ 'op' => 'describe' ] + $params, $opts );
633    }
634
635    /**
636     * Perform a set of independent file operations on some files.
637     *
638     * This does no locking, and possibly no stat calls.
639     * Any destination files that already exist will be overwritten.
640     * This should *only* be used on non-original files, like cache files.
641     *
642     * Supported operations and their parameters:
643     *  - create
644     *  - store
645     *  - copy
646     *  - move
647     *  - delete
648     *  - describe (since 1.21)
649     *  - null
650     *
651     * FSFile/TempFSFile object support was added in 1.27.
652     *
653     * a) Create a new file in storage with the contents of a string
654     * @code
655     *     [
656     *         'op'                  => 'create',
657     *         'dst'                 => <storage path>,
658     *         'content'             => <string of new file contents>,
659     *         'headers'             => <HTTP header name/value map> # since 1.21
660     *     ]
661     * @endcode
662     *
663     * b) Copy a file system file into storage
664     * @code
665     *     [
666     *         'op'                  => 'store',
667     *         'src'                 => <file system path, FSFile, or TempFSFile>,
668     *         'dst'                 => <storage path>,
669     *         'headers'             => <HTTP header name/value map> # since 1.21
670     *     ]
671     * @endcode
672     *
673     * c) Copy a file within storage
674     * @code
675     *     [
676     *         'op'                  => 'copy',
677     *         'src'                 => <storage path>,
678     *         'dst'                 => <storage path>,
679     *         'ignoreMissingSource' => <boolean>, # since 1.21
680     *         'headers'             => <HTTP header name/value map> # since 1.21
681     *     ]
682     * @endcode
683     *
684     * d) Move a file within storage
685     * @code
686     *     [
687     *         'op'                  => 'move',
688     *         'src'                 => <storage path>,
689     *         'dst'                 => <storage path>,
690     *         'ignoreMissingSource' => <boolean>, # since 1.21
691     *         'headers'             => <HTTP header name/value map> # since 1.21
692     *     ]
693     * @endcode
694     *
695     * e) Delete a file within storage
696     * @code
697     *     [
698     *         'op'                  => 'delete',
699     *         'src'                 => <storage path>,
700     *         'ignoreMissingSource' => <boolean>
701     *     ]
702     * @endcode
703     *
704     * f) Update metadata for a file within storage
705     * @code
706     *     [
707     *         'op'                  => 'describe',
708     *         'src'                 => <storage path>,
709     *         'headers'             => <HTTP header name/value map>
710     *     ]
711     * @endcode
712     *
713     * g) Do nothing (no-op)
714     * @code
715     *     [
716     *         'op'                  => 'null',
717     *     ]
718     * @endcode
719     *
720     * @par Boolean flags for operations (operation-specific):
721     *   - ignoreMissingSource : The operation will simply succeed and do
722     *                           nothing if the source file does not exist.
723     *   - headers             : If supplied with a header name/value map, the backend will
724     *                           reply with these headers when GETs/HEADs of the destination
725     *                           file are made. Header values should be smaller than 256 bytes.
726     *                           Content-Disposition headers can be longer, though the system
727     *                           might ignore or truncate ones that are too long to store.
728     *                           Existing headers will remain, but these will replace any
729     *                           conflicting previous headers, and headers will be removed
730     *                           if they are set to an empty string.
731     *                           Backends that don't support metadata ignore this. (since 1.21)
732     *
733     * $opts is an associative of boolean flags, including:
734     *   - bypassReadOnly      : Allow writes in read-only mode (since 1.20)
735     *
736     * @par Return value:
737     * This returns a Status, which contains all warnings and fatals that occurred
738     * during the operation. The 'failCount', 'successCount', and 'success' members
739     * will reflect each operation attempted for the given files. The StatusValue will be
740     * considered "OK" as long as no fatal errors occurred.
741     *
742     * @param array $ops Set of operations to execute
743     * @phan-param list<array{op:?string,src?:string,dst?:string,ignoreMissingSource?:bool,headers?:array}> $ops
744     * @param array $opts Batch operation options
745     * @phan-param array{bypassReadOnly?:bool} $opts
746     * @return StatusValue
747     * @since 1.20
748     */
749    final public function doQuickOperations( array $ops, array $opts = [] ) {
750        if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
751            return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
752        }
753        if ( $ops === [] ) {
754            return $this->newStatus(); // nothing to do
755        }
756
757        $ops = $this->resolveFSFileObjects( $ops );
758        foreach ( $ops as &$op ) {
759            $op['overwrite'] = true; // avoids RTTs in key/value stores
760        }
761
762        /** @noinspection PhpUnusedLocalVariableInspection */
763        $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
764
765        return $this->doQuickOperationsInternal( $ops, $opts );
766    }
767
768    /**
769     * @see FileBackend::doQuickOperations()
770     * @param array $ops
771     * @param array $opts
772     * @return StatusValue
773     * @since 1.20
774     */
775    abstract protected function doQuickOperationsInternal( array $ops, array $opts );
776
777    /**
778     * Same as doQuickOperations() except it takes a single operation.
779     * If you are doing a batch of operations, then use that function instead.
780     *
781     * @see FileBackend::doQuickOperations()
782     *
783     * @param array $op Operation
784     * @param array $opts Batch operation options
785     * @return StatusValue
786     * @since 1.20
787     */
788    final public function doQuickOperation( array $op, array $opts = [] ) {
789        return $this->doQuickOperations( [ $op ], $opts );
790    }
791
792    /**
793     * Performs a single quick create operation.
794     * This sets $params['op'] to 'create' and passes it to doQuickOperation().
795     *
796     * @see FileBackend::doQuickOperation()
797     *
798     * @param array $params Operation parameters
799     * @param array $opts Operation options
800     * @return StatusValue
801     * @since 1.20
802     */
803    final public function quickCreate( array $params, array $opts = [] ) {
804        return $this->doQuickOperation( [ 'op' => 'create' ] + $params, $opts );
805    }
806
807    /**
808     * Performs a single quick store operation.
809     * This sets $params['op'] to 'store' and passes it to doQuickOperation().
810     *
811     * @see FileBackend::doQuickOperation()
812     *
813     * @param array $params Operation parameters
814     * @param array $opts Operation options
815     * @return StatusValue
816     * @since 1.20
817     */
818    final public function quickStore( array $params, array $opts = [] ) {
819        return $this->doQuickOperation( [ 'op' => 'store' ] + $params, $opts );
820    }
821
822    /**
823     * Performs a single quick copy operation.
824     * This sets $params['op'] to 'copy' and passes it to doQuickOperation().
825     *
826     * @see FileBackend::doQuickOperation()
827     *
828     * @param array $params Operation parameters
829     * @param array $opts Operation options
830     * @return StatusValue
831     * @since 1.20
832     */
833    final public function quickCopy( array $params, array $opts = [] ) {
834        return $this->doQuickOperation( [ 'op' => 'copy' ] + $params, $opts );
835    }
836
837    /**
838     * Performs a single quick move operation.
839     * This sets $params['op'] to 'move' and passes it to doQuickOperation().
840     *
841     * @see FileBackend::doQuickOperation()
842     *
843     * @param array $params Operation parameters
844     * @param array $opts Operation options
845     * @return StatusValue
846     * @since 1.20
847     */
848    final public function quickMove( array $params, array $opts = [] ) {
849        return $this->doQuickOperation( [ 'op' => 'move' ] + $params, $opts );
850    }
851
852    /**
853     * Performs a single quick delete operation.
854     * This sets $params['op'] to 'delete' and passes it to doQuickOperation().
855     *
856     * @see FileBackend::doQuickOperation()
857     *
858     * @param array $params Operation parameters
859     * @param array $opts Operation options
860     * @return StatusValue
861     * @since 1.20
862     */
863    final public function quickDelete( array $params, array $opts = [] ) {
864        return $this->doQuickOperation( [ 'op' => 'delete' ] + $params, $opts );
865    }
866
867    /**
868     * Performs a single quick describe operation.
869     * This sets $params['op'] to 'describe' and passes it to doQuickOperation().
870     *
871     * @see FileBackend::doQuickOperation()
872     *
873     * @param array $params Operation parameters
874     * @param array $opts Operation options
875     * @return StatusValue
876     * @since 1.21
877     */
878    final public function quickDescribe( array $params, array $opts = [] ) {
879        return $this->doQuickOperation( [ 'op' => 'describe' ] + $params, $opts );
880    }
881
882    /**
883     * Concatenate a list of storage files into a single file system file.
884     * The target path should refer to a file that is already locked or
885     * otherwise safe from modification from other processes. Normally,
886     * the file will be a new temp file, which should be adequate.
887     *
888     * @param array $params Operation parameters, include:
889     *   - srcs        : ordered source storage paths (e.g. chunk1, chunk2, ...)
890     *   - dst         : file system path to 0-byte temp file
891     *   - parallelize : try to do operations in parallel when possible
892     * @return StatusValue
893     */
894    abstract public function concatenate( array $params );
895
896    /**
897     * Prepare a storage directory for usage.
898     * This will create any required containers and parent directories.
899     * Backends using key/value stores only need to create the container.
900     *
901     * The 'noAccess' and 'noListing' parameters works the same as in secure(),
902     * except they are only applied *if* the directory/container had to be created.
903     * These flags should always be set for directories that have private files.
904     * However, setting them is not guaranteed to actually do anything.
905     * Additional server configuration may be needed to achieve the desired effect.
906     *
907     * @param array $params Parameters include:
908     *   - dir            : storage directory
909     *   - noAccess       : try to deny file access (since 1.20)
910     *   - noListing      : try to deny file listing (since 1.20)
911     *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
912     * @return StatusValue Good status without value for success, fatal otherwise.
913     */
914    final public function prepare( array $params ) {
915        if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
916            return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
917        }
918        /** @noinspection PhpUnusedLocalVariableInspection */
919        $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
920        return $this->doPrepare( $params );
921    }
922
923    /**
924     * @see FileBackend::prepare()
925     * @param array $params
926     * @return StatusValue Good status without value for success, fatal otherwise.
927     */
928    abstract protected function doPrepare( array $params );
929
930    /**
931     * Take measures to block web access to a storage directory and
932     * the container it belongs to. FS backends might add .htaccess
933     * files whereas key/value store backends might revoke container
934     * access to the storage user representing end-users in web requests.
935     *
936     * This is not guaranteed to actually make files or listings publicly hidden.
937     * Additional server configuration may be needed to achieve the desired effect.
938     *
939     * @param array $params Parameters include:
940     *   - dir            : storage directory
941     *   - noAccess       : try to deny file access
942     *   - noListing      : try to deny file listing
943     *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
944     * @return StatusValue
945     */
946    final public function secure( array $params ) {
947        if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
948            return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
949        }
950        /** @noinspection PhpUnusedLocalVariableInspection */
951        $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
952        return $this->doSecure( $params );
953    }
954
955    /**
956     * @see FileBackend::secure()
957     * @param array $params
958     * @return StatusValue
959     */
960    abstract protected function doSecure( array $params );
961
962    /**
963     * Remove measures to block web access to a storage directory and
964     * the container it belongs to. FS backends might remove .htaccess
965     * files whereas key/value store backends might grant container
966     * access to the storage user representing end-users in web requests.
967     * This essentially can undo the result of secure() calls.
968     *
969     * This is not guaranteed to actually make files or listings publicly viewable.
970     * Additional server configuration may be needed to achieve the desired effect.
971     *
972     * @param array $params Parameters include:
973     *   - dir            : storage directory
974     *   - access         : try to allow file access
975     *   - listing        : try to allow file listing
976     *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
977     * @return StatusValue
978     * @since 1.20
979     */
980    final public function publish( array $params ) {
981        if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
982            return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
983        }
984        /** @noinspection PhpUnusedLocalVariableInspection */
985        $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
986        return $this->doPublish( $params );
987    }
988
989    /**
990     * @see FileBackend::publish()
991     * @param array $params
992     * @return StatusValue
993     */
994    abstract protected function doPublish( array $params );
995
996    /**
997     * Delete a storage directory if it is empty.
998     * Backends using key/value stores may do nothing unless the directory
999     * is that of an empty container, in which case it will be deleted.
1000     *
1001     * @param array $params Parameters include:
1002     *   - dir            : storage directory
1003     *   - recursive      : recursively delete empty subdirectories first (since 1.20)
1004     *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
1005     * @return StatusValue
1006     */
1007    final public function clean( array $params ) {
1008        if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
1009            return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
1010        }
1011        /** @noinspection PhpUnusedLocalVariableInspection */
1012        $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
1013        return $this->doClean( $params );
1014    }
1015
1016    /**
1017     * @see FileBackend::clean()
1018     * @param array $params
1019     * @return StatusValue
1020     */
1021    abstract protected function doClean( array $params );
1022
1023    /**
1024     * Check if a file exists at a storage path in the backend.
1025     * This returns false if only a directory exists at the path.
1026     *
1027     * Callers that only care if a file is readily accessible can use non-strict
1028     * comparisons on the result. If "does not exist" and "existence is unknown"
1029     * must be distinguished, then strict comparisons to true/null should be used.
1030     *
1031     * @see FileBackend::EXISTENCE_ERROR
1032     * @see FileBackend::directoryExists()
1033     *
1034     * @param array $params Parameters include:
1035     *   - src    : source storage path
1036     *   - latest : use the latest available data
1037     * @return bool|null Whether the file exists or null (I/O error)
1038     */
1039    abstract public function fileExists( array $params );
1040
1041    /**
1042     * Get the last-modified timestamp of the file at a storage path.
1043     *
1044     * @see FileBackend::TIMESTAMP_FAIL
1045     *
1046     * @param array $params Parameters include:
1047     *   - src    : source storage path
1048     *   - latest : use the latest available data
1049     * @return string|false TS_MW timestamp or false (missing file or I/O error)
1050     */
1051    abstract public function getFileTimestamp( array $params );
1052
1053    /**
1054     * Get the contents of a file at a storage path in the backend.
1055     * This should be avoided for potentially large files.
1056     *
1057     * @see FileBackend::CONTENT_FAIL
1058     *
1059     * @param array $params Parameters include:
1060     *   - src    : source storage path
1061     *   - latest : use the latest available data
1062     * @return string|false Content string or false (missing file or I/O error)
1063     */
1064    final public function getFileContents( array $params ) {
1065        $contents = $this->getFileContentsMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1066
1067        return $contents[$params['src']];
1068    }
1069
1070    /**
1071     * Like getFileContents() except it takes an array of storage paths
1072     * and returns an order preserved map of storage paths to their content.
1073     *
1074     * @see FileBackend::getFileContents()
1075     *
1076     * @param array $params Parameters include:
1077     *   - srcs        : list of source storage paths
1078     *   - latest      : use the latest available data
1079     *   - parallelize : try to do operations in parallel when possible
1080     * @return string[]|false[] Map of (path name => file content or false on failure)
1081     * @since 1.20
1082     */
1083    abstract public function getFileContentsMulti( array $params );
1084
1085    /**
1086     * Get metadata about a file at a storage path in the backend.
1087     * If the file does not exist, then this returns false.
1088     * Otherwise, the result is an associative array that includes:
1089     *   - headers  : map of HTTP headers used for GET/HEAD requests (name => value)
1090     *   - metadata : map of file metadata (name => value)
1091     * Metadata keys and headers names will be returned in all lower-case.
1092     * Additional values may be included for internal use only.
1093     *
1094     * Use FileBackend::hasFeatures() to check how well this is supported.
1095     *
1096     * @see FileBackend::XATTRS_FAIL
1097     *
1098     * @param array $params
1099     * $params include:
1100     *   - src    : source storage path
1101     *   - latest : use the latest available data
1102     * @return array|false File metadata array or false (missing file or I/O error)
1103     * @since 1.23
1104     */
1105    abstract public function getFileXAttributes( array $params );
1106
1107    /**
1108     * Get the size (bytes) of a file at a storage path in the backend.
1109     *
1110     * @see FileBackend::SIZE_FAIL
1111     *
1112     * @param array $params Parameters include:
1113     *   - src    : source storage path
1114     *   - latest : use the latest available data
1115     * @return int|false File size in bytes or false (missing file or I/O error)
1116     */
1117    abstract public function getFileSize( array $params );
1118
1119    /**
1120     * Get quick information about a file at a storage path in the backend.
1121     * If the file does not exist, then this returns false.
1122     * Otherwise, the result is an associative array that includes:
1123     *   - mtime  : the last-modified timestamp (TS_MW)
1124     *   - size   : the file size (bytes)
1125     * Additional values may be included for internal use only.
1126     *
1127     * @see FileBackend::STAT_ABSENT
1128     * @see FileBackend::STAT_ERROR
1129     *
1130     * @param array $params Parameters include:
1131     *   - src    : source storage path
1132     *   - latest : use the latest available data
1133     * @return array|false|null Attribute map, false (missing file), or null (I/O error)
1134     */
1135    abstract public function getFileStat( array $params );
1136
1137    /**
1138     * Get a SHA-1 hash of the content of the file at a storage path in the backend.
1139     *
1140     * @see FileBackend::SHA1_FAIL
1141     *
1142     * @param array $params Parameters include:
1143     *   - src    : source storage path
1144     *   - latest : use the latest available data
1145     * @return string|false Hash string or false (missing file or I/O error)
1146     */
1147    abstract public function getFileSha1Base36( array $params );
1148
1149    /**
1150     * Get the properties of the content of the file at a storage path in the backend.
1151     * This gives the result of FSFile::getProps() on a local copy of the file.
1152     *
1153     * @param array $params Parameters include:
1154     *   - src    : source storage path
1155     *   - latest : use the latest available data
1156     * @return array Properties map; FSFile::placeholderProps() if file missing or on I/O error
1157     */
1158    abstract public function getFileProps( array $params );
1159
1160    /**
1161     * Stream the content of the file at a storage path in the backend.
1162     *
1163     * If the file does not exists, an HTTP 404 error will be given.
1164     * Appropriate HTTP headers (Status, Content-Type, Content-Length)
1165     * will be sent if streaming began, while none will be sent otherwise.
1166     * Implementations should flush the output buffer before sending data.
1167     *
1168     * @param array $params Parameters include:
1169     *   - src      : source storage path
1170     *   - headers  : list of additional HTTP headers to send if the file exists
1171     *   - options  : HTTP request header map with lower case keys (since 1.28). Supports:
1172     *                range             : format is "bytes=(\d*-\d*)"
1173     *                if-modified-since : format is an HTTP date
1174     *   - headless : do not send HTTP headers (including those of "headers") (since 1.28)
1175     *   - latest   : use the latest available data
1176     *   - allowOB  : preserve any output buffers (since 1.28)
1177     * @return StatusValue
1178     */
1179    abstract public function streamFile( array $params );
1180
1181    /**
1182     * Returns a file system file, identical in content to the file at a storage path.
1183     * The file returned is either:
1184     *   - a) A TempFSFile local copy of the file at a storage path in the backend.
1185     *        The temporary copy will have the same extension as the source.
1186     *        Temporary files may be purged when the file object falls out of scope.
1187     *   - b) An FSFile pointing to the original file at a storage path in the backend.
1188     *        This is applicable for backends layered directly on top of file systems.
1189     *
1190     * Never modify the returned file since it might be the original, it might be shared
1191     * among multiple callers of this method, or the backend might internally keep FSFile
1192     * references for deferred operations.
1193     *
1194     * @param array $params Parameters include:
1195     *   - src    : source storage path
1196     *   - latest : use the latest available data
1197     * @return FSFile|null|false Local file copy or false (missing) or null (error)
1198     */
1199    final public function getLocalReference( array $params ) {
1200        $fsFiles = $this->getLocalReferenceMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1201
1202        return $fsFiles[$params['src']];
1203    }
1204
1205    /**
1206     * Like getLocalReference() except it takes an array of storage paths and
1207     * yields an order-preserved map of storage paths to temporary local file copies.
1208     *
1209     * Never modify the returned files since they might be originals, they might be shared
1210     * among multiple callers of this method, or the backend might internally keep FSFile
1211     * references for deferred operations.
1212     *
1213     * @see FileBackend::getLocalReference()
1214     *
1215     * @param array $params Parameters include:
1216     *   - srcs        : list of source storage paths
1217     *   - latest      : use the latest available data
1218     *   - parallelize : try to do operations in parallel when possible
1219     * @return array Map of (path name => FSFile or false (missing) or null (error))
1220     * @since 1.20
1221     */
1222    abstract public function getLocalReferenceMulti( array $params );
1223
1224    /**
1225     * Get a local copy on disk of the file at a storage path in the backend.
1226     * The temporary copy will have the same file extension as the source.
1227     * Temporary files may be purged when the file object falls out of scope.
1228     *
1229     * Multiple calls to this method for the same path will create new copies.
1230     *
1231     * @param array $params Parameters include:
1232     *   - src    : source storage path
1233     *   - latest : use the latest available data
1234     * @return TempFSFile|null|false Temporary local file copy or false (missing) or null (error)
1235     */
1236    final public function getLocalCopy( array $params ) {
1237        $tmpFiles = $this->getLocalCopyMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1238
1239        return $tmpFiles[$params['src']];
1240    }
1241
1242    /**
1243     * Like getLocalCopy() except it takes an array of storage paths and yields
1244     * an order preserved-map of storage paths to temporary local file copies.
1245     *
1246     * Multiple calls to this method for the same path will create new copies.
1247     *
1248     * @see FileBackend::getLocalCopy()
1249     *
1250     * @param array $params Parameters include:
1251     *   - srcs        : list of source storage paths
1252     *   - latest      : use the latest available data
1253     *   - parallelize : try to do operations in parallel when possible
1254     * @return array Map of (path name => TempFSFile or false (missing) or null (error))
1255     * @since 1.20
1256     */
1257    abstract public function getLocalCopyMulti( array $params );
1258
1259    /**
1260     * Return an HTTP URL to a given file that requires no authentication to use.
1261     * The URL may be pre-authenticated (via some token in the URL) and temporary.
1262     * This will return null if the backend cannot make an HTTP URL for the file.
1263     *
1264     * This is useful for key/value stores when using scripts that seek around
1265     * large files and those scripts (and the backend) support HTTP Range headers.
1266     * Otherwise, one would need to use getLocalReference(), which involves loading
1267     * the entire file on to local disk.
1268     *
1269     * @see FileBackend::TEMPURL_ERROR
1270     *
1271     * @param array $params Parameters include:
1272     *   - src     : source storage path
1273     *   - ttl     : lifetime (seconds) if pre-authenticated; default is 1 day
1274     *   - latest  : use the latest available data
1275     *   - method  : the allowed method; default GET
1276     *   - ipRange : the allowed IP range; default unlimited
1277     * @return string|null URL or null (not supported or I/O error)
1278     * @since 1.21
1279     */
1280    abstract public function getFileHttpUrl( array $params );
1281
1282    /**
1283     * Add a file to a Shellbox command as an input file.
1284     *
1285     * @param BoxedCommand $command
1286     * @param string $boxedName
1287     * @param array $params Parameters include:
1288     *   - src    : source storage path
1289     *   - latest : use the latest available data
1290     * @return StatusValue
1291     * @since 1.43
1292     */
1293    abstract public function addShellboxInputFile( BoxedCommand $command, string $boxedName,
1294        array $params );
1295
1296    /**
1297     * Check if a directory exists at a given storage path
1298     *
1299     * For backends using key/value stores, a directory is said to exist whenever
1300     * there exist any files with paths using the given directory path as a prefix
1301     * followed by a forward slash. For example, if there is a file called
1302     * "mwstore://backend/container/dir/path.svg" then directories are said to exist
1303     * at "mwstore://backend/container" and "mwstore://backend/container/dir". These
1304     * can be thought of as "virtual" directories.
1305     *
1306     * Backends that directly use a filesystem layer might enumerate empty directories.
1307     * The clean() method should always be used when files are deleted or moved if this
1308     * is a concern. This is a trade-off to avoid write amplication/contention on file
1309     * changes or read amplification when calling this method.
1310     *
1311     * Storage backends with eventual consistency might return stale data.
1312     *
1313     * @see FileBackend::EXISTENCE_ERROR
1314     * @see FileBackend::clean()
1315     *
1316     * @param array $params Parameters include:
1317     *   - dir : storage directory
1318     * @return bool|null Whether a directory exists or null (I/O error)
1319     * @since 1.20
1320     */
1321    abstract public function directoryExists( array $params );
1322
1323    /**
1324     * Get an iterator to list *all* directories under a storage directory
1325     *
1326     * If the directory is of the form "mwstore://backend/container",
1327     * then all directories in the container will be listed.
1328     * If the directory is of form "mwstore://backend/container/dir",
1329     * then all directories directly under that directory will be listed.
1330     * Results will be storage directories relative to the given directory.
1331     *
1332     * Storage backends with eventual consistency might return stale data.
1333     *
1334     * Failures during iteration can result in FileBackendError exceptions (since 1.22).
1335     *
1336     * @see FileBackend::LIST_ERROR
1337     * @see FileBackend::directoryExists()
1338     *
1339     * @param array $params Parameters include:
1340     *   - dir     : storage directory
1341     *   - topOnly : only return direct child dirs of the directory
1342     * @return \Traversable|array|null Directory list enumerator or null (initial I/O error)
1343     * @since 1.20
1344     */
1345    abstract public function getDirectoryList( array $params );
1346
1347    /**
1348     * Same as FileBackend::getDirectoryList() except only lists
1349     * directories that are immediately under the given directory.
1350     *
1351     * Storage backends with eventual consistency might return stale data.
1352     *
1353     * Failures during iteration can result in FileBackendError exceptions (since 1.22).
1354     *
1355     * @see FileBackend::LIST_ERROR
1356     * @see FileBackend::directoryExists()
1357     *
1358     * @param array $params Parameters include:
1359     *   - dir : storage directory
1360     * @return \Traversable|array|null Directory list enumerator or null (initial I/O error)
1361     * @since 1.20
1362     */
1363    final public function getTopDirectoryList( array $params ) {
1364        return $this->getDirectoryList( [ 'topOnly' => true ] + $params );
1365    }
1366
1367    /**
1368     * Get an iterator to list *all* stored files under a storage directory
1369     *
1370     * If the directory is of the form "mwstore://backend/container", then all
1371     * files in the container will be listed. If the directory is of form
1372     * "mwstore://backend/container/dir", then all files under that directory will
1373     * be listed. Results will be storage paths relative to the given directory.
1374     *
1375     * Storage backends with eventual consistency might return stale data.
1376     *
1377     * Failures during iteration can result in FileBackendError exceptions (since 1.22).
1378     *
1379     * @see FileBackend::LIST_ERROR
1380     *
1381     * @param array $params Parameters include:
1382     *   - dir        : storage directory
1383     *   - topOnly    : only return direct child files of the directory (since 1.20)
1384     *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
1385     *   - forWrite   : true if the list will inform a write operations (since 1.41)
1386     * @return \Traversable|array|null File list enumerator or null (initial I/O error)
1387     */
1388    abstract public function getFileList( array $params );
1389
1390    /**
1391     * Same as FileBackend::getFileList() except only lists
1392     * files that are immediately under the given directory.
1393     *
1394     * Storage backends with eventual consistency might return stale data.
1395     *
1396     * Failures during iteration can result in FileBackendError exceptions (since 1.22).
1397     *
1398     * @see FileBackend::LIST_ERROR
1399     *
1400     * @param array $params Parameters include:
1401     *   - dir        : storage directory
1402     *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
1403     * @return \Traversable|array|null File list enumerator or null on failure
1404     * @since 1.20
1405     */
1406    final public function getTopFileList( array $params ) {
1407        return $this->getFileList( [ 'topOnly' => true ] + $params );
1408    }
1409
1410    /**
1411     * Preload persistent file stat cache and property cache into in-process cache.
1412     * This should be used when stat calls will be made on a known list of a many files.
1413     *
1414     * @see FileBackend::getFileStat()
1415     *
1416     * @param array $paths Storage paths
1417     */
1418    abstract public function preloadCache( array $paths );
1419
1420    /**
1421     * Invalidate any in-process file stat and property cache.
1422     * If $paths is given, then only the cache for those files will be cleared.
1423     *
1424     * @see FileBackend::getFileStat()
1425     *
1426     * @param array|null $paths Storage paths (optional)
1427     */
1428    abstract public function clearCache( ?array $paths = null );
1429
1430    /**
1431     * Preload file stat information (concurrently if possible) into in-process cache.
1432     *
1433     * This should be used when stat calls will be made on a known list of a many files.
1434     * This does not make use of the persistent file stat cache.
1435     *
1436     * @see FileBackend::getFileStat()
1437     *
1438     * @param array $params Parameters include:
1439     *   - srcs        : list of source storage paths
1440     *   - latest      : use the latest available data
1441     * @return bool Whether all requests proceeded without I/O errors (since 1.24)
1442     * @since 1.23
1443     */
1444    abstract public function preloadFileStat( array $params );
1445
1446    /**
1447     * Lock the files at the given storage paths in the backend.
1448     * This will either lock all the files or none (on failure).
1449     *
1450     * Callers should consider using getScopedFileLocks() instead.
1451     *
1452     * @param array $paths Storage paths
1453     * @param int $type LockManager::LOCK_* constant
1454     * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
1455     * @return StatusValue
1456     */
1457    final public function lockFiles( array $paths, $type, $timeout = 0 ) {
1458        $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths );
1459
1460        return $this->wrapStatus( $this->lockManager->lock( $paths, $type, $timeout ) );
1461    }
1462
1463    /**
1464     * Unlock the files at the given storage paths in the backend.
1465     *
1466     * @param array $paths Storage paths
1467     * @param int $type LockManager::LOCK_* constant
1468     * @return StatusValue
1469     */
1470    final public function unlockFiles( array $paths, $type ) {
1471        $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths );
1472
1473        return $this->wrapStatus( $this->lockManager->unlock( $paths, $type ) );
1474    }
1475
1476    /**
1477     * Lock the files at the given storage paths in the backend.
1478     * This will either lock all the files or none (on failure).
1479     * On failure, the StatusValue object will be updated with errors.
1480     *
1481     * Once the return value goes out scope, the locks will be released and
1482     * the StatusValue updated. Unlock fatals will not change the StatusValue "OK" value.
1483     *
1484     * @see ScopedLock::factory()
1485     *
1486     * @param array $paths List of storage paths or map of lock types to path lists
1487     * @param int|string $type LockManager::LOCK_* constant or "mixed"
1488     * @param StatusValue $status StatusValue to update on lock/unlock
1489     * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
1490     * @return ScopedLock|null RAII-style self-unlocking lock or null on failure
1491     */
1492    final public function getScopedFileLocks(
1493        array $paths, $type, StatusValue $status, $timeout = 0
1494    ) {
1495        if ( $type === 'mixed' ) {
1496            foreach ( $paths as &$typePaths ) {
1497                $typePaths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $typePaths );
1498            }
1499        } else {
1500            $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths );
1501        }
1502
1503        return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout );
1504    }
1505
1506    /**
1507     * Get an array of scoped locks needed for a batch of file operations.
1508     *
1509     * Normally, FileBackend::doOperations() handles locking, unless
1510     * the 'nonLocking' param is passed in. This function is useful if you
1511     * want the files to be locked for a broader scope than just when the
1512     * files are changing. For example, if you need to update DB metadata,
1513     * you may want to keep the files locked until finished.
1514     *
1515     * @see FileBackend::doOperations()
1516     *
1517     * @param array $ops List of file operations to FileBackend::doOperations()
1518     * @param StatusValue $status StatusValue to update on lock/unlock
1519     * @return ScopedLock|null RAII-style self-unlocking lock or null on failure
1520     * @since 1.20
1521     */
1522    abstract public function getScopedLocksForOps( array $ops, StatusValue $status );
1523
1524    /**
1525     * Get the root storage path of this backend.
1526     * All container paths are "subdirectories" of this path.
1527     *
1528     * @return string Storage path
1529     * @since 1.20
1530     */
1531    final public function getRootStoragePath() {
1532        return "mwstore://{$this->name}";
1533    }
1534
1535    /**
1536     * Get the storage path for the given container for this backend
1537     *
1538     * @param string $container Container name
1539     * @return string Storage path
1540     * @since 1.21
1541     */
1542    final public function getContainerStoragePath( $container ) {
1543        return $this->getRootStoragePath() . "/{$container}";
1544    }
1545
1546    /**
1547     * Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile)
1548     *
1549     * The 'srcRef' field keeps any TempFSFile objects in scope for the backend to have it
1550     * around as long it needs (which may vary greatly depending on configuration)
1551     *
1552     * @param array $ops File operation batch for FileBaclend::doOperations()
1553     * @return array File operation batch
1554     */
1555    protected function resolveFSFileObjects( array $ops ) {
1556        foreach ( $ops as &$op ) {
1557            $src = $op['src'] ?? null;
1558            if ( $src instanceof FSFile ) {
1559                $op['srcRef'] = $src;
1560                $op['src'] = $src->getPath();
1561            }
1562        }
1563        unset( $op );
1564
1565        return $ops;
1566    }
1567
1568    /**
1569     * Check if a given path is a "mwstore://" path.
1570     * This does not do any further validation or any existence checks.
1571     *
1572     * @param string|null $path
1573     * @return bool
1574     */
1575    final public static function isStoragePath( $path ) {
1576        return ( strpos( $path ?? '', 'mwstore://' ) === 0 );
1577    }
1578
1579    /**
1580     * Split a storage path into a backend name, a container name,
1581     * and a relative file path. The relative path may be the empty string.
1582     * This does not do any path normalization or traversal checks.
1583     *
1584     * @param string $storagePath
1585     * @return array (backend, container, rel object) or (null, null, null)
1586     */
1587    final public static function splitStoragePath( $storagePath ) {
1588        if ( self::isStoragePath( $storagePath ) ) {
1589            // Remove the "mwstore://" prefix and split the path
1590            $parts = explode( '/', substr( $storagePath, 10 ), 3 );
1591            if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
1592                if ( count( $parts ) == 3 ) {
1593                    return $parts; // e.g. "backend/container/path"
1594                } else {
1595                    return [ $parts[0], $parts[1], '' ]; // e.g. "backend/container"
1596                }
1597            }
1598        }
1599
1600        return [ null, null, null ];
1601    }
1602
1603    /**
1604     * Normalize a storage path by cleaning up directory separators.
1605     * Returns null if the path is not of the format of a valid storage path.
1606     *
1607     * @param string $storagePath
1608     * @return string|null Normalized storage path or null on failure
1609     */
1610    final public static function normalizeStoragePath( $storagePath ) {
1611        [ $backend, $container, $relPath ] = self::splitStoragePath( $storagePath );
1612        if ( $relPath !== null ) { // must be for this backend
1613            $relPath = self::normalizeContainerPath( $relPath );
1614            if ( $relPath !== null ) {
1615                return ( $relPath != '' )
1616                    ? "mwstore://{$backend}/{$container}/{$relPath}"
1617                    : "mwstore://{$backend}/{$container}";
1618            }
1619        }
1620
1621        return null;
1622    }
1623
1624    /**
1625     * Get the parent storage directory of a storage path.
1626     * This returns a path like "mwstore://backend/container",
1627     * "mwstore://backend/container/...", or null if there is no parent.
1628     *
1629     * @param string $storagePath
1630     * @return string|null Parent storage path or null on failure
1631     */
1632    final public static function parentStoragePath( $storagePath ) {
1633        // XXX dirname() depends on platform and locale! If nothing enforces that the storage path
1634        // doesn't contain characters like '\', behavior can vary by platform. We should use
1635        // explode() instead.
1636        $storagePath = dirname( $storagePath );
1637        [ , , $rel ] = self::splitStoragePath( $storagePath );
1638
1639        return ( $rel === null ) ? null : $storagePath;
1640    }
1641
1642    /**
1643     * Get the final extension from a storage or FS path
1644     *
1645     * @param string $path
1646     * @param string $case One of (rawcase, uppercase, lowercase) (since 1.24)
1647     * @return string
1648     */
1649    final public static function extensionFromPath( $path, $case = 'lowercase' ) {
1650        // This will treat a string starting with . as not having an extension, but store paths have
1651        // to start with 'mwstore://', so "garbage in, garbage out".
1652        $i = strrpos( $path, '.' );
1653        $ext = $i ? substr( $path, $i + 1 ) : '';
1654
1655        if ( $case === 'lowercase' ) {
1656            $ext = strtolower( $ext );
1657        } elseif ( $case === 'uppercase' ) {
1658            $ext = strtoupper( $ext );
1659        }
1660
1661        return $ext;
1662    }
1663
1664    /**
1665     * Check if a relative path has no directory traversals
1666     *
1667     * @param string $path
1668     * @return bool
1669     * @since 1.20
1670     */
1671    final public static function isPathTraversalFree( $path ) {
1672        return ( self::normalizeContainerPath( $path ) !== null );
1673    }
1674
1675    /**
1676     * Build a Content-Disposition header value per RFC 6266.
1677     *
1678     * @param string $type One of (attachment, inline)
1679     * @param string $filename Suggested file name (should not contain slashes)
1680     * @return string
1681     * @since 1.20
1682     */
1683    final public static function makeContentDisposition( $type, $filename = '' ) {
1684        $parts = [];
1685
1686        $type = strtolower( $type );
1687        if ( !in_array( $type, [ 'inline', 'attachment' ] ) ) {
1688            throw new InvalidArgumentException( "Invalid Content-Disposition type '$type'." );
1689        }
1690        $parts[] = $type;
1691
1692        if ( $filename !== '' ) {
1693            $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1694        }
1695
1696        return implode( ';', $parts );
1697    }
1698
1699    /**
1700     * Validate and normalize a relative storage path.
1701     * Null is returned if the path involves directory traversal.
1702     * Traversal is insecure for FS backends and broken for others.
1703     *
1704     * This uses the same traversal protection as Title::secureAndSplit().
1705     *
1706     * @param string $path Storage path relative to a container
1707     * @return string|null Normalized container path or null on failure
1708     */
1709    final protected static function normalizeContainerPath( $path ) {
1710        // Normalize directory separators
1711        $path = strtr( $path, '\\', '/' );
1712        // Collapse any consecutive directory separators
1713        $path = preg_replace( '![/]{2,}!', '/', $path );
1714        // Remove any leading directory separator
1715        $path = ltrim( $path, '/' );
1716        // Use the same traversal protection as Title::secureAndSplit()
1717        if ( strpos( $path, '.' ) !== false ) {
1718            if (
1719                $path === '.' ||
1720                $path === '..' ||
1721                strpos( $path, './' ) === 0 ||
1722                strpos( $path, '../' ) === 0 ||
1723                strpos( $path, '/./' ) !== false ||
1724                strpos( $path, '/../' ) !== false
1725            ) {
1726                return null;
1727            }
1728        }
1729
1730        return $path;
1731    }
1732
1733    /**
1734     * Yields the result of the status wrapper callback on either:
1735     *   - StatusValue::newGood(), if this method is called without a message
1736     *   - StatusValue::newFatal( ... ) with all parameters to this method, if a message was given
1737     *
1738     * @param null|string $message Message key
1739     * @phpcs:ignore Generic.Files.LineLength
1740     * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params
1741     *   See Message::params()
1742     * @return StatusValue
1743     */
1744    final protected function newStatus( $message = null, ...$params ) {
1745        if ( $message !== null ) {
1746            $sv = StatusValue::newFatal( $message, ...$params );
1747        } else {
1748            $sv = StatusValue::newGood();
1749        }
1750
1751        return $this->wrapStatus( $sv );
1752    }
1753
1754    /**
1755     * @param StatusValue $sv
1756     * @return StatusValue Modified status or StatusValue subclass
1757     */
1758    final protected function wrapStatus( StatusValue $sv ) {
1759        return $this->statusWrapper ? ( $this->statusWrapper )( $sv ) : $sv;
1760    }
1761
1762    /**
1763     * @param string $section
1764     * @return ScopedCallback|null
1765     */
1766    protected function scopedProfileSection( $section ) {
1767        return $this->profiler ? ( $this->profiler )( $section ) : null;
1768    }
1769
1770    /**
1771     * Default behavior of resetOutputBuffer().
1772     * @codeCoverageIgnore
1773     * @internal
1774     */
1775    public static function resetOutputBufferTheDefaultWay() {
1776        // XXX According to documentation, ob_get_status() always returns a non-empty array and this
1777        // condition will always be true
1778        while ( ob_get_status() ) {
1779            if ( !ob_end_clean() ) {
1780                // Could not remove output buffer handler; abort now
1781                // to avoid getting in some kind of infinite loop.
1782                break;
1783            }
1784        }
1785    }
1786
1787    /**
1788     * Return options for use with HTTPFileStreamer.
1789     *
1790     * @internal
1791     */
1792    public function getStreamerOptions(): array {
1793        return $this->streamerOptions;
1794    }
1795}
1796/** @deprecated class alias since 1.43 */
1797class_alias( FileBackend::class, 'FileBackend' );