66 $this->tmpParentDir =
"{$this->libDir}/.foreign/tmp";
68 $cacheHome = getenv(
'XDG_CACHE_HOME' ) ? realpath( getenv(
'XDG_CACHE_HOME' ) ) :
false;
69 $this->cacheDir = $cacheHome ?
"$cacheHome/mw-foreign" :
"{$this->libDir}/.foreign/cache";
77 $actions = [
'update',
'verify',
'make-sri' ];
78 if ( !in_array(
$action, $actions ) ) {
79 $this->
error(
"Invalid action.\n\nMust be one of " . implode(
', ', $actions ) .
'.' );
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 ] ];
90 $this->
error(
"Unknown module name.\n\nMust be one of:\n" .
91 wordwrap( implode(
', ', array_keys( $this->registry ) ), 80 ) .
98 $this->
verbose(
"\n### {$moduleName}\n\n" );
99 $destDir =
"{$this->libDir}/$moduleName";
101 if ( $this->
action ===
'update' ) {
102 $this->
output(
"... updating '{$moduleName}'\n" );
103 $this->
verbose(
"... emptying directory for $moduleName\n" );
105 } elseif ( $this->
action ===
'verify' ) {
106 $this->
output(
"... verifying '{$moduleName}'\n" );
108 $this->
output(
"... checking '{$moduleName}'\n" );
111 $this->
verbose(
"... preparing {$this->tmpParentDir}\n" );
114 throw new Exception(
"Unable to create {$this->tmpParentDir}" );
117 if ( !isset( $info[
'type'] ) ) {
118 throw new Exception(
"Module '$moduleName' must have a 'type' key." );
120 switch ( $info[
'type'] ) {
131 throw new Exception(
"Unknown type '{$info['type']}' for '$moduleName'" );
135 $this->
output(
"\nDone!\n" );
137 if ( $this->hasErrors ) {
146 $key = basename( $src ) .
'_' . substr( $integrity, -12 );
147 $key = preg_replace(
'/[.\/+?=_-]+/',
'_', $key );
148 return rtrim( $key,
'_' );
153 return Wikimedia\quietCall(
'file_get_contents',
"{$this->cacheDir}/$key.data" );
158 file_put_contents(
"{$this->cacheDir}/$key.data",
$data, LOCK_EX );
161 private function fetch( $src, $integrity ) {
162 $key = $this->
cacheKey( $src, $integrity );
169 if ( !
$req->execute()->isOK() ) {
170 throw new Exception(
"Failed to download resource at {$src}" );
172 if (
$req->getStatus() !== 200 ) {
173 throw new Exception(
"Unexpected HTTP {$req->getStatus()} response from {$src}" );
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" );
181 } elseif ( $this->
action ===
'make-sri' ) {
182 $this->
output(
"Integrity for {$src}\n\tintegrity: ${actualIntegrity}\n" );
184 throw new Exception(
"Integrity check failed for {$src}\n" .
185 "\tExpected: {$integrity}\n" .
186 "\tActual: {$actualIntegrity}"
193 if ( !isset( $info[
'src'] ) ) {
194 throw new Exception(
"Module '$moduleName' must have a 'src' key." );
196 $data = $this->
fetch( $info[
'src'], $info[
'integrity'] ??
null );
197 $dest = $info[
'dest'] ?? basename( $info[
'src'] );
198 $path =
"$destDir/$dest";
200 throw new Exception(
"File for '$moduleName' is different." );
202 if ( $this->
action ===
'update' ) {
204 file_put_contents(
"$destDir/$dest",
$data );
209 if ( !isset( $info[
'files'] ) ) {
210 throw new Exception(
"Module '$moduleName' must have a 'files' key." );
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." );
217 $path =
"$destDir/$dest";
219 throw new Exception(
"File '$dest' for '$moduleName' is different." );
220 } elseif ( $this->
action ===
'update' ) {
222 file_put_contents(
"$destDir/$dest",
$data );
228 $info += [
'src' =>
null,
'integrity' =>
null,
'dest' =>
null ];
229 if ( $info[
'src'] ===
null ) {
230 throw new Exception(
"Module '$moduleName' must have a 'src' key." );
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 );
242 if ( $info[
'dest'] ===
null ) {
244 $toCopy = [ $tmpDir => $destDir ];
248 foreach ( $info[
'dest']
as $fromSubPath => $toSubPath ) {
250 $fromPaths = glob(
"{$tmpDir}/{$fromSubPath}", GLOB_BRACE );
252 throw new Exception(
"Path '$fromSubPath' of '$moduleName' not found." );
254 foreach ( $fromPaths
as $fromPath ) {
255 $toCopy[$fromPath] = $toSubPath ===
null
256 ?
"$destDir/" . basename( $fromPath )
257 :
"$destDir/$toSubPath/" . basename( $fromPath );
261 foreach ( $toCopy
as $from => $to ) {
262 if ( $this->
action ===
'verify' ) {
263 $this->
verbose(
"... verifying $to\n" );
264 if ( is_dir( $from ) ) {
265 $rii =
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
267 RecursiveDirectoryIterator::SKIP_DOTS
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;
278 } elseif ( sha1_file( $from ) !== sha1_file( $to ) ) {
279 $this->
error(
"File '$to' is different." );
280 $this->hasErrors =
true;
282 } elseif ( $this->
action ===
'update' ) {
283 $this->
verbose(
"... moving $from to $to\n" );
285 if ( !rename( $from, $to ) ) {
286 throw new Exception(
"Could not move $from to $to." );
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'] );
318 foreach ( glob(
"{$this->cacheDir}/*" )
as $cacheFile ) {
319 if ( !in_array( basename( $cacheFile,
'.data' ), $knownKeys ) ) {
320 unlink( $cacheFile );
341 $trimmed = ltrim( $text,
' ' );
342 if ( $trimmed ===
'' || $trimmed[0] ===
'#' ) {
345 $indent = strlen( $text ) - strlen( $trimmed );
346 if ( $indent % 2 !== 0 ) {
347 throw new Exception( __METHOD__ .
": Odd indentation on line $line." );
349 $depth = $indent === 0 ? 0 : ( $indent / 2 );
350 if ( $depth < $prev ) {
352 array_splice( $stack, $depth + 1 );
354 if ( !array_key_exists( $depth, $stack ) ) {
355 throw new Exception( __METHOD__ .
": Too much indentation on line $line." );
357 if ( strpos( $trimmed,
':' ) ===
false ) {
358 throw new Exception( __METHOD__ .
": Missing colon on line $line." );
360 $dest =& $stack[ $depth ];
361 if ( $dest ===
null ) {
365 list( $key, $val ) = explode(
':', $trimmed, 2 );
366 $val = ltrim( $val,
' ' );
369 $dest[ $key ] = $val;
374 $dest[ $key ] = &$val;
377 unset( $dest, $val );