Merge branch 'release/0.9.25'

This commit is contained in:
Andy Miller
2015-04-24 14:07:01 -06:00
21 changed files with 220 additions and 86 deletions

View File

@@ -1,3 +1,19 @@
# v0.9.25
## 04/24/2015
1. [](#new)
* Added support for E-Tag, Last-Modified, Cache-Control and Page-based expires headers
2. [](#improved)
* Refactored media image handling to make it more flexible and support absolute paths
* Refactored page modification check process to make it faster
* User account improvements in preparation for Admin plugin
* Protect against timing attacks
* Reset default system expires time to 0 seconds (can override if you need to)
3. [](#bugfix)
* Fix issues with spaces in webroot when using `bin/grav install`
* Fix for spaces in relative directory
* Bug fix in collection filtering
# v0.9.24
## 04/15/2015

View File

@@ -2,9 +2,9 @@
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/getgrav/grav?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Grav is a **Fast**, **Simple**, and **Flexible**, file-based Web-platform. There is **Zero** installation required. Just extract the ZIP archive, and you are already up and running. It follows similar principals to other flat-file CMS platforms, but has a different design philosophy than most. Grav comes with a powerful **Package Management System** to allow for simple installation and upgrading of plugins and themes, as well as simple updating of Grav itself.
Grav is a **Fast**, **Simple**, and **Flexible**, file-based Web-platform. There is **Zero** installation required. Just extract the ZIP archive, and you are already up and running. It follows similar principles to other flat-file CMS platforms, but has a different design philosophy than most. Grav comes with a powerful **Package Management System** to allow for simple installation and upgrading of plugins and themes, as well as simple updating of Grav itself.
The underlying architecture of Grav has been designed to use well-established and _best-in-class_ technologies, where applicable, to ensure that Grav is simple to use and easy to extend. Some of these key technologies include:
The underlying architecture of Grav is designed to use well-established and _best-in-class_ technologies, to ensure that Grav is simple to use and easy to extend. Some of these key technologies include:
* [Twig Templating](http://twig.sensiolabs.org/): for powerful control of the user interface
* [Markdown](http://en.wikipedia.org/wiki/Markdown): for easy content creation

View File

@@ -21,15 +21,8 @@
"mrclay/minify": "dev-master",
"donatj/phpuseragentparser": "dev-master",
"pimple/pimple": "~3.0",
"rockettheme/toolbox": "dev-develop"
"rockettheme/toolbox": "1.0.*"
},
"repositories": [
{
"type": "vcs",
"no-api": true,
"url": "https://github.com/rockettheme/toolbox"
}
],
"autoload": {
"psr-4": {
"Grav\\": "system/src/Grav"

View File

@@ -31,7 +31,7 @@ pages:
'>': 'gt'
'<': 'lt'
types: 'txt|xml|html|json|rss|atom' # Pipe separated list of valid page types
expires: 604800 # Page expires time in seconds (default 7 days)
expires: 0 # Page expires time in seconds (604800 seconds = 7 days)
cache:
enabled: true # Set to true to enable caching
@@ -78,3 +78,6 @@ images:
media:
enable_media_timestamp: false # Enable media timetsamps
security:
default_hash: $2y$10$kwsyMVwM8/7j0K/6LHT.g.Fs49xOCTp2b8hh/S5.dPJuJcJB6T.UK

View File

@@ -2,7 +2,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '0.9.24');
define('GRAV_VERSION', '0.9.25');
define('DS', '/');
// Directories and Paths

View File

@@ -42,17 +42,16 @@ abstract class Folder
*/
public static function lastModifiedFile($path)
{
// pipe separated list of extensions to search for changes with
$extensions = 'md|yaml';
$last_modified = 0;
$dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
$filterItr = new RecursiveFileFilterIterator($dirItr);
$itr = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST);
$dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
$itrItr = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST);
$itr = new \RegexIterator($itrItr, '/^.+\.'.$extensions.'$/i');
/** @var \RecursiveDirectoryIterator $file */
foreach ($itr as $file) {
if ($file->isDir()) {
continue;
}
foreach ($itr as $filepath => $file) {
$file_modified = $file->getMTime();
if ($file_modified > $last_modified) {
$last_modified = $file_modified;

View File

@@ -1,13 +0,0 @@
<?php
namespace Grav\Common\Filesystem;
class RecursiveFileFilterIterator extends \RecursiveFilterIterator
{
public static $FILTERS = ['.DS_Store'];
public function accept()
{
// Ensure any filtered file names are skipped
return !in_array($this->current()->getFilename(), self::$FILTERS, true);
}
}

View File

@@ -296,9 +296,27 @@ class Grav extends Container
public function header()
{
$extension = $this['uri']->extension();
/** @var Page $page */
$page = $this['page'];
header('Content-type: ' . $this->mime($extension));
header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + $this['config']->get('system.pages.expires')));
// Calculate Expires Headers if set to > 0
$expires = $page->expires();
if ($expires > 0) {
$expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT';
header('Cache-Control: max-age=' . $expires_date);
header('Expires: '. $expires_date);
}
// Set the last modified time
$last_modified_date = gmdate('D, d M Y H:i:s', $page->modified()) . ' GMT';
header('Last-Modified: ' . $last_modified_date);
// Calculate a Hash based on the raw file
header('ETag: ' . md5($page->raw().$page->modified()));
// Set debugger data in headers
if (!($extension === null || $extension == 'html')) {

View File

@@ -5,9 +5,9 @@ class Parsedown extends \Parsedown
{
use ParsedownGravTrait;
public function __construct($page)
public function __construct($page, $defaults)
{
$this->init($page);
$this->init($page, $defaults);
}
}

View File

@@ -5,9 +5,9 @@ class ParsedownExtra extends \ParsedownExtra
{
use ParsedownGravTrait;
public function __construct($page)
public function __construct($page, $defaults)
{
parent::__construct();
$this->init($page);
$this->init($page, $defaults);
}
}

View File

@@ -6,6 +6,7 @@ use Grav\Common\Debugger;
use Grav\Common\GravTrait;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Uri;
use Grav\Common\Utils;
/**
* A trait to add some custom processing to the identifyLink() method in Parsedown and ParsedownExtra
@@ -25,8 +26,9 @@ trait ParsedownGravTrait
* Initialiazation function to setup key variables needed by the MarkdownGravLinkTrait
*
* @param $page
* @param $defaults
*/
protected function init($page)
protected function init($page, $defaults)
{
$this->page = $page;
$this->pages = self::getGrav()['pages'];
@@ -35,7 +37,9 @@ trait ParsedownGravTrait
$this->pages_dir = self::getGrav()['locator']->findResource('page://');
$this->special_chars = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
$defaults = self::getGrav()['config']->get('system.pages.markdown');
if ($defaults == null) {
$defaults = self::getGrav()['config']->get('system.pages.markdown');
}
$this->setBreaksEnabled($defaults['auto_line_breaks']);
$this->setUrlsLinked($defaults['auto_url_links']);
@@ -116,7 +120,6 @@ trait ParsedownGravTrait
// if this is an image
if (isset($excerpt['element']['attributes']['src'])) {
$alt = $excerpt['element']['attributes']['alt'] ?: '';
$title = $excerpt['element']['attributes']['title'] ?: '';
$class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : '';
@@ -124,10 +127,9 @@ trait ParsedownGravTrait
//get the url and parse it
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src']));
$path_parts = pathinfo($url['path']);
// if there is no host set but there is a path, the file is local
if (!isset($url['host']) && isset($url['path'])) {
$path_parts = pathinfo($url['path']);
// get the local path to page media if possible
if ($path_parts['dirname'] == $this->page->url()) {
@@ -136,7 +138,6 @@ trait ParsedownGravTrait
$media = $this->page->media();
} else {
// see if this is an external page to this one
$page_route = str_replace($this->base_url, '', $path_parts['dirname']);
@@ -219,37 +220,58 @@ trait ParsedownGravTrait
protected function convertUrl($markdown_url)
{
// if absolute and starts with a base_url move on
if ($this->base_url != '' && strpos($markdown_url, $this->base_url) === 0) {
if ($this->base_url != '' && Utils::startsWith($markdown_url, $this->base_url)) {
return $markdown_url;
// if contains only a fragment
} elseif (Utils::startsWith($markdown_url, '#')) {
return $markdown_url;
// if its absolute and starts with /
} elseif (strpos($markdown_url, '/') === 0) {
return $this->base_url . $markdown_url;
} else {
$relative_path = $this->base_url . $this->page->route();
$real_path = $this->page->path() . '/' . parse_url($markdown_url, PHP_URL_PATH);
// strip numeric order from markdown path
if (($real_path)) {
$markdown_url = preg_replace('/^([\d]+\.)/', '', preg_replace('/\/([\d]+\.)/', '/', trim(preg_replace('/[^\/]+(\.md$)/', '', $markdown_url), '/')));
$target = null;
// see if page is relative to this or absolute
if (Utils::startsWith($markdown_url, '/')) {
$normalized_path = Utils::normalizePath($this->pages_dir . $markdown_url);
$normalized_url = Utils::normalizePath($this->base_url . $markdown_url);
} else {
// contains path, so need to normalize it
if (Utils::contains($markdown_url, '/')) {
$normalized_path = Utils::normalizePath($this->page->path() . '/' . $markdown_url);
} else {
$normalized_path = false;
}
$normalized_url = $this->base_url . Utils::normalizePath($this->page->route() . '/' . $markdown_url);
}
// else its a relative path already
$newpath = array();
$paths = explode('/', $markdown_url);
// if this file exits, get the page and work with that
if ($normalized_path) {
$url_bits = parse_url($normalized_path);
$full_path = $url_bits['path'];
// remove the updirectory references (..)
foreach ($paths as $path) {
if ($path == '..') {
$relative_path = dirname($relative_path);
} else {
$newpath[] = $path;
if ($full_path && file_exists($full_path)) {
$path_info = pathinfo($full_path);
$page_path = $path_info['dirname'];
$filename = '';
// save the filename if a file is part of the path
$filename_regex = "/([\w\d-_]+\.([a-zA-Z]{2,4}))$/";
if (preg_match($filename_regex, $full_path, $matches)) {
if ($matches[2] != 'md') {
$filename = '/' . $matches[1];
}
} else {
$page_path = $full_path;
}
// get page instances and try to find one that fits
$instances = $this->pages->instances();
if (isset($instances[$page_path])) {
$target = $instances[$page_path];
$url_bits['path'] = $this->base_url . $target->route() . $filename;
return Uri::buildUrl($url_bits);
}
}
}
// build the new url
$new_url = rtrim($relative_path, '/') . '/' . implode('/', $newpath);
return $normalized_url;
}
return $new_url;
}
}

View File

@@ -361,12 +361,13 @@ class Collection extends Iterator
{
$routable = [];
foreach (array_keys($this->items) as $path => $slug) {
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
if ($page->routable()) {
$routable[$path] = $slug;
}
}
$this->items = $routable;
return $this;
}

View File

@@ -23,7 +23,7 @@ trait ParsedownHtmlTrait
$element = $this->parsedownElement($title, $alt, $class, $reset);
if (!$this->parsedown) {
$this->parsedown = new Parsedown(null);
$this->parsedown = new Parsedown(null, null);
}
return $this->parsedown->elementToHtml($element);

View File

@@ -47,6 +47,7 @@ class Page
protected $parent;
protected $template;
protected $expires;
protected $visible;
protected $published;
protected $publish_date;
@@ -273,6 +274,10 @@ class Page
if (isset($this->header->unpublish_date)) {
$this->unpublish_date = strtotime($this->header->unpublish_date);
}
if (isset($this->header->expires)) {
$this->expires = intval($this->header->expires);
}
}
return $this->header;
@@ -442,9 +447,9 @@ class Page
// Initialize the preferred variant of Parsedown
if ($defaults['extra']) {
$parsedown = new ParsedownExtra($this);
$parsedown = new ParsedownExtra($this, $defaults);
} else {
$parsedown = new Parsedown($this);
$parsedown = new Parsedown($this, $defaults);
}
$this->content = $parsedown->text($this->content);
@@ -781,6 +786,20 @@ class Page
return $this->template;
}
/**
* Gets and sets the expires field. If not set will return the default
*
* @param string $var The name of this page.
* @return string The name of this page.
*/
public function expires($var = null)
{
if ($var !== null) {
$this->expires = $var;
}
return empty($this->expires) ? self::getGrav()['config']->get('system.pages.expires') : $this->expires;
}
/**
* Gets and sets the title for this Page. If no title is set, it will use the slug() to get a name
*

View File

@@ -331,6 +331,28 @@ class Pages
return $blueprint;
}
/**
* Get all pages
*
* @param \Grav\Common\Page\Page $current
* @return \Grav\Common\Page\Collection
*/
public function all(Page $current = null)
{
$all = new Collection();
$current = $current ?: $this->root();
if ($current->routable()) {
$all[$current->path()] = [ 'slug' => $current->slug() ];
}
foreach ($current->children() as $next) {
$all->append($this->all($next));
}
return $all;
}
/**
* Get list of route/title of all pages.
*

View File

@@ -337,9 +337,9 @@ class TwigExtension extends \Twig_Extension
// Initialize the preferred variant of Parsedown
if ($defaults['extra']) {
$parsedown = new ParsedownExtra($page);
$parsedown = new ParsedownExtra($page, $defaults);
} else {
$parsedown = new Parsedown($page);
$parsedown = new Parsedown($page, $defaults);
}
$string = $parsedown->text($string);

View File

@@ -34,8 +34,7 @@ class Uri
$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')), '/');
$root_path = str_replace(' ', '%20', rtrim(substr($_SERVER['PHP_SELF'], 0, strpos($_SERVER['PHP_SELF'], 'index.php')), '/'));
if (isset($_SERVER['HTTPS'])) {
$base = (@$_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';

View File

@@ -13,11 +13,22 @@ abstract class Authentication
* Create password hash from plaintext password.
*
* @param string $password Plaintext password.
* @throws \RuntimeException
* @return string|bool
*/
public static function create($password)
{
return password_hash($password, PASSWORD_DEFAULT);
if (!$password) {
throw new \RuntimeException('Password hashing failed: no password provided.');
}
$hash = password_hash($password, PASSWORD_DEFAULT);
if (!$hash) {
throw new \RuntimeException('Password hashing failed: internal error.');
}
return $hash;
}
/**
@@ -29,13 +40,8 @@ abstract class Authentication
*/
public static function verify($password, $hash)
{
// Always accept plaintext passwords (needs an update).
if ($password && $password == $hash) {
return 2;
}
// Fail if hash doesn't match.
if (!$password || !password_verify($password, $hash)) {
// Fail if hash doesn't match
if (!$password || !$hash || !password_verify($password, $hash)) {
return 0;
}

View File

@@ -53,11 +53,36 @@ class User extends Data
*/
public function authenticate($password)
{
$result = Authentication::verify($password, $this->password);
$save = false;
// Plain-text is still stored
if ($this->password) {
if ($password !== $this->password) {
// Plain-text passwords do not match, we know we should fail but execute
// verify to protect us from timing attacks and return false regardless of
// the result
Authentication::verify($password, self::getGrav()['config']->get('system.security.default_hash'));
return false;
} else {
// Plain-text does match, we can update the hash and proceed
$save = true;
$this->hashed_password = Authentication::create($this->password);
unset($this->password);
}
}
$result = Authentication::verify($password, $this->hashed_password);
// Password needs to be updated, save the file.
if ($result == 2) {
$this->password = Authentication::create($password);
$save = true;
$this->hashed_password = Authentication::create($password);
}
if ($save) {
$this->save();
}

View File

@@ -295,7 +295,6 @@ abstract class Utils
* Return the mimetype based on filename
*
* @param $extension Extension of file (eg .txt)
*
* @return string
*/
public static function getMimeType($extension)
@@ -396,4 +395,29 @@ abstract class Utils
return "application/octet-stream";
}
}
/**
* Normalize path by processing relative `.` and `..` syntax and merging path
*
* @param $path
* @return string
*/
public static function normalizePath($path)
{
$root = ($path[0] === '/') ? '/' : '';
$segments = explode('/', trim($path, '/'));
$ret = array();
foreach ($segments as $segment) {
if (($segment == '.') || empty($segment)) {
continue;
}
if ($segment == '..') {
array_pop($ret);
} else {
array_push($ret, $segment);
}
}
return $root . implode('/', $ret);
}
}

View File

@@ -125,7 +125,7 @@ class InstallCommand extends Command
foreach ($this->config['git'] as $repo => $data) {
$path = $this->destination . DS . $data['path'];
if (!file_exists($path)) {
exec('cd ' . $this->destination . ' && git clone -b ' . $data['branch'] . ' ' . $data['url'] . ' ' . $data['path']);
exec('cd "' . $this->destination . '" && git clone -b ' . $data['branch'] . ' ' . $data['url'] . ' ' . $data['path']);
$output->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>');
$output->writeln('');
} else {