22 use Wikimedia\AtEase\AtEase;
68 $this->tmpParentDir =
"{$this->libDir}/.foreign/tmp";
70 $cacheHome = getenv(
'XDG_CACHE_HOME' ) ? realpath( getenv(
'XDG_CACHE_HOME' ) ) :
false;
71 $this->cacheDir = $cacheHome ?
"$cacheHome/mw-foreign" :
"{$this->libDir}/.foreign/cache";
79 $actions = [
'update',
'verify',
'make-sri' ];
80 if ( !in_array(
$action, $actions ) ) {
81 $this->
error(
"Invalid action.\n\nMust be one of " . implode(
', ', $actions ) .
'.' );
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 ] ];
92 $this->
error(
"Unknown module name.\n\nMust be one of:\n" .
93 wordwrap( implode(
', ', array_keys( $this->registry ) ), 80 ) .
99 foreach (
$modules as $moduleName => $info ) {
100 $this->
verbose(
"\n### {$moduleName}\n\n" );
101 $destDir =
"{$this->libDir}/$moduleName";
103 if ( $this->action ===
'update' ) {
104 $this->
output(
"... updating '{$moduleName}'\n" );
105 $this->
verbose(
"... emptying directory for $moduleName\n" );
107 } elseif ( $this->action ===
'verify' ) {
108 $this->
output(
"... verifying '{$moduleName}'\n" );
110 $this->
output(
"... checking '{$moduleName}'\n" );
113 $this->
verbose(
"... preparing {$this->tmpParentDir}\n" );
116 throw new Exception(
"Unable to create {$this->tmpParentDir}" );
119 if ( !isset( $info[
'type'] ) ) {
120 throw new Exception(
"Module '$moduleName' must have a 'type' key." );
122 switch ( $info[
'type'] ) {
133 throw new Exception(
"Unknown type '{$info['type']}' for '$moduleName'" );
137 $this->
output(
"\nDone!\n" );
139 if ( $this->hasErrors ) {
148 $key = basename( $src ) .
'_' . substr( $integrity, -12 );
149 $key = preg_replace(
'/[.\/+?=_-]+/',
'_', $key );
150 return rtrim( $key,
'_' );
155 return AtEase::quietCall(
'file_get_contents',
"{$this->cacheDir}/$key.data" );
160 file_put_contents(
"{$this->cacheDir}/$key.data", $data, LOCK_EX );
163 private function fetch( $src, $integrity ) {
164 $key = $this->
cacheKey( $src, $integrity );
171 if ( !$req->execute()->isOK() ) {
172 throw new Exception(
"Failed to download resource at {$src}" );
174 if ( $req->getStatus() !== 200 ) {
175 throw new Exception(
"Unexpected HTTP {$req->getStatus()} response from {$src}" );
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" );
183 } elseif ( $this->action ===
'make-sri' ) {
184 $this->
output(
"Integrity for {$src}\n\tintegrity: ${actualIntegrity}\n" );
186 throw new Exception(
"Integrity check failed for {$src}\n" .
187 "\tExpected: {$integrity}\n" .
188 "\tActual: {$actualIntegrity}"
195 if ( !isset( $info[
'src'] ) ) {
196 throw new Exception(
"Module '$moduleName' must have a 'src' key." );
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." );
204 if ( $this->action ===
'update' ) {
206 file_put_contents(
"$destDir/$dest", $data );
211 if ( !isset( $info[
'files'] ) ) {
212 throw new Exception(
"Module '$moduleName' must have a 'files' key." );
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." );
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' ) {
224 file_put_contents(
"$destDir/$dest", $data );
230 $info += [
'src' =>
null,
'integrity' =>
null,
'dest' => null ];
231 if ( $info[
'src'] ===
null ) {
232 throw new Exception(
"Module '$moduleName' must have a 'src' key." );
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 );
244 if ( $info[
'dest'] ===
null ) {
246 $toCopy = [ $tmpDir => $destDir ];
250 foreach ( $info[
'dest'] as $fromSubPath => $toSubPath ) {
252 $fromPaths = glob(
"{$tmpDir}/{$fromSubPath}", GLOB_BRACE );
254 throw new Exception(
"Path '$fromSubPath' of '$moduleName' not found." );
256 foreach ( $fromPaths as $fromPath ) {
257 $toCopy[$fromPath] = $toSubPath ===
null
258 ?
"$destDir/" . basename( $fromPath )
259 :
"$destDir/$toSubPath/" . basename( $fromPath );
263 foreach ( $toCopy as $from => $to ) {
264 if ( $this->action ===
'verify' ) {
265 $this->
verbose(
"... verifying $to\n" );
266 if ( is_dir( $from ) ) {
267 $rii =
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
269 RecursiveDirectoryIterator::SKIP_DOTS
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;
280 } elseif ( sha1_file( $from ) !== sha1_file( $to ) ) {
281 $this->
error(
"File '$to' is different." );
282 $this->hasErrors =
true;
284 } elseif ( $this->action ===
'update' ) {
285 $this->
verbose(
"... moving $from to $to\n" );
287 if ( !rename( $from, $to ) ) {
288 throw new Exception(
"Could not move $from to $to." );
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'] );
320 foreach ( glob(
"{$this->cacheDir}/*" ) as $cacheFile ) {
321 if ( !in_array( basename( $cacheFile,
'.data' ), $knownKeys ) ) {
322 unlink( $cacheFile );
337 $lines = explode(
"\n", $input );
341 foreach (
$lines as $i => $text ) {
343 $trimmed = ltrim( $text,
' ' );
344 if ( $trimmed ===
'' || $trimmed[0] ===
'#' ) {
347 $indent = strlen( $text ) - strlen( $trimmed );
348 if ( $indent % 2 !== 0 ) {
349 throw new Exception( __METHOD__ .
": Odd indentation on line $line." );
351 $depth = $indent === 0 ? 0 : ( $indent / 2 );
352 if ( $depth < $prev ) {
354 array_splice( $stack, $depth + 1 );
356 if ( !array_key_exists( $depth, $stack ) ) {
357 throw new Exception( __METHOD__ .
": Too much indentation on line $line." );
359 if ( strpos( $trimmed,
':' ) ===
false ) {
360 throw new Exception( __METHOD__ .
": Missing colon on line $line." );
362 $dest =& $stack[ $depth ];
363 if ( $dest ===
null ) {
367 list( $key, $val ) = explode(
':', $trimmed, 2 );
368 $val = ltrim( $val,
' ' );
371 $dest[ $key ] = $val;
376 $dest[ $key ] = &$val;
379 unset( $dest, $val );