]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/filerepo/FileBackendDBRepoWrapper.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / filerepo / FileBackendDBRepoWrapper.php
1 <?php
2 /**
3  * Proxy backend that manages file layout rewriting for FileRepo.
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 FileRepo
22  * @ingroup FileBackend
23  */
24
25 use Wikimedia\Rdbms\DBConnRef;
26
27 /**
28  * @brief Proxy backend that manages file layout rewriting for FileRepo.
29  *
30  * LocalRepo may be configured to store files under their title names or by SHA-1.
31  * This acts as a shim in the latter case, providing backwards compatability for
32  * most callers. All "public"/"deleted" zone files actually go in an "original"
33  * container and are never changed.
34  *
35  * This requires something like thumb_handler.php and img_auth.php for client viewing of files.
36  *
37  * @ingroup FileRepo
38  * @ingroup FileBackend
39  * @since 1.25
40  */
41 class FileBackendDBRepoWrapper extends FileBackend {
42         /** @var FileBackend */
43         protected $backend;
44         /** @var string */
45         protected $repoName;
46         /** @var Closure */
47         protected $dbHandleFunc;
48         /** @var ProcessCacheLRU */
49         protected $resolvedPathCache;
50         /** @var DBConnRef[] */
51         protected $dbs;
52
53         public function __construct( array $config ) {
54                 /** @var FileBackend $backend */
55                 $backend = $config['backend'];
56                 $config['name'] = $backend->getName();
57                 $config['wikiId'] = $backend->getWikiId();
58                 parent::__construct( $config );
59                 $this->backend = $config['backend'];
60                 $this->repoName = $config['repoName'];
61                 $this->dbHandleFunc = $config['dbHandleFactory'];
62                 $this->resolvedPathCache = new ProcessCacheLRU( 100 );
63         }
64
65         /**
66          * Get the underlying FileBackend that is being wrapped
67          *
68          * @return FileBackend
69          */
70         public function getInternalBackend() {
71                 return $this->backend;
72         }
73
74         /**
75          * Translate a legacy "title" path to it's "sha1" counterpart
76          *
77          * E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg
78          * => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg
79          *
80          * @param string $path
81          * @param bool $latest
82          * @return string
83          */
84         public function getBackendPath( $path, $latest = true ) {
85                 $paths = $this->getBackendPaths( [ $path ], $latest );
86                 return current( $paths );
87         }
88
89         /**
90          * Translate legacy "title" paths to their "sha1" counterparts
91          *
92          * E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg
93          * => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg
94          *
95          * @param array $paths
96          * @param bool $latest
97          * @return array Translated paths in same order
98          */
99         public function getBackendPaths( array $paths, $latest = true ) {
100                 $db = $this->getDB( $latest ? DB_MASTER : DB_REPLICA );
101
102                 // @TODO: batching
103                 $resolved = [];
104                 foreach ( $paths as $i => $path ) {
105                         if ( !$latest && $this->resolvedPathCache->has( $path, 'target', 10 ) ) {
106                                 $resolved[$i] = $this->resolvedPathCache->get( $path, 'target' );
107                                 continue;
108                         }
109
110                         list( , $container ) = FileBackend::splitStoragePath( $path );
111
112                         if ( $container === "{$this->repoName}-public" ) {
113                                 $name = basename( $path );
114                                 if ( strpos( $path, '!' ) !== false ) {
115                                         $sha1 = $db->selectField( 'oldimage', 'oi_sha1',
116                                                 [ 'oi_archive_name' => $name ],
117                                                 __METHOD__
118                                         );
119                                 } else {
120                                         $sha1 = $db->selectField( 'image', 'img_sha1',
121                                                 [ 'img_name' => $name ],
122                                                 __METHOD__
123                                         );
124                                 }
125                                 if ( !strlen( $sha1 ) ) {
126                                         $resolved[$i] = $path; // give up
127                                         continue;
128                                 }
129                                 $resolved[$i] = $this->getPathForSHA1( $sha1 );
130                                 $this->resolvedPathCache->set( $path, 'target', $resolved[$i] );
131                         } elseif ( $container === "{$this->repoName}-deleted" ) {
132                                 $name = basename( $path ); // <hash>.<ext>
133                                 $sha1 = substr( $name, 0, strpos( $name, '.' ) ); // ignore extension
134                                 $resolved[$i] = $this->getPathForSHA1( $sha1 );
135                                 $this->resolvedPathCache->set( $path, 'target', $resolved[$i] );
136                         } else {
137                                 $resolved[$i] = $path;
138                         }
139                 }
140
141                 $res = [];
142                 foreach ( $paths as $i => $path ) {
143                         $res[$i] = $resolved[$i];
144                 }
145
146                 return $res;
147         }
148
149         protected function doOperationsInternal( array $ops, array $opts ) {
150                 return $this->backend->doOperationsInternal( $this->mungeOpPaths( $ops ), $opts );
151         }
152
153         protected function doQuickOperationsInternal( array $ops ) {
154                 return $this->backend->doQuickOperationsInternal( $this->mungeOpPaths( $ops ) );
155         }
156
157         protected function doPrepare( array $params ) {
158                 return $this->backend->doPrepare( $params );
159         }
160
161         protected function doSecure( array $params ) {
162                 return $this->backend->doSecure( $params );
163         }
164
165         protected function doPublish( array $params ) {
166                 return $this->backend->doPublish( $params );
167         }
168
169         protected function doClean( array $params ) {
170                 return $this->backend->doClean( $params );
171         }
172
173         public function concatenate( array $params ) {
174                 return $this->translateSrcParams( __FUNCTION__, $params );
175         }
176
177         public function fileExists( array $params ) {
178                 return $this->translateSrcParams( __FUNCTION__, $params );
179         }
180
181         public function getFileTimestamp( array $params ) {
182                 return $this->translateSrcParams( __FUNCTION__, $params );
183         }
184
185         public function getFileSize( array $params ) {
186                 return $this->translateSrcParams( __FUNCTION__, $params );
187         }
188
189         public function getFileStat( array $params ) {
190                 return $this->translateSrcParams( __FUNCTION__, $params );
191         }
192
193         public function getFileXAttributes( array $params ) {
194                 return $this->translateSrcParams( __FUNCTION__, $params );
195         }
196
197         public function getFileSha1Base36( array $params ) {
198                 return $this->translateSrcParams( __FUNCTION__, $params );
199         }
200
201         public function getFileProps( array $params ) {
202                 return $this->translateSrcParams( __FUNCTION__, $params );
203         }
204
205         public function streamFile( array $params ) {
206                 // The stream methods use the file extension to determine the
207                 // Content-Type (as MediaWiki should already validate it on upload).
208                 // The translated SHA1 path has no extension, so this needs to use
209                 // the untranslated path extension.
210                 $type = StreamFile::contentTypeFromPath( $params['src'] );
211                 if ( $type && $type != 'unknown/unknown' ) {
212                         $params['headers'][] = "Content-type: $type";
213                 }
214                 return $this->translateSrcParams( __FUNCTION__, $params );
215         }
216
217         public function getFileContentsMulti( array $params ) {
218                 return $this->translateArrayResults( __FUNCTION__, $params );
219         }
220
221         public function getLocalReferenceMulti( array $params ) {
222                 return $this->translateArrayResults( __FUNCTION__, $params );
223         }
224
225         public function getLocalCopyMulti( array $params ) {
226                 return $this->translateArrayResults( __FUNCTION__, $params );
227         }
228
229         public function getFileHttpUrl( array $params ) {
230                 return $this->translateSrcParams( __FUNCTION__, $params );
231         }
232
233         public function directoryExists( array $params ) {
234                 return $this->backend->directoryExists( $params );
235         }
236
237         public function getDirectoryList( array $params ) {
238                 return $this->backend->getDirectoryList( $params );
239         }
240
241         public function getFileList( array $params ) {
242                 return $this->backend->getFileList( $params );
243         }
244
245         public function getFeatures() {
246                 return $this->backend->getFeatures();
247         }
248
249         public function clearCache( array $paths = null ) {
250                 $this->backend->clearCache( null ); // clear all
251         }
252
253         public function preloadCache( array $paths ) {
254                 $paths = $this->getBackendPaths( $paths );
255                 $this->backend->preloadCache( $paths );
256         }
257
258         public function preloadFileStat( array $params ) {
259                 return $this->translateSrcParams( __FUNCTION__, $params );
260         }
261
262         public function getScopedLocksForOps( array $ops, StatusValue $status ) {
263                 return $this->backend->getScopedLocksForOps( $ops, $status );
264         }
265
266         /**
267          * Get the ultimate original storage path for a file
268          *
269          * Use this when putting a new file into the system
270          *
271          * @param string $sha1 File SHA-1 base36
272          * @return string
273          */
274         public function getPathForSHA1( $sha1 ) {
275                 if ( strlen( $sha1 ) < 3 ) {
276                         throw new InvalidArgumentException( "Invalid file SHA-1." );
277                 }
278                 return $this->backend->getContainerStoragePath( "{$this->repoName}-original" ) .
279                         "/{$sha1[0]}/{$sha1[1]}/{$sha1[2]}/{$sha1}";
280         }
281
282         /**
283          * Get a connection to the repo file registry DB
284          *
285          * @param int $index
286          * @return DBConnRef
287          */
288         protected function getDB( $index ) {
289                 if ( !isset( $this->dbs[$index] ) ) {
290                         $func = $this->dbHandleFunc;
291                         $this->dbs[$index] = $func( $index );
292                 }
293                 return $this->dbs[$index];
294         }
295
296         /**
297          * Translates paths found in the "src" or "srcs" keys of a params array
298          *
299          * @param string $function
300          * @param array $params
301          * @return mixed
302          */
303         protected function translateSrcParams( $function, array $params ) {
304                 $latest = !empty( $params['latest'] );
305
306                 if ( isset( $params['src'] ) ) {
307                         $params['src'] = $this->getBackendPath( $params['src'], $latest );
308                 }
309
310                 if ( isset( $params['srcs'] ) ) {
311                         $params['srcs'] = $this->getBackendPaths( $params['srcs'], $latest );
312                 }
313
314                 return $this->backend->$function( $params );
315         }
316
317         /**
318          * Translates paths when the backend function returns results keyed by paths
319          *
320          * @param string $function
321          * @param array $params
322          * @return array
323          */
324         protected function translateArrayResults( $function, array $params ) {
325                 $origPaths = $params['srcs'];
326                 $params['srcs'] = $this->getBackendPaths( $params['srcs'], !empty( $params['latest'] ) );
327                 $pathMap = array_combine( $params['srcs'], $origPaths );
328
329                 $results = $this->backend->$function( $params );
330
331                 $contents = [];
332                 foreach ( $results as $path => $result ) {
333                         $contents[$pathMap[$path]] = $result;
334                 }
335
336                 return $contents;
337         }
338
339         /**
340          * Translate legacy "title" source paths to their "sha1" counterparts
341          *
342          * This leaves destination paths alone since we don't want those to mutate
343          *
344          * @param array $ops
345          * @return array
346          */
347         protected function mungeOpPaths( array $ops ) {
348                 // Ops that use 'src' and do not mutate core file data there
349                 static $srcRefOps = [ 'store', 'copy', 'describe' ];
350                 foreach ( $ops as &$op ) {
351                         if ( isset( $op['src'] ) && in_array( $op['op'], $srcRefOps ) ) {
352                                 $op['src'] = $this->getBackendPath( $op['src'], true );
353                         }
354                         if ( isset( $op['srcs'] ) ) {
355                                 $op['srcs'] = $this->getBackendPaths( $op['srcs'], true );
356                         }
357                 }
358                 return $ops;
359         }
360 }