MediaWiki  master
ForeignResourceManager.php
Go to the documentation of this file.
1 <?php
23 
30  private $defaultAlgo = 'sha384';
31  private $hasErrors = false;
32  private $registryFile;
33  private $libDir;
34  private $tmpParentDir;
35  private $cacheDir;
36  private $infoPrinter;
37  private $errorPrinter;
38  private $verbosePrinter;
39  private $action;
40  private $registry;
41 
50  public function __construct(
52  $libDir,
53  callable $infoPrinter = null,
54  callable $errorPrinter = null,
55  callable $verbosePrinter = null
56  ) {
57  $this->registryFile = $registryFile;
58  $this->libDir = $libDir;
59  $this->infoPrinter = $infoPrinter ?? function () {
60  };
61  $this->errorPrinter = $errorPrinter ?? $this->infoPrinter;
62  $this->verbosePrinter = $verbosePrinter ?? function () {
63  };
64 
65  // Use a temporary directory under the destination directory instead
66  // of wfTempDir() because PHP's rename() does not work across file
67  // systems, and the user's /tmp and $IP may be on different filesystems.
68  $this->tmpParentDir = "{$this->libDir}/.foreign/tmp";
69 
70  $cacheHome = getenv( 'XDG_CACHE_HOME' ) ? realpath( getenv( 'XDG_CACHE_HOME' ) ) : false;
71  $this->cacheDir = $cacheHome ? "$cacheHome/mw-foreign" : "{$this->libDir}/.foreign/cache";
72  }
73 
78  public function run( $action, $module ) {
79  $actions = [ 'update', 'verify', 'make-sri' ];
80  if ( !in_array( $action, $actions ) ) {
81  $this->error( "Invalid action.\n\nMust be one of " . implode( ', ', $actions ) . '.' );
82  return false;
83  }
84  $this->action = $action;
85 
86  $this->registry = $this->parseBasicYaml( file_get_contents( $this->registryFile ) );
87  if ( $module === 'all' ) {
89  } elseif ( isset( $this->registry[ $module ] ) ) {
90  $modules = [ $module => $this->registry[ $module ] ];
91  } else {
92  $this->error( "Unknown module name.\n\nMust be one of:\n" .
93  wordwrap( implode( ', ', array_keys( $this->registry ) ), 80 ) .
94  '.'
95  );
96  return false;
97  }
98 
99  foreach ( $modules as $moduleName => $info ) {
100  $this->verbose( "\n### {$moduleName}\n\n" );
101  $destDir = "{$this->libDir}/$moduleName";
102 
103  if ( $this->action === 'update' ) {
104  $this->output( "... updating '{$moduleName}'\n" );
105  $this->verbose( "... emptying directory for $moduleName\n" );
106  wfRecursiveRemoveDir( $destDir );
107  } elseif ( $this->action === 'verify' ) {
108  $this->output( "... verifying '{$moduleName}'\n" );
109  } else {
110  $this->output( "... checking '{$moduleName}'\n" );
111  }
112 
113  $this->verbose( "... preparing {$this->tmpParentDir}\n" );
114  wfRecursiveRemoveDir( $this->tmpParentDir );
115  if ( !wfMkdirParents( $this->tmpParentDir ) ) {
116  throw new Exception( "Unable to create {$this->tmpParentDir}" );
117  }
118 
119  if ( !isset( $info['type'] ) ) {
120  throw new Exception( "Module '$moduleName' must have a 'type' key." );
121  }
122  switch ( $info['type'] ) {
123  case 'tar':
124  $this->handleTypeTar( $moduleName, $destDir, $info );
125  break;
126  case 'file':
127  $this->handleTypeFile( $moduleName, $destDir, $info );
128  break;
129  case 'multi-file':
130  $this->handleTypeMultiFile( $moduleName, $destDir, $info );
131  break;
132  default:
133  throw new Exception( "Unknown type '{$info['type']}' for '$moduleName'" );
134  }
135  }
136 
137  $this->output( "\nDone!\n" );
138  $this->cleanUp();
139  if ( $this->hasErrors ) {
140  // The verify mode should check all modules/files and fail after, not during.
141  return false;
142  }
143 
144  return true;
145  }
146 
147  private function cacheKey( $src, $integrity ) {
148  $key = basename( $src ) . '_' . substr( $integrity, -12 );
149  $key = preg_replace( '/[.\/+?=_-]+/', '_', $key );
150  return rtrim( $key, '_' );
151  }
152 
154  private function cacheGet( $key ) {
155  return AtEase::quietCall( 'file_get_contents', "{$this->cacheDir}/$key.data" );
156  }
157 
158  private function cacheSet( $key, $data ) {
159  wfMkdirParents( $this->cacheDir );
160  file_put_contents( "{$this->cacheDir}/$key.data", $data, LOCK_EX );
161  }
162 
163  private function fetch( $src, $integrity ) {
164  $key = $this->cacheKey( $src, $integrity );
165  $data = $this->cacheGet( $key );
166  if ( $data ) {
167  return $data;
168  }
169 
170  $req = MWHttpRequest::factory( $src, [ 'method' => 'GET', 'followRedirects' => false ] );
171  if ( !$req->execute()->isOK() ) {
172  throw new Exception( "Failed to download resource at {$src}" );
173  }
174  if ( $req->getStatus() !== 200 ) {
175  throw new Exception( "Unexpected HTTP {$req->getStatus()} response from {$src}" );
176  }
177  $data = $req->getContent();
178  $algo = $integrity === null ? $this->defaultAlgo : explode( '-', $integrity )[0];
179  $actualIntegrity = $algo . '-' . base64_encode( hash( $algo, $data, true ) );
180  if ( $integrity === $actualIntegrity ) {
181  $this->verbose( "... passed integrity check for {$src}\n" );
182  $this->cacheSet( $key, $data );
183  } elseif ( $this->action === 'make-sri' ) {
184  $this->output( "Integrity for {$src}\n\tintegrity: ${actualIntegrity}\n" );
185  } else {
186  throw new Exception( "Integrity check failed for {$src}\n" .
187  "\tExpected: {$integrity}\n" .
188  "\tActual: {$actualIntegrity}"
189  );
190  }
191  return $data;
192  }
193 
194  private function handleTypeFile( $moduleName, $destDir, array $info ) {
195  if ( !isset( $info['src'] ) ) {
196  throw new Exception( "Module '$moduleName' must have a 'src' key." );
197  }
198  $data = $this->fetch( $info['src'], $info['integrity'] ?? null );
199  $dest = $info['dest'] ?? basename( $info['src'] );
200  $path = "$destDir/$dest";
201  if ( $this->action === 'verify' && sha1_file( $path ) !== sha1( $data ) ) {
202  throw new Exception( "File for '$moduleName' is different." );
203  }
204  if ( $this->action === 'update' ) {
205  wfMkdirParents( $destDir );
206  file_put_contents( "$destDir/$dest", $data );
207  }
208  }
209 
210  private function handleTypeMultiFile( $moduleName, $destDir, array $info ) {
211  if ( !isset( $info['files'] ) ) {
212  throw new Exception( "Module '$moduleName' must have a 'files' key." );
213  }
214  foreach ( $info['files'] as $dest => $file ) {
215  if ( !isset( $file['src'] ) ) {
216  throw new Exception( "Module '$moduleName' file '$dest' must have a 'src' key." );
217  }
218  $data = $this->fetch( $file['src'], $file['integrity'] ?? null );
219  $path = "$destDir/$dest";
220  if ( $this->action === 'verify' && sha1_file( $path ) !== sha1( $data ) ) {
221  throw new Exception( "File '$dest' for '$moduleName' is different." );
222  } elseif ( $this->action === 'update' ) {
223  wfMkdirParents( $destDir );
224  file_put_contents( "$destDir/$dest", $data );
225  }
226  }
227  }
228 
229  private function handleTypeTar( $moduleName, $destDir, array $info ) {
230  $info += [ 'src' => null, 'integrity' => null, 'dest' => null ];
231  if ( $info['src'] === null ) {
232  throw new Exception( "Module '$moduleName' must have a 'src' key." );
233  }
234  // Download the resource to a temporary file and open it
235  $data = $this->fetch( $info['src'], $info['integrity' ] );
236  $tmpFile = "{$this->tmpParentDir}/$moduleName.tar";
237  $this->verbose( "... writing '$moduleName' src to $tmpFile\n" );
238  file_put_contents( $tmpFile, $data );
239  $p = new PharData( $tmpFile );
240  $tmpDir = "{$this->tmpParentDir}/$moduleName";
241  $p->extractTo( $tmpDir );
242  unset( $data, $p );
243 
244  if ( $info['dest'] === null ) {
245  // Default: Replace the entire directory
246  $toCopy = [ $tmpDir => $destDir ];
247  } else {
248  // Expand and normalise the 'dest' entries
249  $toCopy = [];
250  foreach ( $info['dest'] as $fromSubPath => $toSubPath ) {
251  // Use glob() to expand wildcards and check existence
252  $fromPaths = glob( "{$tmpDir}/{$fromSubPath}", GLOB_BRACE );
253  if ( !$fromPaths ) {
254  throw new Exception( "Path '$fromSubPath' of '$moduleName' not found." );
255  }
256  foreach ( $fromPaths as $fromPath ) {
257  $toCopy[$fromPath] = $toSubPath === null
258  ? "$destDir/" . basename( $fromPath )
259  : "$destDir/$toSubPath/" . basename( $fromPath );
260  }
261  }
262  }
263  foreach ( $toCopy as $from => $to ) {
264  if ( $this->action === 'verify' ) {
265  $this->verbose( "... verifying $to\n" );
266  if ( is_dir( $from ) ) {
268  $from,
269  RecursiveDirectoryIterator::SKIP_DOTS
270  ) );
272  foreach ( $rii as $file ) {
273  $remote = $file->getPathname();
274  $local = strtr( $remote, [ $from => $to ] );
275  if ( sha1_file( $remote ) !== sha1_file( $local ) ) {
276  $this->error( "File '$local' is different." );
277  $this->hasErrors = true;
278  }
279  }
280  } elseif ( sha1_file( $from ) !== sha1_file( $to ) ) {
281  $this->error( "File '$to' is different." );
282  $this->hasErrors = true;
283  }
284  } elseif ( $this->action === 'update' ) {
285  $this->verbose( "... moving $from to $to\n" );
286  wfMkdirParents( dirname( $to ) );
287  if ( !rename( $from, $to ) ) {
288  throw new Exception( "Could not move $from to $to." );
289  }
290  }
291  }
292  }
293 
294  private function verbose( $text ) {
295  ( $this->verbosePrinter )( $text );
296  }
297 
298  private function output( $text ) {
299  ( $this->infoPrinter )( $text );
300  }
301 
302  private function error( $text ) {
303  ( $this->errorPrinter )( $text );
304  }
305 
306  private function cleanUp() {
307  wfRecursiveRemoveDir( $this->tmpParentDir );
308 
309  // Prune the cache of files we don't recognise.
310  $knownKeys = [];
311  foreach ( $this->registry as $info ) {
312  if ( $info['type'] === 'file' || $info['type'] === 'tar' ) {
313  $knownKeys[] = $this->cacheKey( $info['src'], $info['integrity'] );
314  } elseif ( $info['type'] === 'multi-file' ) {
315  foreach ( $info['files'] as $file ) {
316  $knownKeys[] = $this->cacheKey( $file['src'], $file['integrity'] );
317  }
318  }
319  }
320  foreach ( glob( "{$this->cacheDir}/*" ) as $cacheFile ) {
321  if ( !in_array( basename( $cacheFile, '.data' ), $knownKeys ) ) {
322  unlink( $cacheFile );
323  }
324  }
325  }
326 
336  private function parseBasicYaml( $input ) {
337  $lines = explode( "\n", $input );
338  $root = [];
339  $stack = [ &$root ];
340  $prev = 0;
341  foreach ( $lines as $i => $text ) {
342  $line = $i + 1;
343  $trimmed = ltrim( $text, ' ' );
344  if ( $trimmed === '' || $trimmed[0] === '#' ) {
345  continue;
346  }
347  $indent = strlen( $text ) - strlen( $trimmed );
348  if ( $indent % 2 !== 0 ) {
349  throw new Exception( __METHOD__ . ": Odd indentation on line $line." );
350  }
351  $depth = $indent === 0 ? 0 : ( $indent / 2 );
352  if ( $depth < $prev ) {
353  // Close previous branches we can't re-enter
354  array_splice( $stack, $depth + 1 );
355  }
356  if ( !array_key_exists( $depth, $stack ) ) {
357  throw new Exception( __METHOD__ . ": Too much indentation on line $line." );
358  }
359  if ( strpos( $trimmed, ':' ) === false ) {
360  throw new Exception( __METHOD__ . ": Missing colon on line $line." );
361  }
362  $dest =& $stack[ $depth ];
363  if ( $dest === null ) {
364  // Promote from null to object
365  $dest = [];
366  }
367  list( $key, $val ) = explode( ':', $trimmed, 2 );
368  $val = ltrim( $val, ' ' );
369  if ( $val !== '' ) {
370  // Add string
371  $dest[ $key ] = $val;
372  } else {
373  // Add null (may become an object later)
374  $val = null;
375  $stack[] = &$val;
376  $dest[ $key ] = &$val;
377  }
378  $prev = $depth;
379  unset( $dest, $val );
380  }
381  return $root;
382  }
383 }
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
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object.
if(is_array( $mode)) switch( $mode) $input
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
wfRecursiveRemoveDir( $dir)
Remove a directory and all its content.
handleTypeMultiFile( $moduleName, $destDir, array $info)
__construct( $registryFile, $libDir, callable $infoPrinter=null, callable $errorPrinter=null, callable $verbosePrinter=null)
$modules
Manage foreign resources registered with ResourceLoader.
handleTypeTar( $moduleName, $destDir, array $info)
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:780
parseBasicYaml( $input)
Basic YAML parser.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
this hook is for auditing only $req
Definition: hooks.txt:979
$lines
Definition: router.php:61
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don&#39;t exist.
$line
Definition: cdb.php:59
handleTypeFile( $moduleName, $destDir, array $info)