Merge "Improve docs for Title::getInternalURL/getCanonicalURL"
[lhc/web/wiklou.git] / includes / ForeignResourceManager.php
index 18014fa..dddbad5 100644 (file)
@@ -30,10 +30,12 @@ class ForeignResourceManager {
        private $registryFile;
        private $libDir;
        private $tmpParentDir;
+       private $cacheDir;
        private $infoPrinter;
        private $errorPrinter;
        private $verbosePrinter;
        private $action;
+       private $registry;
 
        /**
         * @param string $registryFile Path to YAML file
@@ -60,8 +62,11 @@ class ForeignResourceManager {
 
                // Use a temporary directory under the destination directory instead
                // of wfTempDir() because PHP's rename() does not work across file
-               // systems, as the user's /tmp and $IP may be on different filesystems.
-               $this->tmpParentDir = "{$this->libDir}/.tmp";
+               // systems, and the user's /tmp and $IP may be on different filesystems.
+               $this->tmpParentDir = "{$this->libDir}/.foreign/tmp";
+
+               $cacheHome = getenv( 'XDG_CACHE_HOME' ) ? realpath( getenv( 'XDG_CACHE_HOME' ) ) : false;
+               $this->cacheDir = $cacheHome ? "$cacheHome/mw-foreign" : "{$this->libDir}/.foreign/cache";
        }
 
        /**
@@ -69,18 +74,24 @@ class ForeignResourceManager {
         * @throws Exception
         */
        public function run( $action, $module ) {
-               if ( !in_array( $action, [ 'update', 'verify', 'make-sri' ] ) ) {
-                       throw new Exception( 'Invalid action parameter.' );
+               $actions = [ 'update', 'verify', 'make-sri' ];
+               if ( !in_array( $action, $actions ) ) {
+                       $this->error( "Invalid action.\n\nMust be one of " . implode( ', ', $actions ) . '.' );
+                       return false;
                }
                $this->action = $action;
 
-               $registry = $this->parseBasicYaml( file_get_contents( $this->registryFile ) );
+               $this->registry = $this->parseBasicYaml( file_get_contents( $this->registryFile ) );
                if ( $module === 'all' ) {
-                       $modules = $registry;
-               } elseif ( isset( $registry[ $module ] ) ) {
-                       $modules = [ $module => $registry[ $module ] ];
+                       $modules = $this->registry;
+               } elseif ( isset( $this->registry[ $module ] ) ) {
+                       $modules = [ $module => $this->registry[ $module ] ];
                } else {
-                       throw new Exception( 'Unknown module name.' );
+                       $this->error( "Unknown module name.\n\nMust be one of:\n" .
+                               wordwrap( implode( ', ', array_keys( $this->registry ) ), 80 ) .
+                               '.'
+                       );
+                       return false;
                }
 
                foreach ( $modules as $moduleName => $info ) {
@@ -121,8 +132,8 @@ class ForeignResourceManager {
                        }
                }
 
-               $this->cleanUp();
                $this->output( "\nDone!\n" );
+               $this->cleanUp();
                if ( $this->hasErrors ) {
                        // The verify mode should check all modules/files and fail after, not during.
                        return false;
@@ -131,7 +142,29 @@ class ForeignResourceManager {
                return true;
        }
 
+       private function cacheKey( $src, $integrity ) {
+               $key = basename( $src ) . '_' . substr( $integrity, -12 );
+               $key = preg_replace( '/[.\/+?=_-]+/', '_', $key );
+               return rtrim( $key, '_' );
+       }
+
+       /** @return string|false */
+       private function cacheGet( $key ) {
+               return Wikimedia\quietCall( 'file_get_contents', "{$this->cacheDir}/$key.data" );
+       }
+
+       private function cacheSet( $key, $data ) {
+               wfMkdirParents( $this->cacheDir );
+               file_put_contents( "{$this->cacheDir}/$key.data", $data, LOCK_EX );
+       }
+
        private function fetch( $src, $integrity ) {
+               $key = $this->cacheKey( $src, $integrity );
+               $data = $this->cacheGet( $key );
+               if ( $data ) {
+                       return $data;
+               }
+
                $req = MWHttpRequest::factory( $src, [ 'method' => 'GET', 'followRedirects' => false ] );
                if ( !$req->execute()->isOK() ) {
                        throw new Exception( "Failed to download resource at {$src}" );
@@ -144,15 +177,14 @@ class ForeignResourceManager {
                $actualIntegrity = $algo . '-' . base64_encode( hash( $algo, $data, true ) );
                if ( $integrity === $actualIntegrity ) {
                        $this->verbose( "... passed integrity check for {$src}\n" );
+                       $this->cacheSet( $key, $data );
+               } elseif ( $this->action === 'make-sri' ) {
+                       $this->output( "Integrity for {$src}\n\tintegrity: ${actualIntegrity}\n" );
                } else {
-                       if ( $this->action === 'make-sri' ) {
-                               $this->output( "Integrity for {$src}\n\tintegrity: ${actualIntegrity}\n" );
-                       } else {
-                               throw new Exception( "Integrity check failed for {$src}\n" .
-                                       "\tExpected: {$integrity}\n" .
-                                       "\tActual: {$actualIntegrity}"
-                               );
-                       }
+                       throw new Exception( "Integrity check failed for {$src}\n" .
+                               "\tExpected: {$integrity}\n" .
+                               "\tActual: {$actualIntegrity}"
+                       );
                }
                return $data;
        }
@@ -234,6 +266,7 @@ class ForeignResourceManager {
                                                $from,
                                                RecursiveDirectoryIterator::SKIP_DOTS
                                        ) );
+                                       /** @var SplFileInfo $file */
                                        foreach ( $rii as $file ) {
                                                $remote = $file->getPathname();
                                                $local = strtr( $remote, [ $from => $to ] );
@@ -270,6 +303,23 @@ class ForeignResourceManager {
 
        private function cleanUp() {
                wfRecursiveRemoveDir( $this->tmpParentDir );
+
+               // Prune the cache of files we don't recognise.
+               $knownKeys = [];
+               foreach ( $this->registry as $info ) {
+                       if ( $info['type'] === 'file' || $info['type'] === 'tar' ) {
+                               $knownKeys[] = $this->cacheKey( $info['src'], $info['integrity'] );
+                       } elseif ( $info['type'] === 'multi-file' ) {
+                               foreach ( $info['files'] as $file ) {
+                                       $knownKeys[] = $this->cacheKey( $file['src'], $file['integrity'] );
+                               }
+                       }
+               }
+               foreach ( glob( "{$this->cacheDir}/*" ) as $cacheFile ) {
+                       if ( !in_array( basename( $cacheFile, '.data' ), $knownKeys ) ) {
+                               unlink( $cacheFile );
+                       }
+               }
        }
 
        /**