diff --git a/features/package-install.feature b/features/package-install.feature index 7b74eb4f5b..5f0a0216cd 100644 --- a/features/package-install.feature +++ b/features/package-install.feature @@ -1,5 +1,9 @@ Feature: Install WP-CLI packages + Background: + When I run `wp package path` + Then save STDOUT as {PACKAGE_PATH} + Scenario: Install a package with an http package index url in package composer.json Given an empty directory And a composer.json file: @@ -65,9 +69,6 @@ Feature: Install WP-CLI packages Scenario: Install a package with a dependency Given an empty directory - When I run `wp package path` - Then save STDOUT as {PACKAGE_PATH} - When I run `wp package install trendwerk/faker` Then STDOUT should contain: """ @@ -123,9 +124,6 @@ Feature: Install WP-CLI packages Scenario: Install a package from a Git URL Given an empty directory - When I run `wp package path` - Then save STDOUT as {PACKAGE_PATH} - When I try `wp package install git@github.com:wp-cli.git` Then STDERR should be: """ @@ -150,6 +148,105 @@ Feature: Install WP-CLI packages | name | | wp-cli/google-sitemap-generator-cli | + When I run `wp google-sitemap` + Then STDOUT should contain: + """ + usage: wp google-sitemap rebuild + """ + + When I run `wp package uninstall wp-cli/google-sitemap-generator-cli` + Then STDOUT should contain: + """ + Removing require statement from {PACKAGE_PATH}composer.json + """ + And STDOUT should contain: + """ + Success: Uninstalled package. + """ + + When I run `wp package list --fields=name` + Then STDOUT should not contain: + """ + wp-cli/google-sitemap-generator-cli + """ + + Scenario: Install a package in a local zip + Given an empty directory + And I run `wget -O google-sitemap-generator-cli.zip https://github.com/wp-cli/google-sitemap-generator-cli/archive/master.zip` + + When I run `wp package install google-sitemap-generator-cli.zip` + Then STDOUT should contain: + """ + Installing package wp-cli/google-sitemap-generator-cli (dev-master) + Updating {PACKAGE_PATH}composer.json to require the package... + Registering {PACKAGE_PATH}local/wp-cli-google-sitemap-generator-cli as a path repository... + Using Composer to install the package... + """ + And STDOUT should contain: + """ + Success: Package installed successfully. + """ + + When I run `wp package list --fields=name` + Then STDOUT should be a table containing rows: + | name | + | wp-cli/google-sitemap-generator-cli | + + When I run `wp google-sitemap` + Then STDOUT should contain: + """ + usage: wp google-sitemap rebuild + """ + + When I run `wp package uninstall wp-cli/google-sitemap-generator-cli` + Then STDOUT should contain: + """ + Removing require statement from {PACKAGE_PATH}composer.json + """ + And STDOUT should contain: + """ + Success: Uninstalled package. + """ + + When I run `wp package list --fields=name` + Then STDOUT should not contain: + """ + wp-cli/google-sitemap-generator-cli + """ + + Scenario: Install a package from a remote ZIP + Given an empty directory + + When I try `wp package install https://github.com/wp-cli/google-sitemap-generator.zip` + Then STDERR should be: + """ + Error: Couldn't download package. + """ + + When I run `wp package install https://github.com/wp-cli/google-sitemap-generator-cli/archive/master.zip` + Then STDOUT should contain: + """ + Installing package wp-cli/google-sitemap-generator-cli (dev-master) + Updating {PACKAGE_PATH}composer.json to require the package... + Registering {PACKAGE_PATH}local/wp-cli-google-sitemap-generator-cli as a path repository... + Using Composer to install the package... + """ + And STDOUT should contain: + """ + Success: Package installed successfully. + """ + + When I run `wp package list --fields=name` + Then STDOUT should be a table containing rows: + | name | + | wp-cli/google-sitemap-generator-cli | + + When I run `wp google-sitemap` + Then STDOUT should contain: + """ + usage: wp google-sitemap rebuild + """ + When I run `wp package uninstall wp-cli/google-sitemap-generator-cli` Then STDOUT should contain: """ @@ -193,9 +290,6 @@ Feature: Install WP-CLI packages } """ - When I run `wp package path` - Then save STDOUT as {PACKAGE_PATH} - When I run `pwd` Then save STDOUT as {CURRENT_PATH} diff --git a/php/WP_CLI/Extractor.php b/php/WP_CLI/Extractor.php new file mode 100644 index 0000000000..defdc51278 --- /dev/null +++ b/php/WP_CLI/Extractor.php @@ -0,0 +1,138 @@ +open( $zipfile ); + if ( true === $res ) { + $tempdir = implode( DIRECTORY_SEPARATOR, Array ( + dirname( $zipfile ), + basename( $zipfile, '.zip' ), + $zip->getNameIndex( 0 ) + ) ); + + $zip->extractTo( dirname( $tempdir ) ); + $zip->close(); + + self::copy_overwrite_files( $tempdir, $dest ); + self::rmdir( dirname( $tempdir ) ); + } else { + throw Exception( $res ); + } + } + + /** + * Extract a tarball to a specific destination. + * + * @param string $tarball + * @param string $dest + */ + private static function extract_tarball( $tarball, $dest ) { + if ( ! class_exists( 'PharData' ) ) { + $cmd = "tar xz --strip-components=1 --directory=%s -f $tarball"; + WP_CLI::launch( Utils\esc_cmd( $cmd, $dest ) ); + return; + } + $phar = new PharData( $tarball ); + $tempdir = implode( DIRECTORY_SEPARATOR, Array ( + dirname( $tarball ), + basename( $tarball, '.tar.gz' ), + $phar->getFileName() + ) ); + + $phar->extractTo( dirname( $tempdir ), null, true ); + + self::copy_overwrite_files( $tempdir, $dest ); + + self::rmdir( dirname( $tempdir ) ); + } + + public static function copy_overwrite_files( $source, $dest ) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $source, RecursiveDirectoryIterator::SKIP_DOTS ), + RecursiveIteratorIterator::SELF_FIRST); + + $error = 0; + + if ( ! is_dir( $dest ) ) { + mkdir( $dest, 0777, true ); + } + + foreach ( $iterator as $item ) { + + $dest_path = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); + + if ( $item->isDir() ) { + if ( !is_dir( $dest_path ) ) { + mkdir( $dest_path ); + } + } else { + if ( file_exists( $dest_path ) && is_writable( $dest_path ) ) { + copy( $item, $dest_path ); + } elseif ( ! file_exists( $dest_path ) ) { + copy( $item, $dest_path ); + } else { + $error = 1; + WP_CLI::warning( "Unable to copy '" . $iterator->getSubPathName() . "' to current directory." ); + } + } + } + + if ( $error ) { + throw new Exception( 'There was an error overwriting existing files.' ); + } + } + + public static function rmdir( $dir ) { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $files as $fileinfo ) { + $todo = $fileinfo->isDir() ? 'rmdir' : 'unlink'; + $todo( $fileinfo->getRealPath() ); + } + rmdir( $dir ); + } + +} diff --git a/php/commands/core.php b/php/commands/core.php index 7e44795ea2..063c4e3ac8 100644 --- a/php/commands/core.php +++ b/php/commands/core.php @@ -1,6 +1,7 @@ getMessage() ); } @@ -239,108 +240,6 @@ public function download( $args, $assoc_args ) { WP_CLI::success( 'WordPress downloaded.' ); } - private static function _extract( $tarball_or_zip, $dest ) { - $path_parts = pathinfo( $tarball_or_zip ); - - if ( preg_match( '/\.zip$/', $tarball_or_zip ) ) { - return self::_extract_zip( $tarball_or_zip, $dest ); - } - - if ( preg_match( '/\.tar\.gz$/', $tarball_or_zip ) ) { - return self::_extract_tarball( $tarball_or_zip, $dest ); - } - - WP_CLI::error( sprintf( 'Extension %s not supported.', $extension ) ); - } - - private static function _extract_tarball( $tarball, $dest ) { - if ( ! class_exists( 'PharData' ) ) { - $cmd = "tar xz --strip-components=1 --directory=%s -f $tarball"; - WP_CLI::launch( Utils\esc_cmd( $cmd, $dest ) ); - return; - } - $phar = new PharData( $tarball ); - $tempdir = implode( DIRECTORY_SEPARATOR, Array ( - dirname( $tarball ), - basename( $tarball, '.tar.gz' ), - $phar->getFileName() - ) ); - - $phar->extractTo( dirname( $tempdir ), null, true ); - - self::_copy_overwrite_files( $tempdir, $dest ); - - self::_rmdir( dirname( $tempdir ) ); - } - - private static function _extract_zip( $zipfile, $dest ) { - if ( ! class_exists( 'ZipArchive' ) ) { - throw new \Exception( 'Extracting a zip file requires ZipArchive.' ); - } - $zip = new ZipArchive(); - $res = $zip->open( $zipfile ); - if ( true === $res ) { - $tempdir = implode( DIRECTORY_SEPARATOR, Array ( - dirname( $zipfile ), - basename( $zipfile, '.zip' ), - $zip->getNameIndex( 0 ) - ) ); - - $zip->extractTo( dirname( $tempdir ) ); - $zip->close(); - - self::_copy_overwrite_files( $tempdir, $dest ); - self::_rmdir( dirname( $tempdir ) ); - } else { - throw \Exception( $res ); - } - } - - private static function _copy_overwrite_files( $source, $dest ) { - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $source, RecursiveDirectoryIterator::SKIP_DOTS ), - RecursiveIteratorIterator::SELF_FIRST); - - $error = 0; - - foreach ( $iterator as $item ) { - - $dest_path = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); - - if ( $item->isDir() ) { - if ( !is_dir( $dest_path ) ) { - mkdir( $dest_path ); - } - } else { - if ( file_exists( $dest_path ) && is_writable( $dest_path ) ) { - copy( $item, $dest_path ); - } elseif ( ! file_exists( $dest_path ) ) { - copy( $item, $dest_path ); - } else { - $error = 1; - WP_CLI::warning( "Unable to copy '" . $iterator->getSubPathName() . "' to current directory." ); - } - } - } - - if ( $error ) { - WP_CLI::error( 'There was an error downloading all WordPress files.' ); - } - } - - private static function _rmdir( $dir ) { - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ( $files as $fileinfo ) { - $todo = $fileinfo->isDir() ? 'rmdir' : 'unlink'; - $todo( $fileinfo->getRealPath() ); - } - rmdir( $dir ); - } - private static function _read( $url ) { $headers = array('Accept' => 'application/json'); $response = Utils\http_request( 'GET', $url, null, $headers, array( 'timeout' => 30 ) ); diff --git a/php/commands/package.php b/php/commands/package.php index 19bd5137e0..d4f31c300b 100644 --- a/php/commands/package.php +++ b/php/commands/package.php @@ -15,6 +15,7 @@ use \Composer\Repository\RepositoryManager; use \Composer\Util\Filesystem; use \WP_CLI\ComposerIO; +use \WP_CLI\Extractor; use \WP_CLI\Utils; /** @@ -135,20 +136,25 @@ public function browse( $_, $assoc_args ) { * * Package name from WP-CLI's package index. * * Git URL accessible by the current shell user. * * Path to a directory on the local machine. + * * Local or remote .zip file. * - * Note: When installing a local directory, WP-CLI simply registers a + * When installing a local directory, WP-CLI simply registers a * reference to the directory. If you move or delete the directory, WP-CLI's * reference breaks. * + * When installing a .zip file, WP-CLI extracts the package to + * `~/.wp-cli/packages/local/`. + * * ## OPTIONS * - * - * : Name, git URL, or directory path for the package to install. Names can - * optionally include a version constraint (e.g. wp-cli/server-command:@stable) + * + * : Name, git URL, directory path, or .zip file for the package to install. + * Names can optionally include a version constraint + * (e.g. wp-cli/server-command:@stable). * * ## EXAMPLES * - * # Install the latest development version from the package index + * # Install the latest development version from the package index. * $ wp package install wp-cli/server-command * Installing package wp-cli/server-command (dev-master) * Updating /home/person/.wp-cli/packages/composer.json to require the package... @@ -166,11 +172,14 @@ public function browse( $_, $assoc_args ) { * --- * Success: Package installed successfully. * - * # Install the latest stable version + * # Install the latest stable version. * $ wp package install wp-cli/server-command:@stable * - * # Install a package hosted at a git URL + * # Install a package hosted at a git URL. * $ wp package install git@github.com:runcommand/hook.git + * + * # Install a package in a .zip file. + * $ wp package install google-sitemap-generator-cli.zip */ public function install( $args, $assoc_args ) { list( $package_name ) = $args; @@ -185,22 +194,42 @@ public function install( $args, $assoc_args ) { } else { WP_CLI::error( "Couldn't parse package name from expected path '/'." ); } + } else if ( ( false !== strpos( $package_name, '://' ) && false !== stripos( $package_name, '.zip' ) ) + || ( pathinfo( $package_name, PATHINFO_EXTENSION ) === 'zip' && is_file( $package_name ) ) ) { + // Download the remote ZIP file to a temp directory + if ( false !== strpos( $package_name, '://' ) ) { + $temp = Utils\get_temp_dir() . uniqid('package_') . ".zip"; + $options = array( + 'timeout' => 600, + 'filename' => $temp + ); + $response = Utils\http_request( 'GET', $package_name, null, array(), $options ); + if ( 20 != substr( $response->status_code, 0, 2 ) ) { + WP_CLI::error( "Couldn't download package." ); + } + $package_name = $temp; + } + $dir_package = Utils\get_temp_dir() . uniqid( 'package_' ); + try { + // Extract the package to get the package name + Extractor::extract( $package_name, $dir_package ); + $package_name = self::get_package_name_from_dir_package( $dir_package ); + // Move to a location based on the package name + $local_dir = rtrim( WP_CLI::get_runner()->get_packages_dir_path(), '/' ) . '/local/'; + $actual_dir_package = $local_dir . str_replace( '/', '-', $package_name ); + Extractor::copy_overwrite_files( $dir_package, $actual_dir_package ); + Extractor::rmdir( $dir_package ); + // Behold, the extracted package + $dir_package = $actual_dir_package; + } catch ( Exception $e ) { + WP_CLI::error( $e->getMessage() ); + } } else if ( is_dir( $package_name ) && file_exists( $package_name . '/composer.json' ) ) { $dir_package = $package_name; if ( ! Utils\is_path_absolute( $dir_package ) ) { $dir_package = getcwd() . DIRECTORY_SEPARATOR . $dir_package; } - $composer_file = $dir_package . '/composer.json'; - $package_name = ''; - if ( file_exists( $composer_file ) ) { - $composer_data = json_decode( file_get_contents( $composer_file ), true ); - if ( ! empty( $composer_data['name'] ) ) { - $package_name = $composer_data['name']; - } - } - if ( empty( $package_name ) ) { - WP_CLI::error( "Invalid package." ); - } + $package_name = self::get_package_name_from_dir_package( $dir_package ); } else { if ( false !== strpos( $package_name, ':' ) ) { list( $package_name, $version ) = explode( ':', $package_name ); @@ -587,6 +616,27 @@ private function is_package_installed( $package_name ) { } } + /** + * Get the name of the package from the composer.json in a directory path + * + * @param string $dir_package + * @return string + */ + private static function get_package_name_from_dir_package( $dir_package ) { + $composer_file = $dir_package . '/composer.json'; + $package_name = ''; + if ( file_exists( $composer_file ) ) { + $composer_data = json_decode( file_get_contents( $composer_file ), true ); + if ( ! empty( $composer_data['name'] ) ) { + $package_name = $composer_data['name']; + } + } + if ( empty( $package_name ) ) { + WP_CLI::error( "Invalid package." ); + } + return $package_name; + } + /** * Get the composer.json object */