diff --git a/CHANGELOG.md b/CHANGELOG.md index c2996b161..620be3db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# v1.1.18 +## 02/xx/2017 + +1. [](#new) + * Added default setting to only allow `direct-installs` from official GPM. Can be configured in `system.yaml` + * Added a new `Utils::isValidUrl()` method +1. [](#improved) + * Genericized `direct-install` so it can be called via Admin plugin +1. [](#bugfix) + * Fixed a minor bug in Number validation [#1329](https://github.com/getgrav/grav/issues/1329) + * Fixed exception when trying to find user account and there is no `user://accounts` folder + # v1.1.17 ## 02/17/2017 diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 6aa0d6012..03f6b898e 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -1057,6 +1057,19 @@ form: fopen: PLUGIN_ADMIN.FOPEN curl: PLUGIN_ADMIN.CURL + gpm.official_gpm_only: + type: toggle + label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY + highlight: auto + help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: true + validate: + type: bool + gpm.verify_peer: type: toggle label: PLUGIN_ADMIN.GPM_VERIFY_PEER diff --git a/system/config/system.yaml b/system/config/system.yaml index 7d4de30e0..99628151c 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -138,3 +138,4 @@ gpm: proxy_url: # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128) method: 'auto' # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL verify_peer: true # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help. + official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index b597cd642..fd886b017 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -339,7 +339,7 @@ class Validation protected static function filterNumber($value, array $params, array $field) { - return (int) $value; + return self::validateFloat($value, $params) ? (float) $value : (int) $value; } protected static function filterDateTime($value, array $params, array $field) @@ -585,7 +585,7 @@ class Validation $values[$key] = array_map('trim', explode(',', $value)); } else { $values[$key] = trim($value); - } + } } } diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php index 857dd8fc0..584adbdd7 100644 --- a/system/src/Grav/Common/GPM/GPM.php +++ b/system/src/Grav/Common/GPM/GPM.php @@ -9,6 +9,7 @@ namespace Grav\Common\GPM; use Grav\Common\Grav; +use Grav\Common\Filesystem\Folder; use Grav\Common\Inflector; use Grav\Common\Iterator; use Grav\Common\Utils; @@ -499,6 +500,151 @@ class GPM extends Iterator return false; } + /** + * Download the zip package via the URL + * + * @param $package_file + * @param $tmp + * @return null|string + */ + public static function downloadPackage($package_file, $tmp) + { + $package = parse_url($package_file); + $filename = basename($package['path']); + + if (Grav::instance()['config']->get('system.gpm.official_gpm_only') && $package['host'] !== 'getgrav.org') { + throw new \RuntimeException("Only offical GPM URLs are allowed. You can modify this behavior in the System configuration."); + } + + $output = Response::get($package_file, []); + + if ($output) { + Folder::mkdir($tmp); + file_put_contents($tmp . DS . $filename, $output); + return $tmp . DS . $filename; + } + + return null; + } + + /** + * Copy the local zip package to tmp + * + * @param $package_file + * @param $tmp + * @return null|string + */ + public static function copyPackage($package_file, $tmp) + { + $package_file = realpath($package_file); + + if (file_exists($package_file)) { + $filename = basename($package_file); + Folder::mkdir($tmp); + copy(realpath($package_file), $tmp . DS . $filename); + return $tmp . DS . $filename; + } + + return null; + } + + /** + * Try to guess the package type from the source files + * + * @param $source + * @return bool|string + */ + public static function getPackageType($source) + { + $plugin_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Plugin/m'; + $theme_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Theme/m'; + + if ( + file_exists($source . 'system/defines.php') && + file_exists($source . 'system/config/system.yaml') + ) { + return 'grav'; + } else { + // must have a blueprint + if (!file_exists($source . 'blueprints.yaml')) { + return false; + } + + // either theme or plugin + $name = basename($source); + if (Utils::contains($name, 'theme')) { + return 'theme'; + } elseif (Utils::contains($name, 'plugin')) { + return 'plugin'; + } + foreach (glob($source . "*.php") as $filename) { + $contents = file_get_contents($filename); + if (preg_match($theme_regex, $contents)) { + return 'theme'; + } elseif (preg_match($plugin_regex, $contents)) { + return 'plugin'; + } + } + + // Assume it's a theme + return 'theme'; + } + } + + /** + * Try to guess the package name from the source files + * + * @param $source + * @return bool|string + */ + public static function getPackageName($source) + { + foreach (glob($source . "*.yaml") as $filename) { + $name = strtolower(basename($filename, '.yaml')); + if ($name == 'blueprints') { + continue; + } + return $name; + } + return false; + } + + /** + * Find/Parse the blueprint file + * + * @param $source + * @return array|bool + */ + public static function getBlueprints($source) + { + $blueprint_file = $source . 'blueprints.yaml'; + if (!file_exists($blueprint_file)) { + return false; + } + + $blueprint = (array)Yaml::parse(file_get_contents($blueprint_file)); + return $blueprint; + } + + /** + * Get the install path for a name and a particular type of package + * + * @param $type + * @param $name + * @return string + */ + public static function getInstallPath($type, $name) + { + $locator = Grav::instance()['locator']; + + if ($type == 'theme') { + $install_path = $locator->findResource('themes://', false) . DS . $name; + } else { + $install_path = $locator->findResource('plugins://', false) . DS . $name; + } + return $install_path; + } + /** * Searches for a list of Packages in the repository * @param array $searches An array of either slugs or names diff --git a/system/src/Grav/Common/GPM/Response.php b/system/src/Grav/Common/GPM/Response.php index 6b3036ff7..f046352ac 100644 --- a/system/src/Grav/Common/GPM/Response.php +++ b/system/src/Grav/Common/GPM/Response.php @@ -189,6 +189,17 @@ class Response return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen')); } + /** + * Is this a remote file or not + * + * @param $file + * @return bool + */ + public static function isRemote($file) + { + return (bool) filter_var($file, FILTER_VALIDATE_URL); + } + /** * Progress normalized for cURL and Fopen * Accepts a variable length of arguments passed in by stream method diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index dd8308db6..e1383688c 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -727,7 +727,7 @@ class Uri * * @return boolean is eternal state */ - public function isExternal($url) + public static function isExternal($url) { if (Utils::startsWith($url, 'http')) { return true; @@ -1086,4 +1086,20 @@ class Uri return $urlWithNonce; } + + /** + * Is the passed in URL a valid URL? + * + * @param $url + * @return bool + */ + public static function isValidUrl($url) + { + $regex = '/^(?:(https?|ftp|telnet):)?\/\/((?:[a-z0-9@:.-]|%[0-9A-F]{2}){3,})(?::(\d+))?((?:\/(?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\@]|%[0-9A-F]{2})*)*)(?:\?((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?/'; + if (preg_match($regex, $url)) { + return true; + } else { + return false; + } + } } diff --git a/system/src/Grav/Common/User/User.php b/system/src/Grav/Common/User/User.php index fcc8f6c43..d5c108ff8 100644 --- a/system/src/Grav/Common/User/User.php +++ b/system/src/Grav/Common/User/User.php @@ -57,14 +57,14 @@ class User extends Data /** * Find a user by username, email, etc * - * @param $query the query to search for + * @param string $query the query to search for * @param array $fields the fields to search * @return User */ public static function find($query, $fields = ['username', 'email']) { $account_dir = Grav::instance()['locator']->findResource('account://'); - $files = array_diff(scandir($account_dir), ['.', '..']); + $files = $account_dir ? array_diff(scandir($account_dir), ['.', '..']) : []; // Try with username first, you never know! if (in_array('username', $fields)) { @@ -100,7 +100,7 @@ class User extends Data public static function remove($username) { $file_path = Grav::instance()['locator']->findResource('account://' . $username . YAML_EXT); - if (file_exists($file_path) && unlink($file_path)) { + if ($file_path && unlink($file_path)) { return true; } diff --git a/system/src/Grav/Console/Gpm/DirectInstallCommand.php b/system/src/Grav/Console/Gpm/DirectInstallCommand.php index e23712e9f..202840289 100644 --- a/system/src/Grav/Console/Gpm/DirectInstallCommand.php +++ b/system/src/Grav/Console/Gpm/DirectInstallCommand.php @@ -9,14 +9,13 @@ namespace Grav\Console\Gpm; use Grav\Common\Grav; -use Grav\Common\Utils; use Grav\Common\Filesystem\Folder; +use Grav\Common\GPM\GPM; use Grav\Common\GPM\Installer; use Grav\Common\GPM\Response; use Grav\Console\ConsoleCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Question\ConfirmationQuestion; -use Symfony\Component\Yaml\Yaml; class DirectInstallCommand extends ConsoleCommand { @@ -63,10 +62,30 @@ class DirectInstallCommand extends ConsoleCommand $this->output->writeln("Preparing to install " . $package_file . ""); - if ($this->isRemote($package_file)) { - $zip = $this->downloadPackage($package_file, $tmp_zip); + if (Response::isRemote($package_file)) { + $this->output->write(" |- Downloading package... 0%"); + try { + $zip = GPM::downloadPackage($package_file, $tmp_zip); + } catch (\RuntimeException $e) { + $this->output->writeln(''); + $this->output->writeln(" `- ERROR: " . $e->getMessage() . ""); + $this->output->writeln(''); + exit; + } + + if ($zip) { + $this->output->write("\x0D"); + $this->output->write(" |- Downloading package... 100%"); + $this->output->writeln(''); + } } else { - $zip = $this->copyPackage($package_file, $tmp_zip); + $this->output->write(" |- Copying package... 0%"); + $zip = GPM::copyPackage($package_file, $tmp_zip); + if ($zip) { + $this->output->write("\x0D"); + $this->output->write(" |- Copying package... 100%"); + $this->output->writeln(''); + } } if (file_exists($zip)) { @@ -85,7 +104,7 @@ class DirectInstallCommand extends ConsoleCommand $this->output->writeln(" |- Extracting package... ok"); - $type = $this->getPackageType($extracted); + $type = GPM::getPackageType($extracted); if (!$type) { $this->output->writeln(" '- ERROR: Not a valid Grav package"); @@ -93,7 +112,7 @@ class DirectInstallCommand extends ConsoleCommand exit; } - $blueprint = $this->getBlueprints($extracted); + $blueprint = GPM::getBlueprints($extracted); if ($blueprint) { if (isset($blueprint['dependencies'])) { $depencencies = []; @@ -137,7 +156,7 @@ class DirectInstallCommand extends ConsoleCommand $this->output->write(" |- Installing package... "); Installer::install($zip, GRAV_ROOT, ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true], $extracted); } else { - $name = $this->getPackageName($extracted); + $name = GPM::getPackageName($extracted); if (!$name) { $this->output->writeln("ERROR: Name could not be determined. Please specify with --name|-n"); @@ -145,7 +164,7 @@ class DirectInstallCommand extends ConsoleCommand exit; } - $install_path = $this->getInstallPath($type, $name); + $install_path = GPM::getInstallPath($type, $name); $is_update = file_exists($install_path); $this->output->write(" |- Checking destination... "); @@ -173,7 +192,7 @@ class DirectInstallCommand extends ConsoleCommand $this->output->write("\x0D"); if(Installer::lastErrorCode()) { - $this->output->writeln(" '- Installation failed or aborted."); + $this->output->writeln(" '- " . Installer::lastErrorMsg() . ""); $this->output->writeln(''); } else { $this->output->writeln(" |- Installing package... ok"); @@ -193,186 +212,4 @@ class DirectInstallCommand extends ConsoleCommand return true; } - - /** - * Get the install path for a name and a particular type of package - * - * @param $type - * @param $name - * @return string - */ - protected function getInstallPath($type, $name) - { - $locator = Grav::instance()['locator']; - - if ($type == 'theme') { - $install_path = $locator->findResource('themes://', false) . DS . $name; - } else { - $install_path = $locator->findResource('plugins://', false) . DS . $name; - } - return $install_path; - } - - /** - * Try to guess the package name from the source files - * - * @param $source - * @return bool|string - */ - protected function getPackageName($source) - { - foreach (glob($source . "*.yaml") as $filename) { - $name = strtolower(basename($filename, '.yaml')); - if ($name == 'blueprints') { - continue; - } - return $name; - } - return false; - } - - /** - * Try to guess the package type from the source files - * - * @param $source - * @return bool|string - */ - protected function getPackageType($source) - { - $plugin_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Plugin/m'; - $theme_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Theme/m'; - - if ( - file_exists($source . 'system/defines.php') && - file_exists($source . 'system/config/system.yaml') - ) { - return 'grav'; - } else { - // must have a blueprint - if (!file_exists($source . 'blueprints.yaml')) { - return false; - } - - // either theme or plugin - $name = basename($source); - if (Utils::contains($name, 'theme')) { - return 'theme'; - } elseif (Utils::contains($name, 'plugin')) { - return 'plugin'; - } - foreach (glob($source . "*.php") as $filename) { - $contents = file_get_contents($filename); - if (preg_match($theme_regex, $contents)) { - return 'theme'; - } elseif (preg_match($plugin_regex, $contents)) { - return 'plugin'; - } - } - - // Assume it's a theme - return 'theme'; - } - } - - /** - * Determine if this is a local or a remote file - * - * @param $file - * @return bool - */ - protected function isRemote($file) - { - return (bool) filter_var($file, FILTER_VALIDATE_URL); - } - - /** - * Find/Parse the blueprint file - * - * @param $source - * @return array|bool - */ - protected function getBlueprints($source) - { - $blueprint_file = $source . 'blueprints.yaml'; - if (!file_exists($blueprint_file)) { - return false; - } - - $blueprint = (array)Yaml::parse(file_get_contents($blueprint_file)); - return $blueprint; - } - - /** - * Download the zip package via the URL - * - * @param $package_file - * @param $tmp - * @return null|string - */ - private function downloadPackage($package_file, $tmp) - { - $this->output->write(" |- Downloading package... 0%"); - - $package = parse_url($package_file); - - - $filename = basename($package['path']); - $output = Response::get($package_file, [], [$this, 'progress']); - - if ($output) { - Folder::mkdir($tmp); - - $this->output->write("\x0D"); - $this->output->write(" |- Downloading package... 100%"); - $this->output->writeln(''); - - file_put_contents($tmp . DS . $filename, $output); - - return $tmp . DS . $filename; - } - - return null; - - } - - /** - * Copy the local zip package to tmp - * - * @param $package_file - * @param $tmp - * @return null|string - */ - private function copyPackage($package_file, $tmp) - { - $this->output->write(" |- Copying package... 0%"); - - $package_file = realpath($package_file); - - if (file_exists($package_file)) { - $filename = basename($package_file); - - Folder::mkdir($tmp); - - $this->output->write("\x0D"); - $this->output->write(" |- Copying package... 100%"); - $this->output->writeln(''); - - copy(realpath($package_file), $tmp . DS . $filename); - - return $tmp . DS . $filename; - } - - return null; - - } - - /** - * @param $progress - */ - public function progress($progress) - { - $this->output->write("\x0D"); - $this->output->write(" |- Downloading package... " . str_pad($progress['percent'], 5, " ", - STR_PAD_LEFT) . '%'); - } } diff --git a/tests/unit/Grav/Common/Markdown/ParsedownTest.php b/tests/unit/Grav/Common/Markdown/ParsedownTest.php index bbd1cad3a..e73e11f01 100644 --- a/tests/unit/Grav/Common/Markdown/ParsedownTest.php +++ b/tests/unit/Grav/Common/Markdown/ParsedownTest.php @@ -675,6 +675,8 @@ class ParsedownTest extends \Codeception\TestCase\Test { $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + $this->assertSame('

Anchor Class

', + $this->parsedown->text('[Anchor Class](?classes=button#something)')); $this->assertSame('

Relative Class

', $this->parsedown->text('[Relative Class](../item2-3?classes=button)')); $this->assertSame('

Relative ID

',