MediaWiki REL1_33
ForeignResourceManager.php
Go to the documentation of this file.
1<?php
28 private $defaultAlgo = 'sha384';
29 private $hasErrors = false;
31 private $libDir;
33 private $cacheDir;
34 private $infoPrinter;
37 private $action;
38 private $registry;
39
48 public function __construct(
50 $libDir,
51 callable $infoPrinter = null,
52 callable $errorPrinter = null,
53 callable $verbosePrinter = null
54 ) {
55 $this->registryFile = $registryFile;
56 $this->libDir = $libDir;
57 $this->infoPrinter = $infoPrinter ?? function () {
58 };
59 $this->errorPrinter = $errorPrinter ?? $this->infoPrinter;
60 $this->verbosePrinter = $verbosePrinter ?? function () {
61 };
62
63 // Use a temporary directory under the destination directory instead
64 // of wfTempDir() because PHP's rename() does not work across file
65 // systems, and the user's /tmp and $IP may be on different filesystems.
66 $this->tmpParentDir = "{$this->libDir}/.foreign/tmp";
67
68 $cacheHome = getenv( 'XDG_CACHE_HOME' ) ? realpath( getenv( 'XDG_CACHE_HOME' ) ) : false;
69 $this->cacheDir = $cacheHome ? "$cacheHome/mw-foreign" : "{$this->libDir}/.foreign/cache";
70 }
71
76 public function run( $action, $module ) {
77 $actions = [ 'update', 'verify', 'make-sri' ];
78 if ( !in_array( $action, $actions ) ) {
79 $this->error( "Invalid action.\n\nMust be one of " . implode( ', ', $actions ) . '.' );
80 return false;
81 }
82 $this->action = $action;
83
84 $this->registry = $this->parseBasicYaml( file_get_contents( $this->registryFile ) );
85 if ( $module === 'all' ) {
87 } elseif ( isset( $this->registry[ $module ] ) ) {
88 $modules = [ $module => $this->registry[ $module ] ];
89 } else {
90 $this->error( "Unknown module name.\n\nMust be one of:\n" .
91 wordwrap( implode( ', ', array_keys( $this->registry ) ), 80 ) .
92 '.'
93 );
94 return false;
95 }
96
97 foreach ( $modules as $moduleName => $info ) {
98 $this->verbose( "\n### {$moduleName}\n\n" );
99 $destDir = "{$this->libDir}/$moduleName";
100
101 if ( $this->action === 'update' ) {
102 $this->output( "... updating '{$moduleName}'\n" );
103 $this->verbose( "... emptying directory for $moduleName\n" );
104 wfRecursiveRemoveDir( $destDir );
105 } elseif ( $this->action === 'verify' ) {
106 $this->output( "... verifying '{$moduleName}'\n" );
107 } else {
108 $this->output( "... checking '{$moduleName}'\n" );
109 }
110
111 $this->verbose( "... preparing {$this->tmpParentDir}\n" );
112 wfRecursiveRemoveDir( $this->tmpParentDir );
113 if ( !wfMkdirParents( $this->tmpParentDir ) ) {
114 throw new Exception( "Unable to create {$this->tmpParentDir}" );
115 }
116
117 if ( !isset( $info['type'] ) ) {
118 throw new Exception( "Module '$moduleName' must have a 'type' key." );
119 }
120 switch ( $info['type'] ) {
121 case 'tar':
122 $this->handleTypeTar( $moduleName, $destDir, $info );
123 break;
124 case 'file':
125 $this->handleTypeFile( $moduleName, $destDir, $info );
126 break;
127 case 'multi-file':
128 $this->handleTypeMultiFile( $moduleName, $destDir, $info );
129 break;
130 default:
131 throw new Exception( "Unknown type '{$info['type']}' for '$moduleName'" );
132 }
133 }
134
135 $this->output( "\nDone!\n" );
136 $this->cleanUp();
137 if ( $this->hasErrors ) {
138 // The verify mode should check all modules/files and fail after, not during.
139 return false;
140 }
141
142 return true;
143 }
144
145 private function cacheKey( $src, $integrity ) {
146 $key = basename( $src ) . '_' . substr( $integrity, -12 );
147 $key = preg_replace( '/[.\/+?=_-]+/', '_', $key );
148 return rtrim( $key, '_' );
149 }
150
152 private function cacheGet( $key ) {
153 return Wikimedia\quietCall( 'file_get_contents', "{$this->cacheDir}/$key.data" );
154 }
155
156 private function cacheSet( $key, $data ) {
157 wfMkdirParents( $this->cacheDir );
158 file_put_contents( "{$this->cacheDir}/$key.data", $data, LOCK_EX );
159 }
160
161 private function fetch( $src, $integrity ) {
162 $key = $this->cacheKey( $src, $integrity );
163 $data = $this->cacheGet( $key );
164 if ( $data ) {
165 return $data;
166 }
167
168 $req = MWHttpRequest::factory( $src, [ 'method' => 'GET', 'followRedirects' => false ] );
169 if ( !$req->execute()->isOK() ) {
170 throw new Exception( "Failed to download resource at {$src}" );
171 }
172 if ( $req->getStatus() !== 200 ) {
173 throw new Exception( "Unexpected HTTP {$req->getStatus()} response from {$src}" );
174 }
175 $data = $req->getContent();
176 $algo = $integrity === null ? $this->defaultAlgo : explode( '-', $integrity )[0];
177 $actualIntegrity = $algo . '-' . base64_encode( hash( $algo, $data, true ) );
178 if ( $integrity === $actualIntegrity ) {
179 $this->verbose( "... passed integrity check for {$src}\n" );
180 $this->cacheSet( $key, $data );
181 } elseif ( $this->action === 'make-sri' ) {
182 $this->output( "Integrity for {$src}\n\tintegrity: ${actualIntegrity}\n" );
183 } else {
184 throw new Exception( "Integrity check failed for {$src}\n" .
185 "\tExpected: {$integrity}\n" .
186 "\tActual: {$actualIntegrity}"
187 );
188 }
189 return $data;
190 }
191
192 private function handleTypeFile( $moduleName, $destDir, array $info ) {
193 if ( !isset( $info['src'] ) ) {
194 throw new Exception( "Module '$moduleName' must have a 'src' key." );
195 }
196 $data = $this->fetch( $info['src'], $info['integrity'] ?? null );
197 $dest = $info['dest'] ?? basename( $info['src'] );
198 $path = "$destDir/$dest";
199 if ( $this->action === 'verify' && sha1_file( $path ) !== sha1( $data ) ) {
200 throw new Exception( "File for '$moduleName' is different." );
201 }
202 if ( $this->action === 'update' ) {
203 wfMkdirParents( $destDir );
204 file_put_contents( "$destDir/$dest", $data );
205 }
206 }
207
208 private function handleTypeMultiFile( $moduleName, $destDir, array $info ) {
209 if ( !isset( $info['files'] ) ) {
210 throw new Exception( "Module '$moduleName' must have a 'files' key." );
211 }
212 foreach ( $info['files'] as $dest => $file ) {
213 if ( !isset( $file['src'] ) ) {
214 throw new Exception( "Module '$moduleName' file '$dest' must have a 'src' key." );
215 }
216 $data = $this->fetch( $file['src'], $file['integrity'] ?? null );
217 $path = "$destDir/$dest";
218 if ( $this->action === 'verify' && sha1_file( $path ) !== sha1( $data ) ) {
219 throw new Exception( "File '$dest' for '$moduleName' is different." );
220 } elseif ( $this->action === 'update' ) {
221 wfMkdirParents( $destDir );
222 file_put_contents( "$destDir/$dest", $data );
223 }
224 }
225 }
226
227 private function handleTypeTar( $moduleName, $destDir, array $info ) {
228 $info += [ 'src' => null, 'integrity' => null, 'dest' => null ];
229 if ( $info['src'] === null ) {
230 throw new Exception( "Module '$moduleName' must have a 'src' key." );
231 }
232 // Download the resource to a temporary file and open it
233 $data = $this->fetch( $info['src'], $info['integrity' ] );
234 $tmpFile = "{$this->tmpParentDir}/$moduleName.tar";
235 $this->verbose( "... writing '$moduleName' src to $tmpFile\n" );
236 file_put_contents( $tmpFile, $data );
237 $p = new PharData( $tmpFile );
238 $tmpDir = "{$this->tmpParentDir}/$moduleName";
239 $p->extractTo( $tmpDir );
240 unset( $data, $p );
241
242 if ( $info['dest'] === null ) {
243 // Default: Replace the entire directory
244 $toCopy = [ $tmpDir => $destDir ];
245 } else {
246 // Expand and normalise the 'dest' entries
247 $toCopy = [];
248 foreach ( $info['dest'] as $fromSubPath => $toSubPath ) {
249 // Use glob() to expand wildcards and check existence
250 $fromPaths = glob( "{$tmpDir}/{$fromSubPath}", GLOB_BRACE );
251 if ( !$fromPaths ) {
252 throw new Exception( "Path '$fromSubPath' of '$moduleName' not found." );
253 }
254 foreach ( $fromPaths as $fromPath ) {
255 $toCopy[$fromPath] = $toSubPath === null
256 ? "$destDir/" . basename( $fromPath )
257 : "$destDir/$toSubPath/" . basename( $fromPath );
258 }
259 }
260 }
261 foreach ( $toCopy as $from => $to ) {
262 if ( $this->action === 'verify' ) {
263 $this->verbose( "... verifying $to\n" );
264 if ( is_dir( $from ) ) {
266 $from,
267 RecursiveDirectoryIterator::SKIP_DOTS
268 ) );
270 foreach ( $rii as $file ) {
271 $remote = $file->getPathname();
272 $local = strtr( $remote, [ $from => $to ] );
273 if ( sha1_file( $remote ) !== sha1_file( $local ) ) {
274 $this->error( "File '$local' is different." );
275 $this->hasErrors = true;
276 }
277 }
278 } elseif ( sha1_file( $from ) !== sha1_file( $to ) ) {
279 $this->error( "File '$to' is different." );
280 $this->hasErrors = true;
281 }
282 } elseif ( $this->action === 'update' ) {
283 $this->verbose( "... moving $from to $to\n" );
284 wfMkdirParents( dirname( $to ) );
285 if ( !rename( $from, $to ) ) {
286 throw new Exception( "Could not move $from to $to." );
287 }
288 }
289 }
290 }
291
292 private function verbose( $text ) {
293 ( $this->verbosePrinter )( $text );
294 }
295
296 private function output( $text ) {
297 ( $this->infoPrinter )( $text );
298 }
299
300 private function error( $text ) {
301 ( $this->errorPrinter )( $text );
302 }
303
304 private function cleanUp() {
305 wfRecursiveRemoveDir( $this->tmpParentDir );
306
307 // Prune the cache of files we don't recognise.
308 $knownKeys = [];
309 foreach ( $this->registry as $info ) {
310 if ( $info['type'] === 'file' || $info['type'] === 'tar' ) {
311 $knownKeys[] = $this->cacheKey( $info['src'], $info['integrity'] );
312 } elseif ( $info['type'] === 'multi-file' ) {
313 foreach ( $info['files'] as $file ) {
314 $knownKeys[] = $this->cacheKey( $file['src'], $file['integrity'] );
315 }
316 }
317 }
318 foreach ( glob( "{$this->cacheDir}/*" ) as $cacheFile ) {
319 if ( !in_array( basename( $cacheFile, '.data' ), $knownKeys ) ) {
320 unlink( $cacheFile );
321 }
322 }
323 }
324
334 private function parseBasicYaml( $input ) {
335 $lines = explode( "\n", $input );
336 $root = [];
337 $stack = [ &$root ];
338 $prev = 0;
339 foreach ( $lines as $i => $text ) {
340 $line = $i + 1;
341 $trimmed = ltrim( $text, ' ' );
342 if ( $trimmed === '' || $trimmed[0] === '#' ) {
343 continue;
344 }
345 $indent = strlen( $text ) - strlen( $trimmed );
346 if ( $indent % 2 !== 0 ) {
347 throw new Exception( __METHOD__ . ": Odd indentation on line $line." );
348 }
349 $depth = $indent === 0 ? 0 : ( $indent / 2 );
350 if ( $depth < $prev ) {
351 // Close previous branches we can't re-enter
352 array_splice( $stack, $depth + 1 );
353 }
354 if ( !array_key_exists( $depth, $stack ) ) {
355 throw new Exception( __METHOD__ . ": Too much indentation on line $line." );
356 }
357 if ( strpos( $trimmed, ':' ) === false ) {
358 throw new Exception( __METHOD__ . ": Missing colon on line $line." );
359 }
360 $dest =& $stack[ $depth ];
361 if ( $dest === null ) {
362 // Promote from null to object
363 $dest = [];
364 }
365 list( $key, $val ) = explode( ':', $trimmed, 2 );
366 $val = ltrim( $val, ' ' );
367 if ( $val !== '' ) {
368 // Add string
369 $dest[ $key ] = $val;
370 } else {
371 // Add null (may become an object later)
372 $val = null;
373 $stack[] = &$val;
374 $dest[ $key ] = &$val;
375 }
376 $prev = $depth;
377 unset( $dest, $val );
378 }
379 return $root;
380 }
381}
and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two and(2) offer you this license which gives you legal permission to copy
wfRecursiveRemoveDir( $dir)
Remove a directory and all its content.
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
$line
Definition cdb.php:59
Manage foreign resources registered with ResourceLoader.
parseBasicYaml( $input)
Basic YAML parser.
handleTypeTar( $moduleName, $destDir, array $info)
handleTypeMultiFile( $moduleName, $destDir, array $info)
__construct( $registryFile, $libDir, callable $infoPrinter=null, callable $errorPrinter=null, callable $verbosePrinter=null)
handleTypeFile( $moduleName, $destDir, array $info)
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
this hook is for auditing only $req
Definition hooks.txt:979
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults error
Definition hooks.txt:2644
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition hooks.txt:783
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
if(is_array($mode)) switch( $mode) $input
$tester verbose
$lines
Definition router.php:61