"wp-cli/php-cli-tools": "0.9.3",
"mustache/mustache": "~2.4",
"rhumsaa/array_column": "~1.1",
- "rmccue/requests": "~1.6"
+ "rmccue/requests": "~1.6",
+ "symfony/finder": "~2.3"
},
"require-dev": {
"symfony/finder": "~2.3"
\WP_CLI::line( "Available {$this->item_type} updates:" );
\WP_CLI\Utils\format_items( 'table', $items_to_update,
- array( 'name', 'status', 'version' ) );
+ array( 'name', 'status', 'version', 'update_version' ) );
return;
}
// Only attempt to update if there is something to update
if ( !empty( $items_to_update ) ) {
+ $cache_manager = \WP_CLI::get_http_cache_manager();
+ foreach ($items_to_update as $item) {
+ $cache_manager->whitelist_package($item['update_package'], $this->item_type, $item['name'], $item['update_version']);
+ }
$upgrader = $this->get_upgrader( $assoc_args );
$result = $upgrader->bulk_upgrade( wp_list_pluck( $items_to_update, 'update_id' ) );
}
return isset( $update_list->response[ $slug ] );
}
+ /**
+ * Get the available update info
+ *
+ * @param string $slug The plugin/theme slug
+ *
+ * @return array|null
+ */
+ protected function get_update_info( $slug ) {
+ $update_list = get_site_transient( $this->upgrade_transient );
+ return isset( $update_list->response[ $slug ] ) ? (array) $update_list->response[ $slug ] : null;
+ }
+
private $map = array(
'short' => array(
'inactive' => 'I',
--- /dev/null
+<?php
+
+/*
+ * This file is heavily inspired and use code from Composer(getcomposer.org),
+ * in particular Composer/Cache and Composer/Util/FileSystem from 1.0.0-alpha7
+ *
+ * The original code and this file are both released under MIT license.
+ *
+ * The copyright holders of the original code are:
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ */
+
+namespace WP_CLI;
+
+use Symfony\Component\Finder\Finder;
+
+/**
+ * Reads/writes to a filesystem cache
+ */
+class FileCache {
+
+ /**
+ * @var string cache path
+ */
+ protected $root;
+ /**
+ * @var bool
+ */
+ protected $enabled = true;
+ /**
+ * @var int files time to live
+ */
+ protected $ttl;
+ /**
+ * @var int max total size
+ */
+ protected $maxSize;
+ /**
+ * @var string key allowed chars (regex class)
+ */
+ protected $whitelist;
+
+ /**
+ * @param string $cacheDir location of the cache
+ * @param int $ttl cache files default time to live (expiration)
+ * @param int $maxSize max total cache size
+ * @param string $whitelist List of characters that are allowed in path names (used in a regex character class)
+ */
+ public function __construct( $cacheDir, $ttl, $maxSize, $whitelist = 'a-z0-9.' ) {
+ $this->root = rtrim( $cacheDir, '/\\' ) . '/';
+ $this->ttl = (int) $ttl;
+ $this->maxSize = (int) $maxSize;
+ $this->whitelist = $whitelist;
+
+ if ( !$this->ensure_dir_exists( $this->root ) ) {
+ $this->enabled = false;
+ }
+
+ }
+
+ /**
+ * Cache is enabled
+ *
+ * @return bool
+ */
+ public function is_enabled() {
+ return $this->enabled;
+ }
+
+ /**
+ * Cache root
+ *
+ * @return string
+ */
+ public function get_root() {
+ return $this->root;
+ }
+
+
+ /**
+ * Check if a file is in cache and return its filename
+ *
+ * @param string $key cache key
+ * @param int $ttl time to live
+ * @return bool|string filename or false
+ */
+ public function has( $key, $ttl = null ) {
+ if ( !$this->enabled ) {
+ return false;
+ }
+
+ $filename = $this->filename( $key );
+
+ if ( !file_exists( $filename ) ) {
+ return false;
+ }
+
+ // use ttl param or global ttl
+ if ( $ttl === null ) {
+ $ttl = $this->ttl;
+ } elseif ( $this->ttl > 0 ) {
+ $ttl = min( (int) $ttl, $this->ttl );
+ } else {
+ $ttl = (int) $ttl;
+ }
+
+ //
+ if ( $ttl > 0 && filemtime( $filename ) + $ttl < time() ) {
+ if ( $this->ttl > 0 && $ttl >= $this->ttl ) {
+ unlink( $filename );
+ }
+ return false;
+ }
+
+ return $filename;
+ }
+
+ /**
+ * Write to cache file
+ *
+ * @param string $key cache key
+ * @param string $contents file contents
+ * @return bool
+ */
+ public function write( $key, $contents ) {
+ $filename = $this->prepare_write( $key );
+
+ if ( $filename ) {
+ return file_put_contents( $filename, $contents ) && touch( $filename );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Read from cache file
+ *
+ * @param string $key cache key
+ * @param int $ttl time to live
+ * @return bool|string file contents or false
+ */
+ public function read( $key, $ttl = null ) {
+ $filename = $this->has( $key, $ttl );
+
+ if ( $filename ) {
+ return file_get_contents( $filename );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Copy a file into the cache
+ *
+ * @param string $key cache key
+ * @param string $source source filename
+ * @return bool
+ */
+ public function import( $key, $source ) {
+ $filename = $this->prepare_write( $key );
+
+ if ( $filename ) {
+ return copy( $source, $filename ) && touch( $filename );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Copy a file out of the cache
+ *
+ * @param string $key cache key
+ * @param string $target target filename
+ * @param int $ttl time to live
+ * @return bool
+ */
+ public function export( $key, $target, $ttl = null ) {
+ $filename = $this->has( $key, $ttl );
+
+ if ( $filename ) {
+ return copy( $filename, $target );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Remove file from cache
+ *
+ * @param string $key cache key
+ * @return bool
+ */
+ public function remove( $key ) {
+ if ( !$this->enabled ) {
+ return false;
+ }
+
+ $filename = $this->filename( $key );
+
+ if ( file_exists( $filename ) ) {
+ return unlink( $filename );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Clean cache based on time to live and max size
+ *
+ * @return bool
+ */
+ public function clean() {
+ if ( !$this->enabled ) {
+ return false;
+ }
+
+ $ttl = $this->ttl;
+ $maxSize = $this->maxSize;
+
+ // unlink expired files
+ if ( $ttl > 0 ) {
+ $expire = new \DateTime();
+ $expire->modify( '-' . $ttl . ' seconds' );
+
+ $finder = $this->get_finder()->date( 'until ' . $expire->format( 'Y-m-d H:i:s' ) );
+ foreach ( $finder as $file ) {
+ unlink( $file->getRealPath() );
+ }
+ }
+
+ // unlink older files if max cache size is exceeded
+ if ( $maxSize > 0 ) {
+ $files = array_reverse( iterator_to_array( $this->get_finder()->sortByAccessedTime()->getIterator() ) );
+ $total = 0;
+
+ foreach ( $files as $file ) {
+ if ( $total + $file->getSize() <= $maxSize ) {
+ $total += $file->getSize();
+ } else {
+ unlink( $file->getRealPath() );
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Ensure directory exists
+ *
+ * @param string $dir directory
+ * @return bool
+ */
+ protected function ensure_dir_exists( $dir ) {
+ if ( !is_dir( $dir ) ) {
+ if ( file_exists( $dir ) ) {
+ // exists and not a dir
+ return false;
+ }
+ if ( !@mkdir( $dir, 0777, true ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Prepare cache write
+ *
+ * @param string $key cache key
+ * @return bool|string filename or false
+ */
+ protected function prepare_write( $key ) {
+ if ( !$this->enabled ) {
+ return false;
+ }
+
+ $filename = $this->filename( $key );
+
+ if ( !$this->ensure_dir_exists( dirname( $filename ) ) ) {
+ return false;
+ }
+
+ return $filename;
+ }
+
+ /**
+ * Validate cache key
+ *
+ * @param string $key cache key
+ * @return string relative filename
+ */
+ protected function validate_key( $key ) {
+ $url_parts = parse_url( $key );
+ if ( $url_parts['scheme'] ) { // is url
+ $parts = array('misc');
+ $parts[] = $url_parts['host'] . ( $url_parts['port'] ? '-' . $url_parts['port'] : '' );
+ $parts[] = $url_parts['path'] . ( $url_parts['query'] ? '-' . $url_parts['query'] : '' );
+ } else {
+ $key = str_replace( '\\', '/', $key );
+ $parts = explode( '/', ltrim( $key ) );
+ }
+
+ $parts = preg_replace( "#[^{$this->whitelist}]#i", '-', $parts );
+
+ return implode( '/', $parts );
+ }
+
+ /**
+ * Filename from key
+ *
+ * @param string $key
+ * @return string filename
+ */
+ protected function filename( $key ){
+ return $this->root . $this->validate_key( $key );
+ }
+
+ /**
+ * Get a Finder that iterates in cache root only the files
+ *
+ * @return Finder
+ */
+ protected function get_finder() {
+ return Finder::create()->in( $this->root )->files();
+ }
+}
--- /dev/null
+<?php
+
+
+namespace WP_CLI;
+use WP_CLI;
+
+
+/**
+ * Manage caching with whitelisting
+ *
+ * @package WP_CLI
+ */
+class WpHttpCacheManager {
+
+ /**
+ * @var array map whitelisted urls to keys and ttls
+ */
+ protected $whitelist = array();
+ /**
+ * @var FileCache
+ */
+ protected $cache;
+
+ /**
+ * @param FileCache $cache
+ */
+ public function __construct( FileCache $cache ) {
+ $this->cache = $cache;
+
+ // hook into wp http api
+ add_filter( 'pre_http_request', array($this, 'filter_pre_http_request'), 10, 3 );
+ add_filter( 'http_response', array($this, 'filter_http_response'), 10, 3 );
+ }
+
+
+ /**
+ * short circuit wp http api with cached file
+ */
+ public function filter_pre_http_request( $response, $args, $url ) {
+ // check if whitelisted
+ if ( !isset( $this->whitelist[$url] ) ) {
+ return $response;
+ }
+ // check if downloading
+ if ( 'GET' !== $args['method'] || empty( $args['filename'] ) ) {
+ return $response;
+ }
+ // check cache and export to designated location
+ $filename = $this->cache->has( $this->whitelist[$url]['key'], $this->whitelist[$url]['ttl'] );
+ if ( $filename ) {
+ WP_CLI::log( sprintf( 'Using cached file \'%s\'...', $filename, $url ) );
+ if ( copy( $filename, $args['filename'] ) ) {
+ // simulate successful download response
+ return array(
+ 'response' => array('code' => ( 200 ), 'message' => 'OK'),
+ 'filename' => $args['filename']
+ );
+ } else {
+ WP_CLI::error( sprintf( 'Error copying cached file %s to %s', $filename, $url ) );
+ }
+ }
+ return $response;
+ }
+
+
+ /**
+ * cache wp http api downloads
+ */
+ public function filter_http_response( $response, $args, $url ) {
+ // check if whitelisted
+ if ( !isset( $this->whitelist[$url] ) ) {
+ return $response;
+ }
+ // check if downloading
+ if ( 'GET' !== $args['method'] || empty( $args['filename'] ) ) {
+ return $response;
+ }
+ // check if download was successful
+ if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ return $response;
+ }
+ // cache downloaded file
+ $this->cache->import( $this->whitelist[$url]['key'], $response['filename']);
+ return $response;
+ }
+
+ /**
+ * whitelist a package url
+ *
+ * @param string $url
+ * @param string $group package group (themes, plugins, ...)
+ * @param string $slug package slug
+ * @param string $version package version
+ * @param int $ttl
+ */
+ public function whitelist_package( $url, $group, $slug, $version, $ttl = null ) {
+ $ext = pathinfo( parse_url( $url, PHP_URL_PATH ), PATHINFO_EXTENSION );
+ $key = "$group/$slug-$version.$ext";
+ $this->whitelist_url( $url, $key, $ttl );
+ wp_update_plugins();
+ }
+
+ /**
+ * whitelist a url
+ *
+ * @param string $url
+ * @param string $key
+ * @param int $ttl
+ */
+ public function whitelist_url( $url, $key = null, $ttl = null ) {
+ $key = $key ? : $url;
+ $this->whitelist[$url] = compact( 'key', 'ttl' );
+ }
+
+ /**
+ * check if url is whitelisted
+ *
+ * @param string $url
+ * @return bool
+ */
+ public function is_whitelisted( $url ) {
+ return isset( $this->whitelist[$url] );
+ }
+}
\ No newline at end of file
use \WP_CLI\Utils;
use \WP_CLI\Dispatcher;
+use \WP_CLI\FileCache;
+use \WP_CLI\WpHttpCacheManager;
/**
* Various utilities for WP-CLI commands.
return $runner;
}
+ /**
+ * @return FileCache
+ */
+ static function get_cache() {
+ static $cache;
+
+ if ( !$cache ) {
+ $home = getenv( 'HOME' ) ? : getenv( 'HOMEDRIVE' ) . '/' . getenv( 'HOMEPATH' );
+ $dir = getenv( 'WP_CLI_CACHE_DIR' ) ? : "$home/.wp-cli/cache";
+ // 6 months, 300mb
+ $cache = new FileCache( $dir, 15552000, 314572800);
+
+ if (0 === mt_rand( 0, 50 ) ) {
+ register_shutdown_function( function () use ( $cache ) {
+ $cache->clean();
+ } );
+ }
+ }
+
+ return $cache;
+ }
+
+ /**
+ * @return WpHttpCacheManager
+ */
+ static function get_http_cache_manager() {
+ static $http_cacher;
+
+ if ( !$http_cacher ) {
+ $http_cacher = new WpHttpCacheManager( self::get_cache() );
+ }
+
+ return $http_cacher;
+ }
+
static function colorize( $string ) {
return \cli\Colors::colorize( $string, self::get_runner()->in_color() );
}
}
WP_CLI::log( sprintf( 'Installing %s (%s)', $api->name, $api->version ) );
+ if ( 'dev' !== $assoc_args['version'] ) {
+ WP_CLI::get_http_cache_manager()->whitelist_package( $api->download_link, $this->item_type, $api->slug, $api->version );
+ }
$result = $this->get_upgrader( $assoc_args )->install( $api->download_link );
return $result;
$items = array();
foreach ( get_plugins() as $file => $details ) {
+ $update_info = $this->get_update_info( $file );
+
$items[ $file ] = array(
'name' => $this->get_name( $file ),
'status' => $this->get_status( $file ),
- 'update' => $this->has_update( $file ),
+ 'update' => (bool) $update_info,
+ 'update_version' => $update_info['new_version'],
+ 'update_package' => $update_info['package'],
'version' => $details['Version'],
'update_id' => $file,
);
}
WP_CLI::log( sprintf( 'Installing %s (%s)', $api->name, $api->version ) );
+ if ( 'dev' !== $assoc_args['version'] ) {
+ WP_CLI::get_http_cache_manager()->whitelist_package( $api->download_link, $this->item_type, $api->slug, $api->version );
+ }
$result = $this->get_upgrader( $assoc_args )->install( $api->download_link );
return $result;
foreach ( wp_get_themes() as $key => $theme ) {
$file = $theme->get_stylesheet_directory();
+ $update_info = $this->get_update_info( $theme->get_stylesheet() );
$items[ $file ] = array(
'name' => $key,
'status' => $this->get_status( $theme ),
- 'update' => $this->has_update( $theme->get_stylesheet() ),
+ 'update' => (bool) $update_info,
+ 'update_version' => $update_info['new_version'],
+ 'update_package' => $update_info['package'],
'version' => $theme->get('Version'),
'update_id' => $theme->get_stylesheet(),
);