diff --git a/.gitignore b/.gitignore index 05b9e7986..d436576ff 100755 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Composer composer.lock .composer -vendor +vendor/ # Sass .sass-cache diff --git a/VERSION b/VERSION deleted file mode 100644 index 2003b639c..000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.9.2 diff --git a/bin/composer.phar b/bin/composer.phar index 1e631a9a6..153fab3b0 100755 Binary files a/bin/composer.phar and b/bin/composer.phar differ diff --git a/bin/gpm b/bin/gpm new file mode 100755 index 000000000..9a5ad34c2 --- /dev/null +++ b/bin/gpm @@ -0,0 +1,42 @@ +#!/usr/bin/env php + $autoload)); +$grav['config']->init(); +$grav['plugins']->init(); +$grav['themes']->init(); + +$app = new Application('Grav Package Manager', GRAV_VERSION); +$app->addCommands(array( + new \Grav\Console\Gpm\IndexCommand(), + new \Grav\Console\Gpm\InfoCommand(), + new \Grav\Console\Gpm\InstallCommand(), + new \Grav\Console\Gpm\UpdateCommand(), + new \Grav\Console\Gpm\SelfupgradeCommand(), +)); +$app->run(); diff --git a/bin/grav b/bin/grav index 55c0fe5a7..2b754f153 100755 --- a/bin/grav +++ b/bin/grav @@ -1,5 +1,6 @@ #!/usr/bin/env php addCommands(array( - new Grav\Console\InstallCommand(), - new Grav\Console\SetupCommand(), - new Grav\Console\CleanCommand(), - new Grav\Console\ClearCacheCommand(), - new Grav\Console\BackupCommand(), - new Grav\Console\NewProjectCommand(), + new Grav\Console\Cli\InstallCommand(), + new Grav\Console\Cli\SandboxCommand(), + new Grav\Console\Cli\CleanCommand(), + new Grav\Console\Cli\ClearCacheCommand(), + new Grav\Console\Cli\BackupCommand(), + new Grav\Console\Cli\NewProjectCommand(), )); $app->run(); diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 0a39fb487..bf30fc261 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -233,6 +233,8 @@ form: options: 1: Yes 0: No + validate: + type: bool assets.css_minify: type: toggle @@ -241,6 +243,8 @@ form: options: 1: Yes 0: No + validate: + type: bool assets.css_minify_windows: type: toggle @@ -249,6 +253,8 @@ form: options: 1: Yes 0: No + validate: + type: bool assets.css_rewrite: type: toggle @@ -257,6 +263,8 @@ form: options: 1: Yes 0: No + validate: + type: bool assets.js_pipeline: type: toggle @@ -265,6 +273,8 @@ form: options: 1: Yes 0: No + validate: + type: bool assets.js_minify: type: toggle @@ -273,6 +283,8 @@ form: options: 1: Yes 0: No + validate: + type: bool debugger: type: section @@ -340,7 +352,7 @@ form: validate: type: bool - debugger.shutdown.close_conection: + debugger.shutdown.close_connection: type: toggle label: Shutdown Close Connection highlight: 1 @@ -349,5 +361,3 @@ form: 0: No validate: type: bool - - diff --git a/system/config/streams.yaml b/system/config/streams.yaml index 7431a502f..e7ac27c5b 100644 --- a/system/config/streams.yaml +++ b/system/config/streams.yaml @@ -7,24 +7,19 @@ schemes: image: type: ReadOnlyStream paths: - - user/images - - user: - type: ReadOnlyStream - paths: - - user + - user://images page: type: ReadOnlyStream paths: - - user/pages + - user://pages account: type: ReadOnlyStream paths: - - user/accounts + - user://accounts data: type: ReadOnlyStream paths: - - user/data + - user://data diff --git a/system/src/Grav/Common/Config/Blueprints.php b/system/src/Grav/Common/Config/Blueprints.php index af8498589..d52a3e138 100644 --- a/system/src/Grav/Common/Config/Blueprints.php +++ b/system/src/Grav/Common/Config/Blueprints.php @@ -1,7 +1,7 @@ files[$key]; } foreach ($files as $name => $item) { - $file = CompiledYaml::instance($item['file']); + $file = CompiledYamlFile::instance($item['file']); $this->blueprints->embed($name, $file->content(), '/'); } } diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php index 448f5f25b..9ee47a24a 100644 --- a/system/src/Grav/Common/Config/Config.php +++ b/system/src/Grav/Common/Config/Config.php @@ -1,7 +1,7 @@ [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user'], + ] + ], 'blueprints' => [ 'type' => 'ReadOnlyStream', 'prefixes' => [ - '' => ['user/blueprints', 'system/blueprints'], + '' => ['user://blueprints', 'system/blueprints'], ] ], 'config' => [ 'type' => 'ReadOnlyStream', 'prefixes' => [ - '' => ['user/config', 'system/config'], + '' => ['user://config', 'system/config'], ] ], 'plugins' => [ 'type' => 'ReadOnlyStream', 'prefixes' => [ - '' => ['user/plugins'], + '' => ['user://plugins'], ] ], 'plugin' => [ 'type' => 'ReadOnlyStream', 'prefixes' => [ - '' => ['user/plugins'], + '' => ['user://plugins'], ] ], 'themes' => [ 'type' => 'ReadOnlyStream', 'prefixes' => [ - '' => ['user/themes'], + '' => ['user://themes'], ] ], 'cache' => [ @@ -101,6 +107,7 @@ class Config extends Data public function reload() { + $this->check(); $this->init(); return $this; @@ -282,7 +289,7 @@ class Config extends Data $files = $this->blueprintFiles[$key]; } foreach ($files as $name => $item) { - $file = CompiledYaml::instance($item['file']); + $file = CompiledYamlFile::instance($item['file']); $this->blueprints->embed($name, $file->content(), '/'); } } @@ -299,7 +306,7 @@ class Config extends Data $files = $this->configFiles[$key]; } foreach ($files as $name => $item) { - $file = CompiledYaml::instance($item['file']); + $file = CompiledYamlFile::instance($item['file']); $this->join($name, $file->content(), '/'); } } diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index 11c1da271..4eb2f8061 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -113,6 +113,7 @@ class Blueprint public function fields() { if (!isset($this->fields)) { + $this->fields = []; $this->embed('', $this->items); } @@ -241,7 +242,7 @@ class Blueprint $this->fields(); $prefix = $name ? strtr($name, $separator, '.') . '.' : ''; $params = array_intersect_key($this->filter, $value); - $this->parseFormFields($value['form']['fields'], $params, $prefix); + $this->parseFormFields($value['form']['fields'], $params, $prefix, $this->fields); } /** @@ -360,12 +361,14 @@ class Blueprint * @param array $fields * @param array $params * @param string $prefix + * @param array $current * @internal */ - protected function parseFormFields(array &$fields, $params, $prefix) + protected function parseFormFields(array &$fields, $params, $prefix, array &$current) { // Go though all the fields in current level. foreach ($fields as $key => &$field) { + $current[$key] = &$field; // Set name from the array key. $field['name'] = $prefix . $key; $field += $params; @@ -373,7 +376,7 @@ class Blueprint if (isset($field['fields'])) { // Recursively get all the nested fields. $newParams = array_intersect_key($this->filter, $field); - $this->parseFormFields($field['fields'], $newParams, $prefix); + $this->parseFormFields($field['fields'], $newParams, $prefix, $current[$key]['fields']); } else { // Add rule. $this->rules[$prefix . $key] = &$field; diff --git a/system/src/Grav/Common/File/CompiledYaml.php b/system/src/Grav/Common/File/CompiledYamlFile.php similarity index 71% rename from system/src/Grav/Common/File/CompiledYaml.php rename to system/src/Grav/Common/File/CompiledYamlFile.php index fcb53ea55..08620a02c 100644 --- a/system/src/Grav/Common/File/CompiledYaml.php +++ b/system/src/Grav/Common/File/CompiledYamlFile.php @@ -3,7 +3,7 @@ namespace Grav\Common\File; use RocketTheme\Toolbox\File\YamlFile; -class CompiledYaml extends YamlFile +class CompiledYamlFile extends YamlFile { use CompiledFile; } diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php index 3dc6b0d03..41164a2b6 100644 --- a/system/src/Grav/Common/Filesystem/Folder.php +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -12,7 +12,7 @@ abstract class Folder /** * Recursively find the last modified time under given path. * - * @param string $path + * @param string $path * @return int */ public static function lastModifiedFolder($path) @@ -29,6 +29,7 @@ abstract class Folder $last_modified = $dir_modified; } } + return $last_modified; } @@ -46,7 +47,7 @@ abstract class Folder /** * Recursively find the last modified time under given path by file. * - * @param string $path + * @param string $path * @return int */ public static function lastModifiedFile($path) @@ -67,15 +68,15 @@ abstract class Folder } } + return $last_modified; } - /** * Return recursive list of all files and directories under given path. * - * @param string $path - * @param array $params + * @param string $path + * @param array $params * @return array * @throws \RuntimeException */ @@ -124,14 +125,15 @@ abstract class Folder $results[] = $filePath; } } + return $results; } /** * Recursively copy directory in filesystem. * - * @param string $source - * @param string $target + * @param string $source + * @param string $target * @throws \RuntimeException */ public static function copy($source, $target) @@ -175,8 +177,8 @@ abstract class Folder /** * Move directory in filesystem. * - * @param string $source - * @param string $target + * @param string $source + * @param string $target * @throws \RuntimeException */ public static function move($source, $target) @@ -204,7 +206,7 @@ abstract class Folder /** * Recursively delete directory from filesystem. * - * @param string $target + * @param string $target * @throws \RuntimeException */ public static function delete($target) @@ -224,6 +226,25 @@ abstract class Folder @touch(dirname($target)); } + /** + * @param string $folder + * @throws \RuntimeException + * @internal + */ + public static function mkdir($folder) + { + if (is_dir($folder)) { + return; + } + + $success = @mkdir($folder, 0777, true); + + if (!$success) { + $error = error_get_last(); + throw new \RuntimeException($error['message']); + } + } + /** * @param string $folder * @return bool @@ -245,34 +266,16 @@ abstract class Folder return @rmdir($folder); } - - /** - * @param string $folder - * @throws \RuntimeException - * @internal - */ - protected static function mkdir($folder) - { - if (is_dir($folder)) { - return; - } - - $success = @mkdir($folder, 0777, true); - - if (!$success) { - $error = error_get_last(); - throw new \RuntimeException($error['message']); - } - } } -class GravRecursiveFilterIterator extends \RecursiveFilterIterator { - +class GravRecursiveFilterIterator extends \RecursiveFilterIterator +{ public static $FILTERS = array( '.', '..', '.DS_Store' ); - public function accept() { + public function accept() + { return !in_array( $this->current()->getFilename(), self::$FILTERS, diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php new file mode 100644 index 000000000..1f72cd03b --- /dev/null +++ b/system/src/Grav/Common/GPM/GPM.php @@ -0,0 +1,364 @@ +installed = new Local\Packages(); + $this->repository = new Remote\Packages($refresh, $callback); + $this->grav = new Remote\Grav($refresh, $callback); + } + + /** + * Returns the Locally installed packages + * @return Iterator The installed packages + */ + public function getInstalled() + { + return $this->installed; + } + + /** + * Returns the amount of locally installed packages + * @return integer Amount of installed packages + */ + public function countInstalled() + { + $installed = $this->getInstalled(); + + return count($installed['plugins']) + count($installed['themes']); + } + + /** + * Return the instance of a specific Plugin + * @param string $slug The slug of the Plugin + * @return Package The instance of the Plugin + */ + public function getInstalledPlugin($slug) + { + return $this->installed['plugins'][$slug]; + } + + /** + * Returns the Locally installed plugins + * @return Iterator The installed plugins + */ + public function getInstalledPlugins() + { + return $this->installed['plugins']; + } + + /** + * Checks if a Plugin is installed + * @param string $slug The slug of the Plugin + * @return boolean True if the Plugin has been installed. False otherwise + */ + public function isPluginInstalled($slug) + { + return isset($this->installed['plugins'][$slug]); + } + + /** + * Return the instance of a specific Theme + * @param string $slug The slug of the Theme + * @return Package The instance of the Theme + */ + public function getInstalledTheme($slug) + { + return $this->installed['themes'][$slug]; + } + + /** + * Returns the Locally installed themes + * @return Iterator The installed themes + */ + public function getInstalledThemes() + { + return $this->installed['themes']; + } + + /** + * Checks if a Theme is installed + * @param string $slug The slug of the Theme + * @return boolean True if the Theme has been installed. False otherwise + */ + public function isThemeInstalled($slug) + { + return isset($this->installed['themes'][$slug]); + } + + /** + * Returns the amount of updates available + * @return integer Amount of available updates + */ + public function countUpdates() + { + $count = 0; + + $count += count($this->getUpdatablePlugins()); + $count += count($this->getUpdatableThemes()); + + return $count; + } + + /** + * Returns an array of Plugins and Themes that can be updated. + * Plugins and Themes are extended with the `available` property that relies to the remote version + * @return array Array of updatable Plugins and Themes. + * Format: ['total' => int, 'plugins' => array, 'themes' => array] + */ + public function getUpdatable() + { + $plugins = $this->getUpdatablePlugins(); + $themes = $this->getUpdatableThemes(); + + $items = [ + 'total' => count($plugins)+count($themes), + 'plugins' => $plugins, + 'themes' => $themes + ]; + + return $items; + } + + /** + * Returns an array of Plugins that can be updated. + * The Plugins are extended with the `available` property that relies to the remote version + * @return Iterator Array of updatable Plugins + */ + public function getUpdatablePlugins() + { + $items = []; + $repository = $this->repository['plugins']; + + // local cache to speed things up + if (isset($this->cache[__METHOD__])) { + return $this->cache[__METHOD__]; + } + + foreach ($this->installed['plugins'] as $slug => $plugin) { + if (!isset($repository[$slug])) { + continue; + } + + $local_version = $plugin->version ? $plugin->version : 'Unknown'; + $remote_version = $repository[$slug]->version; + + if (version_compare($local_version, $remote_version) < 0) { + $repository[$slug]->available = $remote_version; + $repository[$slug]->version = $local_version; + $items[$slug] = $repository[$slug]; + } + } + + $this->cache[__METHOD__] = $items; + + return $items; + } + + /** + * Check if a Plugin or Theme is updatable + * @param string $slug The slug of the package + * @return boolean True if updatable. False otherwise or if not found + */ + public function isUpdatable($slug) + { + return $this->isPluginUpdatable($slug) || $this->isThemeUpdatable($slug); + } + + /** + * Checks if a Plugin is updatable + * @param string $plugin The slug of the Plugin + * @return boolean True if the Plugin is updatable. False otherwise + */ + public function isPluginUpdatable($plugin) + { + return array_key_exists($plugin, $this->getUpdatablePlugins()); + } + + /** + * Returns an array of Themes that can be updated. + * The Themes are extended with the `available` property that relies to the remote version + * @return Iterator Array of updatable Themes + */ + public function getUpdatableThemes() + { + $items = []; + $repository = $this->repository['themes']; + + // local cache to speed things up + if (isset($this->cache[__METHOD__])) { + return $this->cache[__METHOD__]; + } + + foreach ($this->installed['themes'] as $slug => $plugin) { + if (!isset($repository[$slug])) { + continue; + } + + $local_version = $plugin->version ? $plugin->version : 'Unknown'; + $remote_version = $repository[$slug]->version; + + if (version_compare($local_version, $remote_version) < 0) { + $repository[$slug]->available = $remote_version; + $repository[$slug]->version = $local_version; + $items[$slug] = $repository[$slug]; + } + } + + $this->cache[__METHOD__] = $items; + + return $items; + } + + /** + * Checks if a Theme is Updatable + * @param string $theme The slug of the Theme + * @return boolean True if the Theme is updatable. False otherwise + */ + public function isThemeUpdatable($theme) + { + return array_key_exists($theme, $this->getUpdatableThemes()); + } + + /** + * Returns a Plugin from the repository + * @param string $slug The slug of the Plugin + * @return mixed Package if found, NULL if not + */ + public function getRepositoryPlugin($slug) + { + return @$this->repository['plugins'][$slug]; + } + + /** + * Returns the list of Plugins available in the repository + * @return Iterator The Plugins remotely available + */ + public function getRepositoryPlugins() + { + return $this->repository['plugins']; + } + + /** + * Returns a Theme from the repository + * @param string $slug The slug of the Theme + * @return mixed Package if found, NULL if not + */ + public function getRepositoryTheme($slug) + { + return @$this->repository['themes'][$slug]; + } + + /** + * Returns the list of Themes available in the repository + * @return Iterator The Themes remotely available + */ + public function getRepositoryThemes() + { + return $this->repository['themes']; + } + + /** + * Returns the list of Plugins and Themes available in the repository + * @return array Array of available Plugins and Themes + * Format: ['plugins' => array, 'themes' => array] + */ + public function getRepository() + { + return $this->repository; + } + + /** + * Searches for a Package in the repository + * @param string $search Can be either the slug or the name + * @return Package Package if found, FALSE if not + */ + public function findPackage($search) + { + $search = strtolower($search); + if ($found = $this->getRepositoryTheme($search)) { + return $found; + } + + if ($found = $this->getRepositoryPlugin($search)) { + return $found; + } + + foreach ($this->getRepositoryThemes() as $slug => $theme) { + if ($search == $slug || $search == $theme->name) { + return $theme; + } + } + + foreach ($this->getRepositoryPlugins() as $slug => $plugin) { + if ($search == $slug || $search == $plugin->name) { + return $plugin; + } + } + + return false; + } + + /** + * Returns the list of Plugins and Themes available in the repository + * @return array Array of available Plugins and Themes + * Format: ['plugins' => array, 'themes' => array] + */ + /** + * Searches for a list of Packages in the repository + * @param array $searches An array of either slugs or names + * @return array Array of found Packages + * Format: ['total' => int, 'not_found' => array, ] + */ + public function findPackages($searches = []) + { + $packages = ['total' => 0, 'not_found' => []]; + + foreach ($searches as $search) { + if ($found = $this->findPackage($search)) { + if (!isset($packages[$found->package_type])) { + $packages[$found->package_type] = []; + } + + $packages[$found->package_type][$found->slug] = $found; + $packages['total']++; + } else { + $packages['not_found'][] = $search; + } + } + + return $packages; + } +} diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php new file mode 100644 index 000000000..19ea58158 --- /dev/null +++ b/system/src/Grav/Common/GPM/Installer.php @@ -0,0 +1,269 @@ + true, + 'ignore_symlinks' => true, + 'sophisticated' => false, + 'install_path' => '', + 'exclude_checks' => [self::EXISTS, self::NOT_FOUND, self::IS_LINK] + ]; + + /** + * Installs a given package to a given destination. + * + * @param string $package The local path to the ZIP package + * @param string $destination The local path to the Grav Instance + * @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter'] + * + * @return boolean True if everything went fine, False otherwise. + */ + public static function install($package, $destination, $options = []) + { + $destination = rtrim($destination, DS); + $options = array_merge(self::$options, $options); + $install_path = rtrim($destination . DS . ltrim($options['install_path'], DS), DS); + + if (!self::isGravInstance($destination) || !self::isValidDestination($install_path, $options['exclude_checks']) + ) { + return false; + } + + if ( + self::lastErrorCode() == self::IS_LINK && $options['ignore_symlinks'] || + self::lastErrorCode() == self::EXISTS && !$options['overwrite'] + ) { + return false; + } + + $zip = new \ZipArchive(); + $archive = $zip->open($package); + $tmp = sys_get_temp_dir() . DS . 'Grav-' . uniqid(); + + if ($archive !== true) { + self::$error = self::ZIP_OPEN_ERROR; + + return false; + } + + Folder::mkdir($tmp); + + $unzip = $zip->extractTo($tmp); + + if (!$unzip) { + self::$error = self::ZIP_EXTRACT_ERROR; + + $zip->close(); + Folder::delete($tmp); + + return false; + } + + + if (!$options['sophisticated']) { + self::nonSophisticatedInstall($zip, $install_path, $tmp); + } else { + self::sophisticatedInstall($zip, $install_path, $tmp); + } + + Folder::delete($tmp); + $zip->close(); + + self::$error = self::OK; + + return true; + + } + + public static function nonSophisticatedInstall($zip, $install_path, $tmp) + { + $container = $zip->getNameIndex(0); // TODO: better way of determining if zip has container folder + if (file_exists($install_path)) { + Folder::delete($install_path); + } + + Folder::move($tmp . DS . $container, $install_path); + + return true; + } + + public static function sophisticatedInstall($zip, $install_path, $tmp) + { + for ($i = 0, $l = $zip->numFiles; $i < $l; $i++) { + $filename = $zip->getNameIndex($i); + $fileinfo = pathinfo($filename); + $depth = count(explode(DS, rtrim($filename, '/'))); + + if ($depth > 2) { + continue; + } + + $path = $install_path . DS . $fileinfo['basename']; + + if (is_link($path)) { + continue; + } else { + if (is_dir($path)) { + Folder::delete($path); + Folder::move($tmp . DS . $filename, $path); + } else { + if (is_file($path)) { + @unlink($path); + @copy($tmp . DS . $filename, $path); + } + } + } + } + + return true; + } + + /** + * Runs a set of checks on the destination and sets the Error if any + * + * @param string $destination The directory to run validations at + * @param array $exclude An array of constants to exclude from the validation + * + * @return boolean True if validation passed. False otherwise + */ + public static function isValidDestination($destination, $exclude = []) + { + self::$error = 0; + self::$target = $destination; + + if (is_link($destination)) { + self::$error = self::IS_LINK; + } elseif (file_exists($destination)) { + self::$error = self::EXISTS; + } elseif (!file_exists($destination)) { + self::$error = self::NOT_FOUND; + } elseif (!is_dir($destination)) { + self::$error = self::NOT_DIRECTORY; + } + + if (count($exclude) && in_array(self::$error, $exclude)) { + return true; + } + + return !(self::$error); + } + + /** + * Validates if the given path is a Grav Instance + * + * @param string $target The local path to the Grav Instance + * + * @return boolean True if is a Grav Instance. False otherwise + */ + public static function isGravInstance($target) + { + self::$error = 0; + self::$target = $target; + + if ( + !file_exists($target . DS . 'index.php') || + !file_exists($target . DS . 'bin') || + !file_exists($target . DS . 'user') || + !file_exists($target . DS . 'system' . DS . 'config' . DS . 'system.yaml') + ) { + self::$error = self::NOT_GRAV_ROOT; + } + + return !self::$error; + } + + /** + * Returns the last error occurred in a string message format + * @return string The message of the last error + */ + public static function lastErrorMsg() + { + $msg = 'Unknown Error'; + + switch (self::$error) { + case 0: + $msg = 'No Error'; + break; + + case self::EXISTS: + $msg = 'The target path "' . self::$target . '" already exists'; + break; + + case self::IS_LINK: + $msg = 'The target path "' . self::$target . '" is a symbolic link'; + break; + + case self::NOT_FOUND: + $msg = 'The target path "' . self::$target . '" does not appear to exist'; + break; + + case self::NOT_DIRECTORY: + $msg = 'The target path "' . self::$target . '" does not appear to be a folder'; + break; + + case self::NOT_GRAV_ROOT: + $msg = 'The target path "' . self::$target . '" does not appear to be a Grav instance'; + break; + + case self::ZIP_OPEN_ERROR: + $msg = 'Unable to open the package file'; + break; + + case self::ZIP_EXTRACT_ERROR: + $msg = 'An error occurred while extracting the package'; + break; + + default: + return 'Unknown error'; + break; + } + + return $msg; + } + + /** + * Returns the last error code of the occurred error + * @return integer The code of the last error + */ + public static function lastErrorCode() + { + return self::$error; + } +} diff --git a/system/src/Grav/Common/GPM/Local/Collection.php b/system/src/Grav/Common/GPM/Local/Collection.php new file mode 100644 index 000000000..9e3cf3638 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Collection.php @@ -0,0 +1,32 @@ +items as $name => $theme) { + $items[$name] = $theme->toArray(); + } + + return json_encode($items); + } + + public function toArray() + { + $items = []; + + foreach ($this->items as $name => $theme) { + $items[$name] = $theme->toArray(); + } + + return $items; + } +} diff --git a/system/src/Grav/Common/GPM/Local/Package.php b/system/src/Grav/Common/GPM/Local/Package.php new file mode 100644 index 000000000..062b73755 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Package.php @@ -0,0 +1,86 @@ +data = $package; + $this->blueprints = $this->data->blueprints(); + + if ($package_type) { + $html_description = \Parsedown::instance()->line($this->blueprints->get('description')); + $this->blueprints->set('package_type', $package_type); + $this->blueprints->set('description_html', $html_description); + $this->blueprints->set('description_plain', strip_tags($html_description)); + } + } + + /** + * @return mixed + */ + public function isEnabled() + { + return $this->data['enabled']; + } + + /** + * @return Data + */ + public function getData() + { + return $this->data; + } + + /** + * @param $key + * @return mixed + */ + public function __get($key) + { + return $this->blueprints->get($key); + } + + /** + * @return string + */ + public function __toString() + { + return $this->toJson(); + } + + /** + * @return string + */ + public function toJson() + { + return $this->blueprints->toJson(); + } + + /** + * @return array + */ + public function toArray() + { + return $this->blueprints->toArray(); + } +} diff --git a/system/src/Grav/Common/GPM/Local/Packages.php b/system/src/Grav/Common/GPM/Local/Packages.php new file mode 100644 index 000000000..4498e6895 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Packages.php @@ -0,0 +1,28 @@ + new Plugins(), + 'themes' => new Themes() + ]; + } + + $this->plugins = self::$cache[__METHOD__]['plugins']; + $this->themes = self::$cache[__METHOD__]['themes']; + + $this->append(['plugins' => $this->plugins]); + $this->append(['themes' => $this->themes]); + } +} diff --git a/system/src/Grav/Common/GPM/Local/Plugins.php b/system/src/Grav/Common/GPM/Local/Plugins.php new file mode 100644 index 000000000..b5193ad86 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Plugins.php @@ -0,0 +1,26 @@ +all() as $name => $data) { + $this->items[$name] = new Package($data, $this->type); + } + } +} diff --git a/system/src/Grav/Common/GPM/Local/Themes.php b/system/src/Grav/Common/GPM/Local/Themes.php new file mode 100644 index 000000000..19d68c6a6 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Themes.php @@ -0,0 +1,15 @@ +all() as $name => $data) { + $this->items[$name] = new Package($data, $this->type); + } + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Collection.php b/system/src/Grav/Common/GPM/Remote/Collection.php new file mode 100644 index 000000000..db9a9c7ee --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Collection.php @@ -0,0 +1,64 @@ +repository = $repository; + $this->raw = self::$grav['cache']->fetch(md5($this->repository)); + } + + public function toJson() { + $items = []; + + foreach ($this->items as $name => $theme) { + $items[$name] = $theme->toArray(); + } + + return json_encode($items); + } + + public function toArray() { + $items = []; + + foreach ($this->items as $name => $theme) { + $items[$name] = $theme->toArray(); + } + + return $items; + } + + public function fetch($refresh = false, $callback = null) { + if (!$this->raw || $refresh) { + $response = Response::get($this->repository, [], $callback); + $this->raw = $response; + self::$grav['cache']->save(md5($this->repository), $this->raw, $this->lifetime); + } + + return $this->raw; + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Grav.php b/system/src/Grav/Common/GPM/Remote/Grav.php new file mode 100644 index 000000000..c83611dbc --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Grav.php @@ -0,0 +1,60 @@ +repository); + + $this->fetch($refresh, $callback); + $this->data = json_decode($this->raw); + + $this->version = @$this->data->version ?: '-'; + $this->date = @$this->data->date ?: '-'; + + $this->data = $this->data->assets; + + foreach ($this->data as $slug => $data) { + $this->items[$slug] = new Package($data); + } + } + + /** + * Returns the list of assets associated to the latest version of Grav + * @return array list of assets + */ + public function getAssets() + { + return $this->data; + } + + /** + * Returns the latest version of Grav available remotely + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Return the release date of the latest Grav + * @return string + */ + public function getDate() + { + return $this->date; + } + + public function isUpdatable() + { + return version_compare(GRAV_VERSION, $this->getVersion(), '<'); + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Package.php b/system/src/Grav/Common/GPM/Remote/Package.php new file mode 100644 index 000000000..45da29478 --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Package.php @@ -0,0 +1,32 @@ +data = $package; + if ($package_type) { + $this->data->package_type = $package_type; + } + } + + public function getData() { + return $this->data; + } + + public function __get($key) { + return $this->data->$key; + } + + public function __toString() { + return $this->toJson(); + } + + public function toJson() { + return json_encode($this->data); + } + + public function toArray() { + return $this->data; + } + +} diff --git a/system/src/Grav/Common/GPM/Remote/Packages.php b/system/src/Grav/Common/GPM/Remote/Packages.php new file mode 100644 index 000000000..f78e9c67e --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Packages.php @@ -0,0 +1,28 @@ + new Plugins($refresh, $callback), + 'themes' => new Themes($refresh, $callback) + ]; + } + + $this->plugins = self::$cache[__METHOD__]['plugins']->toArray(); + $this->themes = self::$cache[__METHOD__]['themes']->toArray(); + + $this->append(['plugins' => $this->plugins]); + $this->append(['themes' => $this->themes]); + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Plugins.php b/system/src/Grav/Common/GPM/Remote/Plugins.php new file mode 100644 index 000000000..fa71fbab6 --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Plugins.php @@ -0,0 +1,21 @@ +repository); + + $this->fetch($refresh, $callback); + $this->data = json_decode($this->raw); + + foreach ($this->data as $slug => $data) { + $this->items[$slug] = new Package($data, $this->type); + } + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Themes.php b/system/src/Grav/Common/GPM/Remote/Themes.php new file mode 100644 index 000000000..fcb5a34c4 --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Themes.php @@ -0,0 +1,21 @@ +repository); + + $this->fetch($refresh, $callback); + $this->data = json_decode($this->raw); + + foreach ($this->data as $slug => $data) { + $this->items[$slug] = new Package($data, $this->type); + } + } +} diff --git a/system/src/Grav/Common/GPM/Response.php b/system/src/Grav/Common/GPM/Response.php new file mode 100644 index 000000000..211b2a415 --- /dev/null +++ b/system/src/Grav/Common/GPM/Response.php @@ -0,0 +1,220 @@ + [ + CURLOPT_REFERER => 'Grav GPM', + CURLOPT_USERAGENT => 'Grav GPM', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_HEADER => false, + /** + * Example of callback parameters from within your own class + */ + //CURLOPT_NOPROGRESS => false, + //CURLOPT_PROGRESSFUNCTION => [$this, 'progress'] + ], + 'fopen' => [ + 'method' => 'GET', + 'user_agent' => 'Grav GPM', + 'max_redirects' => 5, + 'follow_location' => 1, + 'timeout' => 15, + /** + * Example of callback parameters from within your own class + */ + //'notification' => [$this, 'progress'] + ] + ]; + + /** + * Sets the preferred method to use for making HTTP calls. + * @param string $method Default is `auto` + */ + public static function setMethod($method = 'auto') + { + if (!in_array($method, ['auto', 'curl', 'fopen'])) { + $method = 'auto'; + } + + self::$method = $method; + + return new self(); + } + + /** + * Makes a request to the URL by using the preferred method + * @param string $uri URL to call + * @param array $options An array of parameters for both `curl` and `fopen` + * @return string The response of the request + */ + public static function get($uri = '', $options = [], $callback = null) + { + if (!self::isCurlAvailable() && !self::isFopenAvailable()) { + throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available'); + } + + $options = array_replace_recursive(self::$defaults, $options); + $method = 'get' . ucfirst(strtolower(self::$method)); + + self::$callback = $callback; + + return static::$method($uri, $options, $callback); + } + + /** + * Progress normalized for cURL and Fopen + * @param args Variable length of arguments passed in by stream method + * @return array Normalized array with useful data. + * Format: ['code' => int|false, 'filesize' => bytes, 'transferred' => bytes, 'percent' => int] + */ + public static function progress() + { + static $filesize = null; + + $args = func_get_args(); + $isCurlResource = is_resource($args[0]) && get_resource_type($args[0]) == 'curl'; + + $notification_code = !$isCurlResource ? $args[0] : false; + $bytes_transferred = $isCurlResource ? $args[2] : $args[4]; + + if ($isCurlResource) { + $filesize = $args[1]; + } elseif ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) { + $filesize = $args[5]; + } + + if ($bytes_transferred > 0) { + if ($notification_code == STREAM_NOTIFY_PROGRESS|STREAM_NOTIFY_COMPLETED || $isCurlResource) { + + $progress = [ + 'code' => $notification_code, + 'filesize' => $filesize, + 'transferred' => $bytes_transferred, + 'percent' => $filesize <= 0 ? '-' : round(($bytes_transferred * 100) / $filesize, 1) + ]; + + if (self::$callback !== null) { + call_user_func_array(self::$callback, [$progress]); + } + } + } + } + + /** + * Checks if cURL is available + * @return boolean + */ + public static function isCurlAvailable() + { + return function_exists('curl_version'); + } + + /** + * Checks if the remote fopen request is enabled in PHP + * @return boolean + */ + public static function isFopenAvailable() + { + return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen')); + } + + /** + * Automatically picks the preferred method + * @return string The response of the request + */ + private static function getAuto() + { + if (self::isFopenAvailable()) { + return self::getFopen(func_get_args()); + } + + if (self::isCurlAvailable()) { + return self::getCurl(func_get_args()); + } + } + + /** + * Starts a HTTP request via cURL + * @return string The response of the request + */ + private static function getCurl() + { + $args = func_get_args(); + $uri = $args[0]; + $options = $args[1]; + $callback = $args[2]; + + $ch = curl_init($uri); + curl_setopt_array($ch, $options['curl']); + + if ($callback) { + curl_setopt_array( + $ch, + [ + CURLOPT_NOPROGRESS => false, + CURLOPT_PROGRESSFUNCTION => ['self', 'progress'] + ] + ); + } + + $response = curl_exec($ch); + + if ($errno = curl_errno($ch)) { + $error_message = curl_strerror($errno); + throw new \RuntimeException("cURL error ({$errno}):\n {$error_message}"); + } + + curl_close($ch); + + return $response; + } + + /** + * Starts a HTTP request via fopen + * @return string The response of the request + */ + private static function getFopen() + { + if (count($args = func_get_args()) == 1) { + $args = $args[0]; + } + + $uri = $args[0]; + $options = $args[1]; + $callback = $args[2]; + + if ($callback) { + $options['fopen']['notification'] = ['self', 'progress']; + } + + $stream = stream_context_create(['http' => $options['fopen']], $options['fopen']); + $content = @file_get_contents($uri, false, $stream); + + if ($content === false) { + throw new \RuntimeException("Error while trying to download '$uri'"); + } + + return $content; + } +} diff --git a/system/src/Grav/Common/GPM/Upgrader.php b/system/src/Grav/Common/GPM/Upgrader.php new file mode 100644 index 000000000..312e77bb2 --- /dev/null +++ b/system/src/Grav/Common/GPM/Upgrader.php @@ -0,0 +1,75 @@ +remote = new Remote\Grav($refresh, $callback); + } + + /** + * Returns the release date of the latest version of Grav + * @return string + */ + public function getReleaseDate() + { + return $this->remote->getDate(); + } + + /** + * Returns the version of the installed Grav + * @return string + */ + public function getLocalVersion() + { + return GRAV_VERSION; + } + + /** + * Returns the version of the remotely available Grav + * @return string + */ + public function getRemoteVersion() + { + return $this->remote->getVersion(); + } + + /** + * Returns an array of assets available to download remotely + * @return array + */ + public function getAssets() + { + return $this->remote->getAssets(); + } + + /** + * Checks if the currently installed Grav is upgradable to a newer version + * @return boolean True if it's upgradable, False otherwise. + */ + public function isUpgradable() + { + return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), "<"); + } +} diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index be97c00d1..341a54fda 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -100,8 +100,9 @@ class Grav extends Container $page = $c['pages']->dispatch($path_parts['dirname']); if ($page) { $media = $page->media()->all(); - if (isset($media[$path_parts['basename']])) { - $medium = $media[$path_parts['basename']]; + $media_file = urldecode($path_parts['basename']); + if (isset($media[$media_file])) { + $medium = $media[$media_file]; // loop through actions for the image and call them foreach ($c['uri']->query(null,true) as $action => $params) { diff --git a/system/src/Grav/Common/Iterator.php b/system/src/Grav/Common/Iterator.php index e213ab723..906a9be40 100644 --- a/system/src/Grav/Common/Iterator.php +++ b/system/src/Grav/Common/Iterator.php @@ -1,5 +1,6 @@ items[$key]; } diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 74b17778e..944d1cefd 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -1,7 +1,7 @@ get('system.pages.events.page')) { $this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page])); } - } else { - $date = $file->getMTime(); - if ($date > $last_modified) { - $last_modified = $date; - } } + // Update the last modified if it's newer than already found + $date = $file->getMTime(); + if ($date > $last_modified) { + $last_modified = $date; + } + + } // Override the modified and ID so that it takes the latest change into account diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php index ef3b968d8..59563f722 100644 --- a/system/src/Grav/Common/Themes.php +++ b/system/src/Grav/Common/Themes.php @@ -2,7 +2,7 @@ namespace Grav\Common; use Grav\Common\Config\Config; -use Grav\Common\File\CompiledYaml; +use Grav\Common\File\CompiledYamlFile; use Grav\Common\Data\Blueprints; use Grav\Common\Data\Data; use RocketTheme\Toolbox\Event\EventDispatcher; @@ -74,7 +74,7 @@ class Themes extends Iterator /** * Get theme configuration or throw exception if it cannot be found. * - * @param string $name + * @param string $name * @return Data * @throws \RuntimeException */ @@ -96,11 +96,11 @@ class Themes extends Iterator } // Load default configuration. - $file = CompiledYaml::instance("themes://{$name}/{$name}" . YAML_EXT); + $file = CompiledYamlFile::instance("themes://{$name}/{$name}" . YAML_EXT); $obj = new Data($file->content(), $blueprint); // Override with user configuration. - $file = CompiledYaml::instance("user://config/themes/{$name}" . YAML_EXT); + $file = CompiledYamlFile::instance("user://config/themes/{$name}" . YAML_EXT); $obj->merge($file->content()); // Save configuration always to user/config. @@ -146,7 +146,7 @@ class Themes extends Iterator $class = new $className($grav, $config, $name); } } - } elseif (!$locator('theme://')) { + } elseif (!$locator('theme://') && !defined('GRAV_CLI')) { exit("Theme '$name' does not exist, unable to display page."); } @@ -162,7 +162,8 @@ class Themes extends Iterator * * @throws \InvalidArgumentException */ - public function configure() { + public function configure() + { $name = $this->current(); $config = $this->config; @@ -204,7 +205,7 @@ class Themes extends Iterator protected function loadConfiguration($name, Config $config) { - $themeConfig = CompiledYaml::instance("themes://{$name}/{$name}" . YAML_EXT)->content(); + $themeConfig = CompiledYamlFile::instance("themes://{$name}/{$name}" . YAML_EXT)->content(); $config->merge(['themes' => [$name => $themeConfig]]); } diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index 9cddd4c55..301363d63 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -28,7 +28,10 @@ class Uri { $base = 'http://'; - $uri = $_SERVER['REQUEST_URI']; + $name = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost'; + $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80; + $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + $root_path = rtrim(substr($_SERVER['PHP_SELF'], 0, strpos($_SERVER['PHP_SELF'], 'index.php')), '/'); @@ -36,15 +39,15 @@ class Uri $base = (@$_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://'; } - $base .= $_SERVER['SERVER_NAME']; + $base .= $name; - if ($_SERVER['SERVER_PORT'] != '80' && $_SERVER['SERVER_PORT'] != '443') { - $base .= ":".$_SERVER['SERVER_PORT']; + if ($port != '80' && $port != '443') { + $base .= ":".$port; } // check if userdir in the path and workaround PHP bug with PHP_SELF - if (strpos($_SERVER['REQUEST_URI'], '/~') !== false && strpos($_SERVER['PHP_SELF'], '/~') === false) { - $root_path = substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '/', 1)) . $root_path; + if (strpos($uri, '/~') !== false && strpos($_SERVER['PHP_SELF'], '/~') === false) { + $root_path = substr($uri, 0, strpos($uri, '/', 1)) . $root_path; } $this->base = $base; @@ -130,7 +133,7 @@ class Uri */ public function route($absolute = false, $domain = false) { - return ($absolute ? $this->rootUrl($domain) : '') . '/' . implode('/', $this->paths); + return urldecode(($absolute ? $this->rootUrl($domain) : '') . '/' . implode('/', $this->paths)); } /** diff --git a/system/src/Grav/Console/BackupCommand.php b/system/src/Grav/Console/Cli/BackupCommand.php similarity index 97% rename from system/src/Grav/Console/BackupCommand.php rename to system/src/Grav/Console/Cli/BackupCommand.php index 2d518ed86..d6bb7757d 100644 --- a/system/src/Grav/Console/BackupCommand.php +++ b/system/src/Grav/Console/Cli/BackupCommand.php @@ -1,5 +1,5 @@ setDescription("Creates a backup of the Grav instance") - ->setHelp('The backup creates a zipped backup'); + ->setHelp('The backup creates a zipped backup. Optionally can be saved in a different destination.'); $this->source = getcwd(); diff --git a/system/src/Grav/Console/CleanCommand.php b/system/src/Grav/Console/Cli/CleanCommand.php similarity index 99% rename from system/src/Grav/Console/CleanCommand.php rename to system/src/Grav/Console/Cli/CleanCommand.php index a191b8f64..bab37ec33 100644 --- a/system/src/Grav/Console/CleanCommand.php +++ b/system/src/Grav/Console/Cli/CleanCommand.php @@ -1,5 +1,5 @@ setDescription("Handles cloning and symlinking for Grav") - ->setHelp('The install provides clone and symlink installation chores'); + ->setDescription("Installs the dependencies needed by Grav. Optionally can create symbolic links") + ->setHelp('The install command installs the dependencies needed by Grav. Optionally can create symbolic links'); } protected function execute(InputInterface $input, OutputInterface $output) @@ -54,8 +54,7 @@ class InstallCommand extends Command { if (file_exists($local_config_file)) { $this->local_config = Yaml::parse($local_config_file); - $output->writeln(''); - $output->writeln('read local config from ' . $local_config_file . ''); + $output->writeln('Read local config from ' . $local_config_file . ''); } // Look for dependencies file in ROOT and USER dir @@ -67,6 +66,10 @@ class InstallCommand extends Command { $output->writeln('ERROR Missing .dependencies file in user/ folder'); } + // Updates composer first + $output->writeln("\nInstalling vendor dependencies"); + $output->writeln(system('php bin/composer.phar --working-dir="'.$this->destination.'" --no-interaction update')); + // If yaml config, process if ($this->config) { if (!$input->getOption('symlink')) { @@ -114,7 +117,7 @@ class InstallCommand extends Command { if (!$this->local_config) { $output->writeln('No local configuration available, aborting...'); $output->writeln(''); - exit; + return; } exec('cd ' . $this->destination); diff --git a/system/src/Grav/Console/NewProjectCommand.php b/system/src/Grav/Console/Cli/NewProjectCommand.php similarity index 77% rename from system/src/Grav/Console/NewProjectCommand.php rename to system/src/Grav/Console/Cli/NewProjectCommand.php index 4518943e6..7d49704a3 100644 --- a/system/src/Grav/Console/NewProjectCommand.php +++ b/system/src/Grav/Console/Cli/NewProjectCommand.php @@ -1,5 +1,5 @@ setDescription("Creates a new Grav project with all the dependencies included") - ->setHelp('The new command provides clone and symlink installation chores'); + ->setDescription("Creates a new Grav project with all the dependencies installed") + ->setHelp("The new-project command is a combination of the `setup` and `install` commands.\nCreates a new Grav instance and performs the installation of all the required dependencies."); } protected function execute(InputInterface $input, OutputInterface $output) { - $setupCommand = $this->getApplication()->find('setup'); + $sandboxCommand = $this->getApplication()->find('sandbox'); $installCommand = $this->getApplication()->find('install'); - $setupArguments = new ArrayInput(array( - 'command' => 'setup', + $sandboxArguments = new ArrayInput(array( + 'command' => 'sandbox', 'destination' => $input->getArgument('destination'), '-s' => $input->getOption('symlink') )); @@ -48,7 +48,7 @@ class NewProjectCommand extends Command { '-s' => $input->getOption('symlink') )); - $setupCommand->run($setupArguments, $output); + $sandboxCommand->run($sandboxArguments, $output); $installCommand->run($installArguments, $output); } diff --git a/system/src/Grav/Console/SetupCommand.php b/system/src/Grav/Console/Cli/SandboxCommand.php similarity index 96% rename from system/src/Grav/Console/SetupCommand.php rename to system/src/Grav/Console/Cli/SandboxCommand.php index 84c4fe887..52d97c2e3 100644 --- a/system/src/Grav/Console/SetupCommand.php +++ b/system/src/Grav/Console/Cli/SandboxCommand.php @@ -1,5 +1,5 @@ setName('setup') - ->setDescription('Setup of a base Grav system in your webroot') + ->setName('sandbox') + ->setDescription('Setup of a base Grav system in your webroot, good for development, playing around or starting fresh') ->addArgument( 'destination', InputArgument::REQUIRED, @@ -55,10 +55,7 @@ class SetupCommand extends Command InputOption::VALUE_NONE, 'Symlink the base grav system' ) - ->setHelp(<<setup command help create a development environment that uses symbolic links to link the core of grav to the git cloned repository -EOT - ); + ->setHelp("The sandbox command help create a development environment that can optionally use symbolic links to link the core of grav to the git cloned repository.\nGood for development, playing around or starting fresh"); $this->source = getcwd(); } diff --git a/system/src/Grav/Console/ConsoleTrait.php b/system/src/Grav/Console/ConsoleTrait.php new file mode 100644 index 000000000..3fc01c7f0 --- /dev/null +++ b/system/src/Grav/Console/ConsoleTrait.php @@ -0,0 +1,64 @@ +set('system.cache.driver', 'default'); + } + + $this->argv = $_SERVER['argv'][0]; + + $this->input = $input; + $this->output = $output; + + $this->output->getFormatter()->setStyle('normal', new OutputFormatterStyle('white')); + $this->output->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow', null, array('bold'))); + $this->output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold'))); + $this->output->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan', null, array('bold'))); + $this->output->getFormatter()->setStyle('green', new OutputFormatterStyle('green', null, array('bold'))); + $this->output->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta', null, array('bold'))); + $this->output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold'))); + } + + private function isGravInstance($path) + { + if (!file_exists($path)) { + $this->output->writeln(''); + $this->output->writeln("ERROR: Destination doesn't exist:"); + $this->output->writeln(" $path"); + $this->output->writeln(''); + exit; + } + + if (!is_dir($path)) { + $this->output->writeln(''); + $this->output->writeln("ERROR: Destination chosen to install is not a directory:"); + $this->output->writeln(" $path"); + $this->output->writeln(''); + exit; + } + + if (!file_exists($path . DS . 'index.php') || !file_exists($path . DS . '.dependencies') || !file_exists($path . DS . 'system' . DS . 'config' . DS . 'system.yaml')) { + $this->output->writeln(''); + $this->output->writeln("ERROR: Destination chosen to install does not appear to be a Grav instance:"); + $this->output->writeln(" $path"); + $this->output->writeln(''); + exit; + } + } +} diff --git a/system/src/Grav/Console/Gpm/IndexCommand.php b/system/src/Grav/Console/Gpm/IndexCommand.php new file mode 100644 index 000000000..7f8b9a24a --- /dev/null +++ b/system/src/Grav/Console/Gpm/IndexCommand.php @@ -0,0 +1,93 @@ +setName("index") + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->setDescription("Lists the plugins and themes available for installation") + ->setHelp('The index command lists the plugins and themes available for installation'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->setupConsole($input, $output); + + $this->gpm = new GPM($this->input->getOption('force')); + + $this->data = $this->gpm->getRepository(); + + $this->output->writeln(''); + + foreach ($this->data as $type => $packages) { + $this->output->writeln("" . ucfirst($type) . " [ " . count($packages) . " ]"); + + $index = 0; + foreach ($packages as $slug => $package) { + $this->output->writeln( + // index + str_pad($index+++1, 2, '0', STR_PAD_LEFT) . ". " . + // package name + "" . str_pad($package->name, 15) . " " . + // slug + "[" . str_pad($slug, 15, ' ', STR_PAD_BOTH) . "] " . + // version details + $this->versionDetails($package) + ); + } + + $this->output->writeln(''); + } + + $this->output->writeln('You can either get more informations about a package by typing:'); + $this->output->writeln(' ' . $this->argv . ' info '); + $this->output->writeln(''); + $this->output->writeln('Or you can install a package by typing:'); + $this->output->writeln(' ' . $this->argv . ' install '); + $this->output->writeln(''); + } + + private function versionDetails($package) + { + $list = $this->gpm->{'getUpdatable' . ucfirst($package->package_type)}(); + $package = isset($list[$package->slug]) ? $list[$package->slug] : $package; + $type = ucfirst(preg_replace("/s$/", '', $package->package_type)); + $updatable = $this->gpm->{'is' . $type . 'Updatable'}($package->slug); + $installed = $this->gpm->{'is' . $type . 'Installed'}($package->slug); + $local = $this->gpm->{'getInstalled' . $type}($package->slug); + + if (!$installed || !$updatable) { + $version = $installed ? $local->version : $package->version; + $installed = !$installed ? ' (not installed)' : ' (installed)'; + + return str_pad(" [v" . $version . "]", 35) . $installed; + } + + if ($updatable) { + $installed = !$installed ? ' (not installed)' : ' (installed)'; + + return str_pad(" [v" . $package->version . " v" . $package->available . "]", 61) . $installed; + } + + } +} diff --git a/system/src/Grav/Console/Gpm/InfoCommand.php b/system/src/Grav/Console/Gpm/InfoCommand.php new file mode 100644 index 000000000..891ef1f01 --- /dev/null +++ b/system/src/Grav/Console/Gpm/InfoCommand.php @@ -0,0 +1,93 @@ +setName("info") + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force fetching the new data remotely' + ) + ->addArgument( + 'package', + InputArgument::REQUIRED, + 'The package of which more informations are desired. Use the "index" command for a list of packages' + ) + ->setDescription("Shows more informations about a package") + ->setHelp('The info shows more informations about a package'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->setupConsole($input, $output); + $this->gpm = new GPM($this->input->getOption('force')); + + $foundPackage = $this->gpm->findPackage($input->getArgument('package')); + + if (!$foundPackage) { + $this->output->writeln("The package '" . $input->getArgument('package') . "' was not found in the Grav repository."); + $this->output->writeln(''); + $this->output->writeln("You can list all the available packages by typing:"); + $this->output->writeln(" " . $this->argv . " index"); + $this->output->writeln(''); + exit; + } + + $this->output->writeln("Found package '" . $input->getArgument('package') . "' under the '" . ucfirst($foundPackage->package_type) . "' section"); + $this->output->writeln(''); + $this->output->writeln("" . $foundPackage->name . " [" . $foundPackage->slug . "]"); + $this->output->writeln(str_repeat('-', strlen($foundPackage->name) + strlen($foundPackage->slug) + 3)); + $this->output->writeln("" . strip_tags($foundPackage->description_plain) . ""); + $this->output->writeln(''); + + $packageURL = ''; + if (isset($foundPackage->author->url)) { + $packageURL = '<' . $foundPackage->author->url . '>'; + } + + $this->output->writeln("".str_pad("Author", 12).": " . $foundPackage->author->name . ' <' . $foundPackage->author->email . '> '.$packageURL); + + foreach (array('version', 'keywords', 'date', 'homepage', 'demo', 'docs', 'guide', 'repository', 'bugs', 'zipball_url', 'license') as $info) { + if (isset($foundPackage->$info)) { + $name = ucfirst($info); + $data = $foundPackage->$info; + + if ($info == 'zipball_url') { + $name = "Download"; + } + + if ($info == 'date') { + $name = "Last Update"; + $data = date('D, j M Y, H:i:s, P ', strtotime('2014-09-16T00:07:16Z')); + } + + $name = str_pad($name, 12); + $this->output->writeln("".$name.": " . $data); + } + } + + $this->output->writeln(''); + $this->output->writeln("You can install this package by typing:"); + $this->output->writeln(" " . $this->argv . " install " . $foundPackage->slug . ""); + $this->output->writeln(''); + + } +} diff --git a/system/src/Grav/Console/Gpm/InstallCommand.php b/system/src/Grav/Console/Gpm/InstallCommand.php new file mode 100644 index 000000000..0447128a9 --- /dev/null +++ b/system/src/Grav/Console/Gpm/InstallCommand.php @@ -0,0 +1,212 @@ +setName("install") + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->addOption( + 'all-yes', + 'y', + InputOption::VALUE_NONE, + 'Assumes yes (or best approach) instead of prompting' + ) + ->addOption( + 'destination', + 'd', + InputOption::VALUE_OPTIONAL, + 'The destination where the package should be installed at. By default this would be where the grav instance has been launched from', + GRAV_ROOT + ) + ->addArgument( + 'package', + InputArgument::IS_ARRAY|InputArgument::REQUIRED, + 'The package of which more informations are desired. Use the "index" command for a list of packages' + ) + ->setDescription("Performs the installation of plugins and themes") + ->setHelp('The install command allows to install plugins and themes'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->setupConsole($input, $output); + + $this->gpm = new GPM($this->input->getOption('force')); + $this->destination = realpath($this->input->getOption('destination')); + + $packages = array_map('strtolower', $this->input->getArgument('package')); + $this->data = $this->gpm->findPackages($packages); + + if ( + !Installer::isGravInstance($this->destination) || + !Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK]) + ) { + $this->output->writeln("ERROR: " . Installer::lastErrorMsg()); + exit; + } + + $this->output->writeln(''); + + if (!$this->data['total']) { + $this->output->writeln("Nothing to install."); + $this->output->writeln(''); + exit; + } + + if (count($this->data['not_found'])) { + $this->output->writeln("These packages were not found on Grav: " . implode(', ', $this->data['not_found']) . ""); + } + + unset($this->data['not_found']); + unset($this->data['total']); + + foreach ($this->data as $data) { + foreach ($data as $package) { + $this->output->writeln("Preparing to install " . $package->name . " [v" . $package->version . "]"); + + $this->output->write(" |- Downloading package... 0%"); + $this->file = $this->downloadPackage($package); + + $this->output->write(" |- Checking destination... "); + $checks = $this->checkDestination($package); + + if (!$checks) { + $this->output->writeln(" '- Installation failed or aborted."); + $this->output->writeln(''); + } else { + $this->output->write(" |- Installing package... "); + $installation = $this->installPackage($package); + if (!$installation) { + $this->output->writeln(" '- Installation failed or aborted."); + $this->output->writeln(''); + } else { + $this->output->writeln(" '- Success! "); + $this->output->writeln(''); + } + } + } + } + } + + private function downloadPackage($package) + { + $this->tmp = sys_get_temp_dir() . DS . 'Grav-' . uniqid(); + $filename = $package->slug . basename($package->zipball_url); + $output = Response::get($package->zipball_url, [], [$this, 'progress']); + + Folder::mkdir($this->tmp); + + $this->output->write("\x0D"); + $this->output->write(" |- Downloading package... 100%"); + $this->output->writeln(''); + + file_put_contents($this->tmp . DS . $filename, $output); + + return $this->tmp . DS . $filename; + } + + private function checkDestination($package) + { + $questionHelper = $this->getHelper('question'); + $skipPrompt = $this->input->getOption('all-yes'); + + Installer::isValidDestination($this->destination . DS . $package->install_path); + + if (Installer::lastErrorCode() == Installer::EXISTS) { + if (!$skipPrompt) { + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... exists"); + + $question = new ConfirmationQuestion(" | '- The package has been detected as installed already, do you want to overwrite it? [y|N] ", false); + $answer = $questionHelper->ask($this->input, $this->output, $question); + + if (!$answer) { + $this->output->writeln(" | '- You decided to not overwrite the already installed package."); + + return false; + } + } + } + + if (Installer::lastErrorCode() == Installer::IS_LINK) { + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... symbolic link"); + + if ($skipPrompt) { + $this->output->writeln(" | '- Skipped automatically."); + + return false; + } + + $question = new ConfirmationQuestion(" | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ", false); + $answer = $questionHelper->ask($this->input, $this->output, $question); + + if (!$answer) { + $this->output->writeln(" | '- You decided to not delete the symlink automatically."); + + return false; + } + } + + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... ok"); + + return true; + } + + private function installPackage($package) + { + $installer = Installer::install($this->file, $this->destination, ['install_path' => $package->install_path]); + $errorCode = Installer::lastErrorCode(); + Folder::delete($this->tmp); + + if ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)) { + $this->output->write("\x0D"); + // extra white spaces to clear out the buffer properly + $this->output->writeln(" |- Installing package... error "); + $this->output->writeln(" | '- " . $installer->lastErrorMsg()); + + return false; + } + + $this->output->write("\x0D"); + // extra white spaces to clear out the buffer properly + $this->output->writeln(" |- Installing package... ok "); + + return true; + } + + public function progress($progress) + { + $this->output->write("\x0D"); + $this->output->write(" |- Downloading package... " . str_pad($progress['percent'], 5, " ", STR_PAD_LEFT) . '%'); + } +} diff --git a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php new file mode 100644 index 000000000..0a05914da --- /dev/null +++ b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php @@ -0,0 +1,122 @@ +setName("self-upgrade") + ->setAliases(['selfupgrade']) + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->setDescription("Detects and performs an update of plugins and themes when available") + ->setHelp('The update command updates plugins and themes when a new version is available'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->setupConsole($input, $output); + $this->upgrader = new Upgrader($this->input->getOption('force')); + + $local = $this->upgrader->getLocalVersion(); + $remote = $this->upgrader->getRemoteVersion(); + $update = $this->upgrader->getAssets()->{'grav-update'}; + $release = strftime('%c', strtotime($this->upgrader->getReleaseDate())); + + if (!$this->upgrader->isUpgradable()) { + $this->output->writeln("You are already running the latest version of Grav (v" . $local . ") released on " . $release); + exit; + } + + $this->output->writeln("Preparing to upgrade Grav to v" . $remote . " [release date: " . $release . "]"); + + $this->output->write(" |- Downloading upgrade [" . $this->formatBytes($update->size) . "]... 0%"); + $this->file = $this->download($update); + + $this->output->write(" |- Installing upgrade... "); + $installation = $this->upgrade(); + + if (!$installation) { + $this->output->writeln(" '- Installation failed or aborted."); + $this->output->writeln(''); + } else { + $this->output->writeln(" '- Success! "); + $this->output->writeln(''); + } + } + + private function download($package) + { + $this->tmp = sys_get_temp_dir() . DS . 'Grav-' . uniqid(); + $output = Response::get($package->download, [], [$this, 'progress']); + + Folder::mkdir($this->tmp); + + $this->output->write("\x0D"); + $this->output->write(" |- Downloading upgrade [" . $this->formatBytes($package->size) . "]... 100%"); + $this->output->writeln(''); + + file_put_contents($this->tmp . DS . $package->name, $output); + + return $this->tmp . DS . $package->name; + } + + private function upgrade() + { + $installer = Installer::install($this->file, GRAV_ROOT, ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true]); + $errorCode = Installer::lastErrorCode(); + Folder::delete($this->tmp); + + if ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)) { + $this->output->write("\x0D"); + // extra white spaces to clear out the buffer properly + $this->output->writeln(" |- Installing upgrade... error "); + $this->output->writeln(" | '- " . $installer->lastErrorMsg()); + + return false; + } + + $this->output->write("\x0D"); + // extra white spaces to clear out the buffer properly + $this->output->writeln(" |- Installing upgrade... ok "); + + return true; + } + + public function progress($progress) + { + $this->output->write("\x0D"); + $this->output->write(" |- Downloading upgrade [" . $this->formatBytes($progress["filesize"]) . "]... " . str_pad($progress['percent'], 5, " ", STR_PAD_LEFT) . '%'); + } + + public function formatBytes($size, $precision = 2) + { + $base = log($size) / log(1024); + $suffixes = array('', 'k', 'M', 'G', 'T'); + + return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)]; + } +} diff --git a/system/src/Grav/Console/Gpm/UpdateCommand.php b/system/src/Grav/Console/Gpm/UpdateCommand.php new file mode 100644 index 000000000..87c91c36e --- /dev/null +++ b/system/src/Grav/Console/Gpm/UpdateCommand.php @@ -0,0 +1,173 @@ +setName("update") + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->addOption( + 'destination', + 'd', + InputOption::VALUE_OPTIONAL, + 'The grav instance location where the updates should be applied to. By default this would be where the grav cli has been launched from', + GRAV_ROOT + ) + ->addArgument( + 'package', + InputArgument::IS_ARRAY|InputArgument::OPTIONAL, + 'The package or packages that is desired to update. By default all available updates will be applied.' + ) + ->setDescription("Detects and performs an update of plugins and themes when available") + ->setHelp('The update command updates plugins and themes when a new version is available'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->setupConsole($input, $output); + + $this->gpm = new GPM($this->input->getOption('force')); + $this->destination = realpath($this->input->getOption('destination')); + + if (!Installer::isGravInstance($this->destination)) { + $this->output->writeln("ERROR: " . Installer::lastErrorMsg()); + exit; + } + + $this->data = $this->gpm->getUpdatable(); + $onlyPackages = array_map('strtolower', $this->input->getArgument('package')); + + if (!$this->data['total']) { + $this->output->writeln("Nothing to update."); + exit; + } + + $this->output->write("Found " . $this->gpm->countInstalled() . " extensions installed of which " . $this->data['total'] . " need updating"); + + $limitTo = $this->userInputPackages($onlyPackages); + + $this->output->writeln(''); + + unset($this->data['total']); + unset($limitTo['total']); + + + // updates review + $slugs = []; + + $index = 0; + foreach ($this->data as $packages) { + foreach ($packages as $slug => $package) { + if (count($limitTo) && !array_key_exists($slug, $limitTo)) { + continue; + } + + $this->output->writeln( + // index + str_pad($index++ + 1, 2, '0', STR_PAD_LEFT) . ". " . + // name + "" . str_pad($package->name, 15) . " " . + // version + "[v" . $package->version . " ➜ v" . $package->available . "]" + ); + $slugs[] = $slug; + } + } + + // prompt to continue + $this->output->writeln(""); + $questionHelper = $this->getHelper('question'); + $question = new ConfirmationQuestion("Continue with the update process? [Y|n] ", true); + $answer = $questionHelper->ask($this->input, $this->output, $question); + + if (!$answer) { + $this->output->writeln("Update aborted. Exiting..."); + exit; + } + + // finally update + $installCommand = $this->getApplication()->find('install'); + + $args = new ArrayInput(array( + 'command' => 'install', + 'package' => $slugs, + '-f' => $this->input->getOption('force'), + '-d' => $this->destination, + '-y' => true + )); + $commandExec = $installCommand->run($args, $this->output); + + if ($commandExec != 0) { + $this->output->writeln("Error: An error occured while trying to install the extensions"); + exit; + } + } + + private function userInputPackages($onlyPackages) + { + $found = ['total' => 0]; + $ignore = []; + + if (!count($onlyPackages)) { + $this->output->writeln(''); + } else { + foreach ($onlyPackages as $onlyPackage) { + $find = $this->gpm->findPackage($onlyPackage); + + if (!$find || !$this->gpm->isUpdatable($find->slug)) { + $name = isset($find->slug) ? $find->slug : $onlyPackage; + $ignore[$name] = $name; + } else { + $found[$find->slug] = $find; + $found['total']++; + } + } + + if ($found['total']) { + $list = $found; + unset($list['total']); + $list = array_keys($list); + + if ($found['total'] !== $this->data['total']) { + $this->output->write(", only ".$found['total']." will be updated"); + } + + $this->output->writeln(''); + $this->output->writeln("Limiting updates for only ".implode(', ', $list).""); + } + + if (count($ignore)) { + $this->output->writeln("Packages not found or not requiring updates: ".implode(', ', $ignore).""); + } + } + + return $found; + } +}