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.');
+ }
+}