diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f855b2a..bd3a997a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# v0.9.36 +## 08/11/2015 + +1. [](#new) + * Added a new `newuser` CLI command to create user accounts + * Added `default` blueprint for all templates + * Support `user` and `system` language translation merging +1. [](#improved) + * Added isSymlink method in GPM to determine if Grav is symbolically linked or not + * Refactored page recursing + * Updated blueprints to use new toggles + * Updated blueprints to use current date for date format fields + * Updated composer.phar + * Use sessions for admin even when disabled for site + * Use `GRAV_ROOT` in session identifier + # v0.9.35 ## 08/06/2015 @@ -5,13 +21,12 @@ * Added `body_classes` field * Added `visiblity` toggle and help tooltips on new page form * Added new `Page.unsetRoute()` method to allow admin to regenerate the route -1. [](#improved) +2. [](#improved) * User save no longer stores username each time * Page list form field now shows all pages except root * Removed required option from page title * Added configuration settings for running Nginx in sub directory -1. [](#bugfix) - * Fixed issue with GPM and cURL throwing `Undefined offset: 1` error +3. [](#bugfix) * Fixed deep translation merging * Fixed broken **metadata** merging with site defaults * Fixed broken **summary** field diff --git a/bin/composer.phar b/bin/composer.phar index 4b099f5b8..deead1618 100755 Binary files a/bin/composer.phar and b/bin/composer.phar differ diff --git a/bin/grav b/bin/grav index fb101b78f..26a633566 100755 --- a/bin/grav +++ b/bin/grav @@ -41,5 +41,6 @@ $app->addCommands(array( new Grav\Console\Cli\ClearCacheCommand(), new Grav\Console\Cli\BackupCommand(), new Grav\Console\Cli\NewProjectCommand(), + new Grav\Console\Cli\NewUserCommand(), )); $app->run(); diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index ab1fead2c..65401dece 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -50,31 +50,31 @@ form: '': 'Default (Server Timezone)' pages.dateformat.short: - type: select + type: dateformat size: medium classes: fancy label: Short date format help: "Set the short date format that can be used by themes" default: "jS M Y" options: - "F jS \\a\\t g:ia": "January 1st at 11:59pm" - "l jS of F g:i A": "Monday 1st of January at 11:59 PM" - "D, m M Y G:i:s": "Mon, 01 Jan 2014 23:59:00" - "d-m-y G:i": "01-01-14 23:59" - "jS M Y": "10th Feb 2014" + "F jS \\a\\t g:ia": Date1 + "l jS of F g:i A": Date2 + "D, m M Y G:i:s": Date3 + "d-m-y G:i": Date4 + "jS M Y": Date5 pages.dateformat.long: - type: select + type: dateformat size: medium classes: fancy label: Long date format help: "Set the long date format that can be used by themes" options: - "F jS \\a\\t g:ia": "January 1st at 11:59pm" - "l jS of F g:i A": "Monday 1st of January at 11:59 PM" - "D, m M Y G:i:s": "Mon, 01 Jan 2014 23:59:00" - "d-m-y G:i": "01-01-14 23:59" - "jS M Y": "10th Feb 2014" + "F jS \\a\\t g:ia": Date1 + "l jS of F g:i A": Date2 + "D, m M Y G:i:s": Date3 + "d-m-y G:i": Date4 + "jS M Y": Date5 pages.order.by: type: select @@ -180,7 +180,7 @@ form: languages.session_store_active: type: toggle label: Active language in session - help: "Support translations in Grav, plugins and extensions" + help: "Store the active language in the session" highlight: 0 options: 1: Yes diff --git a/system/blueprints/pages/default.yaml b/system/blueprints/pages/default.yaml index d0f78c6ea..2e757a472 100644 --- a/system/blueprints/pages/default.yaml +++ b/system/blueprints/pages/default.yaml @@ -50,12 +50,12 @@ form: fields: header.published: type: toggle + toggleable: true label: Published help: "By default, a page is published unless you explicitly set published: false or via a publish_date being in the future, or unpublish_date in the past" highlight: 1 size: medium options: - '': Global 1: Yes 0: No validate: @@ -232,11 +232,11 @@ form: header.visible: type: toggle + toggleable: true label: Visible help: "Determines if a page is visible in the navigation." highlight: 1 options: - '': Global 1: Enabled 0: Disabled validate: @@ -244,12 +244,11 @@ form: header.routable: type: toggle + toggleable: true label: Routable help: If this page is reachable by a URL highlight: 1 - default: '' options: - '': Global 1: Enabled 0: Disabled validate: @@ -257,10 +256,10 @@ form: header.cache_enable: type: toggle + toggleable: true label: Caching highlight: 1 options: - '': Global 1: Enabled 0: Disabled validate: diff --git a/system/defines.php b/system/defines.php index 6828ce4fe..84c41d5f6 100644 --- a/system/defines.php +++ b/system/defines.php @@ -2,7 +2,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '0.9.35'); +define('GRAV_VERSION', '0.9.36'); define('DS', '/'); // Directories and Paths diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php index cc1feeac6..0cafeb7a0 100644 --- a/system/src/Grav/Common/Config/Config.php +++ b/system/src/Grav/Common/Config/Config.php @@ -373,10 +373,11 @@ class Config extends Data $content = $lang_file->content(); $this->languages->mergeRecursive($content); } + unset($languageFiles['user/plugins']); } - if (isset($languageFiles['system/languages'])) { - foreach ((array) $languageFiles['system/languages'] as $lang => $item) { + foreach ($languageFiles as $location) { + foreach ($location as $lang => $item) { $lang_file = CompiledYamlFile::instance($item['file']); $content = $lang_file->content(); $this->languages->join($lang, $content, '/'); diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php index e2ebd0ed2..859257f3b 100644 --- a/system/src/Grav/Common/GPM/Installer.php +++ b/system/src/Grav/Common/GPM/Installer.php @@ -287,4 +287,14 @@ class Installer { return self::$error; } + + /** + * Allows to manually set an error + * @param $error the Error code + */ + + public static function setError($error) + { + self::$error = $error; + } } diff --git a/system/src/Grav/Common/GPM/Remote/Grav.php b/system/src/Grav/Common/GPM/Remote/Grav.php index 7b9cef274..d0a62a25f 100644 --- a/system/src/Grav/Common/GPM/Remote/Grav.php +++ b/system/src/Grav/Common/GPM/Remote/Grav.php @@ -87,4 +87,9 @@ class Grav extends AbstractPackageCollection { return version_compare(GRAV_VERSION, $this->getVersion(), '<'); } + + public function isSymlink() + { + return is_link(GRAV_ROOT . DS . 'index.php'); + } } diff --git a/system/src/Grav/Common/GPM/Upgrader.php b/system/src/Grav/Common/GPM/Upgrader.php index f9e3c0e81..590ad4df7 100644 --- a/system/src/Grav/Common/GPM/Upgrader.php +++ b/system/src/Grav/Common/GPM/Upgrader.php @@ -80,4 +80,14 @@ class Upgrader { return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), "<"); } + + /** + * Checks if Grav is currently symbolically linked + * @return boolean True if Grav is symlinked, False otherwise. + */ + + public function isSymlink() + { + return $this->remote->isSymlink(); + } } diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index 5e4737411..e9aceffef 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -257,7 +257,7 @@ class Grav extends Container $this['session']->close(); } - if ($this['uri']->isExternal($route)) { + if ($uri->isExternal($route)) { $url = $route; } else { $url = rtrim($uri->rootUrl(), '/') .'/'. trim($route, '/'); @@ -277,7 +277,6 @@ class Grav extends Container { /** @var Language $language */ $language = $this['language']; - $config = $this['config']; if ($language->enabled()) { return $this->redirect($language->getLanguage() . $route, $code); @@ -413,7 +412,7 @@ class Grav extends Container } /** - * This attempts to fine media, other files, and download them + * This attempts to find media, other files, and download them * @param $page * @param $path */ diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 0666b8fc4..4263a5731 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -705,16 +705,16 @@ class Page $pages = self::getGrav()['pages']; $blueprint = $pages->blueprints($this->blueprintName()); - $fields = $blueprint->fields(); + $edit_mode = self::getGrav()['admin'] ? self::getGrav()['config']->get('plugins.admin.edit_mode') : null; // override if you only want 'normal' mode - if (empty($fields) && self::getGrav()['admin'] && self::getGrav()['config']->get('plugins.admin.edit_mode', 'auto') == 'normal') { + if (empty($fields) && ($edit_mode == 'auto' || $edit_mode == 'normal')) { $blueprint = $pages->blueprints('default'); } // override if you only want 'expert' mode - if (!empty($fields) && self::getGrav()['admin'] && self::getGrav()['config']->get('plugins.admin.edit_mode', 'auto') == 'expert') { + if (!empty($fields) && $edit_mode == 'expert') { $blueprint = $pages->blueprints(''); } diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index c132e158b..27fe4c3ce 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -514,7 +514,7 @@ class Pages /** @var UniformResourceLocator $locator */ $locator = $this->grav['locator']; - $pagesDir = $locator->findResource('page://'); + $pages_dir = $locator->findResource('page://'); if ($config->get('system.cache.enabled')) { /** @var Cache $cache */ @@ -529,10 +529,10 @@ class Pages $last_modified = 0; break; case 'folder': - $last_modified = Folder::lastModifiedFolder($pagesDir); + $last_modified = Folder::lastModifiedFolder($pages_dir); break; default: - $last_modified = Folder::lastModifiedFile($pagesDir); + $last_modified = Folder::lastModifiedFile($pages_dir); } $page_cache_id = md5(USER_DIR.$last_modified.$language->getActive().$config->checksum()); @@ -541,25 +541,46 @@ class Pages if (!$this->instances) { $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..'); - $this->recurse($pagesDir); - $this->buildRoutes(); + // recurse pages and cache result + $this->resetPages($pages_dir, $page_cache_id); - // save pages, routes, taxonomy, and sort to cache - $cache->save( - $page_cache_id, - array($this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort) - ); } else { // If pages was found in cache, set the taxonomy $this->grav['debugger']->addMessage('Page cache hit.'); $taxonomy->taxonomy($taxonomy_map); } } else { - $this->recurse($pagesDir); + $this->recurse($pages_dir); $this->buildRoutes(); } } + /** + * Accessible method to manually reset the pages cache + * + * @param $pages_dir + * @param $page_cache_id + */ + public function resetPages($pages_dir, $page_cache_id) + { + $this->recurse($pages_dir); + $this->buildRoutes(); + + // cache if needed + if ($this->grav['config']->get('system.cache.enabled')) { + /** @var Cache $cache */ + $cache = $this->grav['cache']; + /** @var Taxonomy $taxonomy */ + $taxonomy = $this->grav['taxonomy']; + + // save pages, routes, taxonomy, and sort to cache + $cache->save( + $page_cache_id, + array($this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort) + ); + } + } + /** * Recursive function to load & build page relationships. * diff --git a/system/src/Grav/Common/Page/Types.php b/system/src/Grav/Common/Page/Types.php index f6407cadd..7138359d9 100644 --- a/system/src/Grav/Common/Page/Types.php +++ b/system/src/Grav/Common/Page/Types.php @@ -49,6 +49,9 @@ class Types implements \ArrayAccess, \Iterator, \Countable $this->systemBlueprints = $this->findBlueprints('blueprints://pages'); } + // register default by default + $this->register('default'); + foreach (Folder::all($path, $options) as $type) { $this->register($type); } diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php index c587b6332..2b3469178 100644 --- a/system/src/Grav/Common/Session.php +++ b/system/src/Grav/Common/Session.php @@ -20,17 +20,23 @@ class Session extends \RocketTheme\Toolbox\Session\Session $uri = $this->grav['uri']; $config = $this->grav['config']; - if ($config->get('system.session.enabled')) { - // Only activate admin if we're inside the admin path. - $is_admin = false; + $is_admin = false; + + $session_timeout = $config->get('system.session.timeout', 1800); + $session_path = $config->get('system.session.path', '/' . ltrim($uri->rootUrl(false), '/')); + + // Activate admin if we're inside the admin path. + if ($config->get('plugins.admin.enabled')) { $route = $config->get('plugins.admin.route'); $base = '/' . trim($route, '/'); if (substr($uri->route(), 0, strlen($base)) == $base) { + $session_timeout = $config->get('plugins.admin.session.timeout', 1800); $is_admin = true; } + } + + if ($config->get('system.session.enabled') || $is_admin) { - $session_timeout = $config->get('system.session.timeout', 1800); - $session_path = $config->get('system.session.path', '/' . ltrim($uri->rootUrl(false), '/')); // Define session service. parent::__construct( @@ -38,8 +44,8 @@ class Session extends \RocketTheme\Toolbox\Session\Session $session_path ); - $site_identifier = $config->get('site.title', 'unknown'); - $this->setName($config->get('system.session.name', 'grav_site') . '_' . substr(md5($site_identifier), 0, 7) . ($is_admin ? '_admin' : '')); + $unique_identifier = GRAV_ROOT; + $this->setName($config->get('system.session.name', 'grav_site') . '_' . substr(md5($unique_identifier), 0, 7) . ($is_admin ? '_admin' : '')); $this->start(); setcookie(session_name(), session_id(), time() + $session_timeout, $session_path); } diff --git a/system/src/Grav/Console/Cli/NewUserCommand.php b/system/src/Grav/Console/Cli/NewUserCommand.php new file mode 100644 index 000000000..cbab7804d --- /dev/null +++ b/system/src/Grav/Console/Cli/NewUserCommand.php @@ -0,0 +1,136 @@ +setName("newuser") + ->setDescription("Creates a new user") + ->setHelp('The newuser creates a new user file in user/accounts/ folder'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int|null|void + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->setupConsole($input, $output); + $helper = $this->getHelper('question'); + $data = []; + + $this->output->writeln('Create new user'); + $this->output->writeln(''); + + // Get username and validate + $question = new Question('Enter a username: ', 'admin'); + $question->setValidator(function ($value) { + if (!preg_match('/^[a-z0-9_-]{3,16}$/', $value)) { + throw new RuntimeException( + 'Username should be between 3 and 16 comprised of lowercase letters, numbers, underscores and hyphens' + ); + } + if (file_exists(self::getGrav()['locator']->findResource('user://accounts/' . $value . YAML_EXT))) { + throw new RuntimeException( + 'Username "'.$value.'" already exists, please pick another username' + ); + } + return $value; + }); + $username = $helper->ask($this->input, $this->output, $question); + + // Get password and validate + $question = new Question('Enter a password: '); + $question->setValidator(function ($value) { + if (!preg_match('/(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/', $value)) { + throw new RuntimeException('Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters'); + } + return $value; + }); + $data['password'] = $helper->ask($this->input, $this->output, $question); + + // Get email and validate + $question = new Question('Enter an email: '); + $question->setValidator(function ($value) { + if (!preg_match('/^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/', $value)) { + throw new RuntimeException( + 'Not a valid email address' + ); + } + return $value; + }); + $data['email'] = $helper->ask($this->input, $this->output, $question); + + // Choose permissions + $question = new ChoiceQuestion( + 'Please choose a set of permissions:', + array('a'=>'admin access', 's'=>'site access', 'b'=>'admin and site access'), + 'a' + ); + $question->setErrorMessage('permissions %s is invalid.'); + $permissions_choice = $helper->ask($this->input, $this->output, $question); + + switch ($permissions_choice) { + case 'a': + $data['access']['admin'] = ['login' => true, 'super' => true]; + break; + case 's': + $data['access']['site'] = ['login' => true]; + break; + case 'b': + $data['access']['admin'] = ['login' => true, 'super' => true]; + $data['access']['site'] = ['login' => true]; + } + + // Get fullname + $question = new Question('Enter a fullname: '); + $question->setValidator(function ($value) { + if ($value === null or trim($value) == '') { + throw new RuntimeException( + 'Fullname is required' + ); + } + return $value; + }); + $data['fullname'] = $helper->ask($this->input, $this->output, $question); + + // Get title + $question = new Question('Enter a title: '); + $data['title'] = $helper->ask($this->input, $this->output, $question); + + // Create user object and save it + $user = new User($data); + $file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('user://accounts/' . $username . YAML_EXT, true, true)); + $user->file($file); + $user->save(); + + $this->output->writeln(''); + $this->output->writeln('Success! User '. $username .' created.'); + } +}