Merge branch 'release/0.9.14'

This commit is contained in:
Andy Miller
2015-01-23 13:08:47 -07:00
31 changed files with 643 additions and 273 deletions

View File

@@ -2,6 +2,17 @@
RewriteEngine On
## Begin RewriteBase
# If you are getting 404 errors on subpages, you may have to uncomment the RewriteBase entry
# You should change the '/' to your appropriate subfolder. For example if you have
# your Grav install at the root of your site '/' should work, else it might be something
# along the lines of: RewriteBase /<your_sub_folder>
##
# RewriteBase /
## End - RewriteBase
## Begin - Exploits
# If you experience problems on your site block out the operations listed below
# This attempts to block the most common type of exploit `attempts` to Grav
@@ -19,17 +30,6 @@ RewriteRule .* index.php [F]
#
## End - Exploits
## Begin RewriteBase
# If you are getting 404 errors on subpages, you may have to uncomment the RewriteBase entry
# You should change the '/' to your appropriate subfolder. For example if you have
# your Grav install at the root of your site '/' should work, else it might be something
# along the lines of: RewriteBase /<your_sub_folder>
##
# RewriteBase /
## End - RewriteBase
## Begin - Index
# If the requested path and file is not /index.php and the request
# has not already been internally rewritten to the index.php script

View File

@@ -1,3 +1,30 @@
# v0.9.14
## 01/23/2015
1. [](#new)
* Added **GZip** support
* Added multiple configurations via `setup.php`
* Added base structure for unit tests
* New `onPageContentRaw()` plugin event that processes before any page processing
* Added ability to dynamically set Metadata on page
* Added ability to dynamically configure Markdown processing via Parsedown options
2. [](#improved)
* Refactored `page.content()` method to be more flexible and reliable
* Various updates and fixes for streams resulting in better multi-site support
* Updated Twig, Parsedown, ParsedownExtra, DoctrineCache libraries
* Refactored Parsedown trait
* Force modular pages to be non-visible in menus
* Moved RewriteBase before Exploits in `.htaccess`
* Added standard video formats to Media support
* Added priority for inline assets
* Check for uniqueness when adding multiple inline assets
* Improved support for Twig-based URLs inside Markdown links and images
* Improved Twig `url()` function
3. [](#bugfix)
* Fix for HTML entities quotes in Metadata values
* Fix for `published` setting to have precedent of `publish_date` and `unpublish_date`
* Fix for `onShutdown()` events not closing connections properly in **php-fpm** environments
# v0.9.13
## 01/09/2015
@@ -15,7 +42,7 @@
* House-cleaning of some unused methods in Pages object
3. [](#bugfix)
* Fix `uninstall` GPM command that was broken in last release
* Fix for intermitten `undefined index` error when working with Collections
* Fix for intermittent `undefined index` error when working with Collections
* Fix for date of some pages being set to incorrect future timestamps
# v0.9.12
@@ -27,8 +54,8 @@
* Added support for **in-page** Twig processing in **modular** pages
* Added configurable support for `undefined` Twig functions and filters
2. [](#improved)
* Fallback to default `.html` template if error occurs on non-html pages
* Added ability to have PSR-1 friendly plugin names (camelcase, no-dashes)
* Fall back to default `.html` template if error occurs on non-html pages
* Added ability to have PSR-1 friendly plugin names (CamelCase, no-dashes)
* Fix to `composer.json` to deter API rate-limit errors
* Added **non-exception-throwing** handler for undefined methods on `Medium` objects
3. [](#bugfix)

Binary file not shown.

View File

@@ -8,7 +8,7 @@
"require": {
"php": ">=5.4.0",
"twig/twig": "~1.16",
"erusev/parsedown-extra": "dev-master",
"erusev/parsedown-extra": "~0.6",
"symfony/yaml": "~2.6",
"symfony/console": "~2.6",
"symfony/event-dispatcher": "~2.6",

View File

@@ -2,6 +2,17 @@
RewriteEngine On
## Begin RewriteBase
# If you are getting 404 errors on subpages, you may have to uncomment the RewriteBase entry
# You should change the '/' to your appropriate subfolder. For example if you have
# your Grav install at the root of your site '/' should work, else it might be something
# along the lines of: RewriteBase /<your_sub_folder>
##
# RewriteBase /
## End - RewriteBase
## Begin - Exploits
# If you experience problems on your site block out the operations listed below
# This attempts to block the most common type of exploit `attempts` to Grav
@@ -19,17 +30,6 @@ RewriteRule .* index.php [F]
#
## End - Exploits
## Begin RewriteBase
# If you are getting 404 errors on subpages, you may have to uncomment the RewriteBase entry
# You should change the '/' to your appropriate subfolder. For example if you have
# your Grav install at the root of your site '/' should work, else it might be something
# along the lines of: RewriteBase /<your_sub_folder>
##
# RewriteBase /
## End - RewriteBase
## Begin - Index
# If the requested path and file is not /index.php and the request
# has not already been internally rewritten to the index.php script

View File

@@ -40,6 +40,31 @@ swf:
type: video
thumb: media/thumb-swf.png
mime: video/x-flv
flv:
type: video
thumb: media/thumb-flv.png
mime: video/x-flv
mp3:
type: audio
thumb: media/thumb-mp3.png
mime: audio/mp3
ogg:
type: audio
thumb: media/thumb-ogg.png
mine: audio/ogg
wma:
type: audio
thumb: media/thumb-wma.png
mine: audio/wma
m4a:
type: audio
thumb: media/thumb-m4a.png
mine: audio/m4a
wav:
type: audio
thumb: media/thumb-wav.png
mine: audio/wav
txt:
type: file

View File

@@ -5,7 +5,6 @@ home:
pages:
theme: antimatter # Default theme (defaults to "antimatter" theme)
markdown_extra: false # Enable support for Markdown Extra support (GFM by default)
order:
by: defaults # Order pages by "default", "alpha" or "date"
dir: asc # Default ordering direction, "asc" or "desc"
@@ -21,6 +20,14 @@ pages:
events:
page: true # Enable page level events
twig: true # Enable twig level events
markdown:
extra: false # Enable support for Markdown Extra support (GFM by default)
auto_line_breaks: false # Enable automatic line breaks
auto_url_links: false # Enable automatic HTML links
escape_markup: false # Escape markup tags into entities
special_chars: # List of special characters to automatically convert to entities
'>': 'gt'
'<': 'lt'
cache:
enabled: true # Set to true to enable caching
@@ -29,6 +36,7 @@ cache:
driver: auto # One of: auto|file|apc|xcache|memcache|wincache
prefix: 'g' # Cache prefix string (prevents cache conflicts)
lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite)
gzip: false # GZip compress the page output
twig:
cache: true # Set to true to enable twig caching

View File

@@ -2,7 +2,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '0.9.13');
define('GRAV_VERSION', '0.9.14');
define('DS', '/');
// Directories and Paths
@@ -17,14 +17,16 @@ define('ASSETS_DIR', ROOT_DIR . 'assets/');
define('CACHE_DIR', ROOT_DIR . 'cache/');
define('IMAGES_DIR', ROOT_DIR . 'images/');
define('LOG_DIR', ROOT_DIR .'logs/');
define('VENDOR_DIR', ROOT_DIR .'vendor/');
define('LIB_DIR', SYSTEM_DIR .'src/');
define('ACCOUNTS_DIR', USER_DIR .'accounts/');
define('DATA_DIR', USER_DIR .'data/');
define('PAGES_DIR', USER_DIR .'pages/');
// DEPRECATED: Do not use!
define('DATA_DIR', USER_DIR .'data/');
define('LIB_DIR', SYSTEM_DIR .'src/');
define('PLUGINS_DIR', USER_DIR .'plugins/');
define('THEMES_DIR', USER_DIR .'themes/');
define('VENDOR_DIR', ROOT_DIR .'vendor/');
// END DEPRECATED
// Some extensions
define('CONTENT_EXT', '.md');

View File

@@ -234,8 +234,9 @@ class Assets
$asset = $this->buildLocalLink($asset);
}
if ($asset && !array_key_exists($asset, $this->css)) {
$this->css[$asset] = [
$key = md5($asset);
if ($asset && !array_key_exists($key, $this->css)) {
$this->css[$key] = [
'asset' => $asset,
'priority' => $priority,
'order' => count($this->css),
@@ -272,8 +273,9 @@ class Assets
$asset = $this->buildLocalLink($asset);
}
if ($asset && !array_key_exists($asset, $this->js)) {
$this->js[$asset] = [
$key = md5($asset);
if ($asset && !array_key_exists($key, $this->js)) {
$this->js[$key] = [
'asset' => $asset,
'priority' => $priority,
'order' => count($this->js),
@@ -297,9 +299,13 @@ class Assets
*/
public function addInlineCss($asset, $priority = 10)
{
if (is_string($asset) && !in_array($asset, $this->inline_css)) {
$this->inline_css[] = $asset;
$key = md5($asset);
if (is_string($asset) && !array_key_exists($key, $this->inline_css)) {
$this->inline_css[$key] = [
'priority' => $priority,
'order' => count($this->inline_css),
'asset' => $asset
];
}
return $this;
@@ -312,14 +318,19 @@ class Assets
* For adding chunks of string-based inline JS
*
* @param mixed $asset
* @param int $priority the priority, bigger comes first
*
* @return $this
*/
public function addInlineJs($asset)
public function addInlineJs($asset, $priority = 10)
{
if (is_string($asset) && !in_array($asset, $this->inline_js)) {
$this->inline_js[] = $asset;
$key = md5($asset);
if (is_string($asset) && !array_key_exists($key, $this->inline_js)) {
$this->inline_js[$key] = [
'priority' => $priority,
'order' => count($this->inline_js),
'asset' => $asset
];
}
return $this;
@@ -346,8 +357,16 @@ class Assets
}
return $a['priority'] - $b['priority'];
});
usort($this->inline_css, function ($a, $b) {
if ($a['priority'] == $b['priority']) {
return $b['order'] - $a['order'];
}
return $a['priority'] - $b['priority'];
});
}
$this->css = array_reverse($this->css);
$this->inline_css = array_reverse($this->inline_css);
$attributes = $this->attributes(array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes));
@@ -368,7 +387,7 @@ class Assets
if (count($this->inline_css) > 0) {
$output .= "<style>\n";
foreach ($this->inline_css as $inline) {
$output .= $inline . "\n";
$output .= $inline['asset'] . "\n";
}
$output .= "</style>\n";
}
@@ -397,7 +416,16 @@ class Assets
}
return $a['priority'] - $b['priority'];
});
usort($this->inline_js, function ($a, $b) {
if ($a['priority'] == $b['priority']) {
return $b['order'] - $a['order'];
}
return $a['priority'] - $b['priority'];
});
$this->js = array_reverse($this->js);
$this->inline_js = array_reverse($this->inline_js);
$attributes = $this->attributes(array_merge(['type' => 'text/javascript'], $attributes));
@@ -417,7 +445,7 @@ class Assets
if (count($this->inline_js) > 0) {
$output .= "<script>\n";
foreach ($this->inline_js as $inline) {
$output .= $inline . "\n";
$output .= $inline['asset'] . "\n";
}
$output .= "</script>\n";
}

View File

@@ -168,6 +168,8 @@ class Config extends Data
$this->loadCompiledBlueprints($this->blueprintLookup, $this->pluginLookup, 'master');
$this->loadCompiledConfig($this->configLookup, $this->pluginLookup, 'master');
$this->initializeLocator($locator);
}
public function checksum()
@@ -331,4 +333,47 @@ class Config extends Data
$this->join($name, $file->content(), '/');
}
}
/**
* Initialize resource locator by using the configuration.
*
* @param UniformResourceLocator $locator
*/
public function initializeLocator(UniformResourceLocator $locator)
{
$locator->reset();
$schemes = (array) $this->get('streams.schemes', []);
foreach ($schemes as $scheme => $config) {
if (isset($config['paths'])) {
$locator->addPath($scheme, '', $config['paths']);
}
if (isset($config['prefixes'])) {
foreach ($config['prefixes'] as $prefix => $paths) {
$locator->addPath($scheme, $prefix, $paths);
}
}
}
}
/**
* Get available streams and their types from the configuration.
*
* @return array
*/
public function getStreams()
{
$schemes = [];
foreach ((array) $this->get('streams.schemes') as $scheme => $config) {
$type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
if ($type[0] != '\\') {
$type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
}
$schemes[$scheme] = $type;
}
return $schemes;
}
}

View File

@@ -33,17 +33,41 @@ abstract class Folder
return $last_modified;
}
public static function getRelativePath($to, $from = ROOT_DIR)
/**
* Get relative path between target and base path. If path isn't relative, return full path.
*
* @param string $path
* @param string $base
* @return string
*/
public static function getRelativePath($path, $base = GRAV_ROOT)
{
$from = preg_replace('![\\|/]+!', '/', $from);
$to = preg_replace('![\\|/]+!', '/', $to);
if (strpos($to, $from) === 0) {
$to = substr($to, strlen($from));
if ($base) {
$base = preg_replace('![\\|/]+!', '/', $base);
$path = preg_replace('![\\|/]+!', '/', $path);
if (strpos($path, $base) === 0) {
$path = ltrim(substr($path, strlen($base)), '/');
}
}
return $to;
return $path;
}
/**
* Shift first directory out of the path.
*
* @param string $path
* @return string
*/
public static function shift(&$path)
{
$parts = explode('/', trim($path, '/'), 2);
$result = array_shift($parts);
$path = array_shift($parts);
return $result ?: null;
}
/**
* Recursively find the last modified time under given path by file.
*
@@ -208,8 +232,9 @@ abstract class Folder
/**
* Recursively delete directory from filesystem.
*
* @param string $target
* @param string $target
* @throws \RuntimeException
* @return bool
*/
public static function delete($target)
{

View File

@@ -1,6 +1,7 @@
<?php
namespace Grav\Common;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Page\Pages;
use Grav\Common\Service\ConfigServiceProvider;
use Grav\Common\Service\ErrorServiceProvider;
@@ -97,13 +98,18 @@ class Grav extends Container
$container['page'] = function ($c) {
/** @var Pages $pages */
$pages = $c['pages'];
$page = $pages->dispatch($c['uri']->route());
// If base URI is set, we want to remove it from the URL.
$path = '/' . ltrim(Folder::getRelativePath($c['uri']->route(), $pages->base()), '/');
$page = $pages->dispatch($path);
if (!$page || !$page->routable()) {
// special case where a media file is requested
if (!$page) {
$path_parts = pathinfo($c['uri']->route());
$path_parts = pathinfo($path);
$page = $c['pages']->dispatch($path_parts['dirname']);
if ($page) {
$media = $page->media()->all();
@@ -164,6 +170,10 @@ class Grav extends Container
{
// Use output buffering to prevent headers from being sent too early.
ob_start();
if ($this['config']->get('system.cache.gzip')) {
ob_start('ob_gzhandler');
}
/** @var Debugger $debugger */
$debugger = $this['debugger'];
@@ -319,12 +329,21 @@ class Grav extends Container
$this['session']->close();
}
header('Content-length: ' . ob_get_length());
if ($this['config']->get('system.cache.gzip')) {
ob_end_flush(); // gzhandler buffer
}
header('Content-Length: ' . ob_get_length());
header("Connection: close\r\n");
ob_end_flush();
ob_end_flush(); // regular buffer
ob_flush();
flush();
if (function_exists('fastcgi_finish_request')) {
@fastcgi_finish_request();
}
}
$this->fireEvent('onShutdown');

View File

@@ -1,14 +0,0 @@
<?php
namespace Grav\Common\Markdown;
class Markdown extends \Parsedown
{
use MarkdownGravLinkTrait;
public function __construct($page)
{
$this->page = $page;
$this->BlockTypes['{'] [] = "TwigTag";
}
}

View File

@@ -1,14 +0,0 @@
<?php
namespace Grav\Common\Markdown;
class MarkdownExtra extends \ParsedownExtra
{
use MarkdownGravLinkTrait;
public function __construct($page)
{
parent::__construct();
$this->page = $page;
$this->BlockTypes['{'] [] = "TwigTag";
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Grav\Common\Markdown;
class Parsedown extends \Parsedown
{
use ParsedownGravTrait;
public function __construct($page)
{
$this->init($page);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Grav\Common\Markdown;
class ParsedownExtra extends \ParsedownExtra
{
use ParsedownGravTrait;
public function __construct($page)
{
parent::__construct();
$this->init($page);
}
}

View File

@@ -10,60 +10,109 @@ use Grav\Common\Uri;
/**
* A trait to add some custom processing to the identifyLink() method in Parsedown and ParsedownExtra
*/
trait MarkdownGravLinkTrait
trait ParsedownGravTrait
{
use GravTrait;
protected $page;
protected $base_url;
protected $pages_dir;
protected $special_chars;
protected $twig_link_regex = '/\!*\[(?:.*)\]\(([{{|{%|{#].*[#}|%}|}}])\)/';
/**
* Initialiazation function to setup key variables needed by the MarkdownGravLinkTrait
*
* @param $page
*/
protected function init($page)
{
$this->page = $page;
$this->BlockTypes['{'] [] = "TwigTag";
$this->base_url = rtrim(self::$grav['base_url'] . self::$grav['pages']->base(), '/');
$this->pages_dir = self::$grav['locator']->findResource('page://');
$this->special_chars = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
}
/**
* Setter for special chars
*
* @param $special_chars
*
* @return $this
*/
function setSpecialChars($special_chars)
{
$this->special_chars = $special_chars;
return $this;
}
/**
* Ensure Twig tags are treated as block level items with no <p></p> tags
*/
protected function identifyTwigTag($Line)
protected function blockTwigTag($Line)
{
if (preg_match('/[{%|{{|{#].*[#}|}}|%}]/', $Line['body'], $matches)) {
$Block = array(
'element' => $Line['body'],
'markup' => $Line['body'],
);
return $Block;
}
}
protected function identifyLink($Excerpt)
protected function inlineSpecialCharacter($Excerpt)
{
/** @var Config $config */
$config = self::$grav['config'];
// Run the parent method to get the actual results
$Excerpt = parent::identifyLink($Excerpt);
$actions = array();
$this->base_url = self::$grav['base_url'];
// if this is a link
if (isset($Excerpt['element']['attributes']['href'])) {
$url = parse_url(htmlspecialchars_decode($Excerpt['element']['attributes']['href']));
// if there is no scheme, the file is local
if (!isset($url['scheme'])) {
// convert the URl is required
$Excerpt['element']['attributes']['href'] = $this->convertUrl(Uri::build_url($url));
}
if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) {
return array(
'markup' => '&amp;',
'extent' => 1,
);
}
// if this is an image
if (isset($Excerpt['element']['attributes']['src'])) {
if (isset($this->special_chars[$Excerpt['text'][0]])) {
return array(
'markup' => '&'.$this->special_chars[$Excerpt['text'][0]].';',
'extent' => 1,
);
}
}
$alt = isset($Excerpt['element']['attributes']['alt']) ? $Excerpt['element']['attributes']['alt'] : '';
$title = isset($Excerpt['element']['attributes']['title']) ? $Excerpt['element']['attributes']['title'] : '';
protected function inlineImage($excerpt)
{
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
$excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
$excerpt = parent::inlineImage($excerpt);
$excerpt['element']['attributes']['src'] = $matches[1];
$excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1;
return $excerpt;
} else {
$excerpt = parent::inlineImage($excerpt);
}
$actions = array();
// if this is an image
if (isset($excerpt['element']['attributes']['src'])) {
$alt = $excerpt['element']['attributes']['alt'] ?: '';
$title = $excerpt['element']['attributes']['title'] ?: '';
//get the url and parse it
$url = parse_url(htmlspecialchars_decode($Excerpt['element']['attributes']['src']));
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src']));
//get back to current page if possible
// if there is no host set but there is a path, the file is local
if (!isset($url['host']) && isset($url['path'])) {
// get the media objects for this page
$media = $this->page->media();
// get the local path to page media if possible
if (strpos($url['path'], $this->page->url()) !== false) {
$url['path'] = ltrim(str_replace($this->page->url(), '', $url['path']), '/');
}
// if there is a media file that matches the path referenced..
if (isset($media->images()[$url['path']])) {
// get the medium object
@@ -92,10 +141,10 @@ trait MarkdownGravLinkTrait
// set the src element with the new generated url
if (!isset($actions['lightbox']) && !is_array($src)) {
$Excerpt['element']['attributes']['src'] = $src;
$excerpt['element']['attributes']['src'] = $src;
} else {
// Create the custom lightbox element
$Element = array(
$element = array(
'name' => 'a',
'attributes' => array('rel' => $src['a_rel'], 'href' => $src['a_url']),
'handler' => 'element',
@@ -106,20 +155,50 @@ trait MarkdownGravLinkTrait
);
// Set any custom classes on the lightbox element
if (isset($Excerpt['element']['attributes']['class'])) {
$Element['attributes']['class'] = $Excerpt['element']['attributes']['class'];
if (isset($excerpt['element']['attributes']['class'])) {
$element['attributes']['class'] = $excerpt['element']['attributes']['class'];
}
// Set the lightbox element on the Excerpt
$Excerpt['element'] = $Element;
$excerpt['element'] = $element;
}
} else {
// not a current page media file, see if it needs converting to relative
$Excerpt['element']['attributes']['src'] = $this->convertUrl(Uri::build_url($url));
$excerpt['element']['attributes']['src'] = $this->convertUrl(Uri::build_url($url));
}
}
}
return $Excerpt;
return $excerpt;
}
protected function inlineLink($excerpt)
{
// do some trickery to get around Parsedown requirement for valid URL if its Twig in there
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
$excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
$excerpt = parent::inlineLink($excerpt);
$excerpt['element']['attributes']['href'] = $matches[1];
$excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1;
return $excerpt;
} else {
$excerpt = parent::inlineLink($excerpt);
}
// if this is a link
if (isset($excerpt['element']['attributes']['href'])) {
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['href']));
// if there is no scheme, the file is local
if (!isset($url['scheme'])) {
// convert the URl is required
$excerpt['element']['attributes']['href'] = $this->convertUrl(Uri::build_url($url));
}
}
return $excerpt;
}
/**
@@ -129,18 +208,18 @@ trait MarkdownGravLinkTrait
*/
protected function convertUrl($markdown_url)
{
// if absolue and starts with a base_url move on
// if absolute and starts with a base_url move on
if ($this->base_url != '' && strpos($markdown_url, $this->base_url) === 0) {
$new_url = $markdown_url;
// if its absolute with /
// if its absolute and starts with /
} elseif (strpos($markdown_url, '/') === 0) {
$new_url = rtrim($this->base_url, '/') . $markdown_url;
$new_url = $this->base_url . $markdown_url;
} else {
$relative_path = rtrim($this->base_url, '/') . $this->page->route();
$relative_path = $this->base_url . $this->page->route();
// If this is a 'real' filepath clean it up
if (file_exists($this->page->path().'/'.parse_url($markdown_url, PHP_URL_PATH))) {
$relative_path = rtrim($this->base_url, '/') . preg_replace('/\/([\d]+.)/', '/', str_replace(PAGES_DIR, '/', $this->page->path()));
if (file_exists($this->page->path() . '/' . parse_url($markdown_url, PHP_URL_PATH))) {
$relative_path = $this->base_url . preg_replace('/\/([\d]+.)/', '/', str_replace($this->pages_dir, '', $this->page->path()));
$markdown_url = preg_replace('/^([\d]+.)/', '', preg_replace('/\/([\d]+.)/', '/', trim(preg_replace('/[^\/]+(\.md$)/', '', $markdown_url), '/')));
}

View File

@@ -226,15 +226,15 @@ class Collection extends Iterator
$start = strtotime($startDate);
$end = $endDate ? strtotime($endDate) : strtotime("now +1000 years");
$daterange = [];
$date_range = [];
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
if ($page->date() > $start && $page->date() < $end) {
$daterange[$path] = $slug;
$date_range[$path] = $slug;
}
}
$this->items = $daterange;
$this->items = $date_range;
return $this;
}

View File

@@ -23,6 +23,7 @@ class Media extends Getters
protected $instances = array();
protected $images = array();
protected $videos = array();
protected $audios = array();
protected $files = array();
/**
@@ -155,6 +156,17 @@ class Media extends Getters
return $this->videos;
}
/**
* Get a list of all audio media.
*
* @return array|Medium[]
*/
public function audios()
{
ksort($this->audios, SORT_NATURAL | SORT_FLAG_CASE);
return $this->audios;
}
/**
* Get a list of all file media.
*
@@ -179,6 +191,9 @@ class Media extends Getters
case 'video':
$this->videos[$file->filename] = $file;
break;
case 'audio':
$this->audios[$file->filename] = $file;
break;
default:
$this->files[$file->filename] = $file;
}

View File

@@ -10,8 +10,8 @@ use Grav\Common\Twig;
use Grav\Common\Uri;
use Grav\Common\Grav;
use Grav\Common\Taxonomy;
use Grav\Common\Markdown\Markdown;
use Grav\Common\Markdown\MarkdownExtra;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Data\Blueprint;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile;
@@ -122,11 +122,9 @@ class Page
$this->visible();
$this->modularTwig($this->slug[0] == '_');
// Handle publishing dates
$config = self::$grav['config'];
if ($config->get('system.pages.publish_dates')) {
// Handle publishing dates if no explict published option set
if (self::$grav['config']->get('system.pages.publish_dates') && !isset($this->header->published)) {
// unpublish if required, if not clear cache right before page should be unpublished
if ($this->unpublishDate()) {
if ($this->unpublishDate() < time()) {
$this->published(false);
@@ -135,14 +133,13 @@ class Page
self::$grav['cache']->setLifeTime($this->unpublishDate());
}
}
// publish if required, if not clear cache right before page is published
if ($this->publishDate() != $this->modified() && $this->publishDate() > time()) {
$this->published(false);
self::$grav['cache']->setLifeTime($this->publishDate());
}
}
$this->published();
}
/**
@@ -349,38 +346,50 @@ class Page
$cache_id = md5('page'.$this->id());
$this->content = $cache->fetch($cache_id);
$update_cache = false;
if ($this->content === false) {
// Process Markdown
$this->content = $this->processMarkdown();
$update_cache = true;
$process_markdown = $this->shouldProcess('markdown');
$process_twig = $this->shouldProcess('twig');
$cache_twig = isset($this->header->cache_enable) ? $this->header->cache_enable : true;
$twig_first = isset($this->header->twig_first) ? $this->header->twig_first : false;
$twig_already_processed = false;
// if no cached-content run everything
if ($this->content == false) {
$this->content = $this->raw_content;
self::$grav->fireEvent('onPageContentRaw', new Event(['page' => $this]));
if ($twig_first) {
if ($process_twig) {
$this->processTwig();
$twig_already_processed = true;
}
if ($process_markdown) {
$this->processMarkdown();
}
if ($cache_twig) {
$this->cachePageContent();
}
} else {
if ($process_markdown) {
$this->processMarkdown();
}
if (!$cache_twig) {
$this->cachePageContent();
}
if ($process_twig) {
$this->processTwig();
$twig_already_processed = true;
}
if ($cache_twig) {
$this->cachePageContent();
}
}
// content cached, but twig cache off
}
// Process Twig if enabled
if ($this->shouldProcess('twig')) {
// Always process twig if caching in the page is disabled
$process_twig = (isset($this->header->cache_enable) && !$this->header->cache_enable);
// Do we want to cache markdown, but process twig in each page?
if ($update_cache && $process_twig) {
$cache->save($cache_id, $this->content);
$update_cache = false;
}
// Do we need to process twig this time?
if ($update_cache || $process_twig) {
/** @var Twig $twig */
$twig = self::$grav['twig'];
$this->content = $twig->processPage($this, $this->content);
}
}
// Cache the whole page, including processed content
if ($update_cache) {
// Process any post-processing but pre-caching functionality
self::$grav->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
$cache->save($cache_id, $this->content);
// only markdown content cached, process twig if required and not already processed
if ($process_twig && !$cache_twig && !$twig_already_processed) {
$this->processTwig();
}
// Handle summary divider
@@ -395,6 +404,61 @@ class Page
return $this->content;
}
/**
* Process the Markdown content. Uses Parsedown or Parsedown Extra depending on configuration
*/
protected function processMarkdown()
{
/** @var Config $config */
$config = self::$grav['config'];
$defaults = (array) $config->get('system.pages.markdown');
if (isset($this->header()->markdown)) {
$defaults = array_merge($defaults, $this->header()->markdown);
}
// pages.markdown_extra is deprecated, but still check it...
if (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null) {
$defaults['extra'] = $this->markdown_extra;
}
// Initialize the preferred variant of Parsedown
if ($defaults['extra']) {
$parsedown = new ParsedownExtra($this);
} else {
$parsedown = new Parsedown($this);
}
$parsedown->setBreaksEnabled($defaults['auto_line_breaks']);
$parsedown->setUrlsLinked($defaults['auto_url_links']);
$parsedown->setMarkupEscaped($defaults['escape_markup']);
$parsedown->setSpecialChars($defaults['special_chars']);
$this->content = $parsedown->text($this->content);
}
/**
* Process the Twig page content.
*/
private function processTwig()
{
$twig = self::$grav['twig'];
$this->content = $twig->processPage($this, $this->content);
}
/**
* Fires the onPageContentProcessed event, and caches the page content using a unique ID for the page
*/
private function cachePageContent()
{
$cache = self::$grav['cache'];
$cache_id = md5('page'.$this->id());
self::$grav->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
$cache->save($cache_id, $this->content);
}
/**
* Needed by the onPageContentProcessed event to get the raw page content
*
@@ -453,6 +517,9 @@ class Page
if ($name == 'media.image') {
return $this->media()->images();
}
if ($name == 'media.audio') {
return $this->media()->audios();
}
$path = explode('.', $name);
$scope = array_shift($path);
@@ -848,9 +915,16 @@ class Page
/**
* Function to merge page metadata tags and build an array of Metadata objects
* that can then be rendered in the page.
*
* @param array $var an Array of metadata values to set
* @return array an Array of metadata values for the page
*/
public function metadata()
public function metadata($var = null)
{
if ($var !== null) {
$this->metadata = (array) $var;
}
// if not metadata yet, process it.
if (null === $this->metadata) {
@@ -879,14 +953,14 @@ class Page
if (is_array($value)) {
foreach ($value as $property => $prop_value) {
$prop_key = $key.":".$property;
$this->metadata[$prop_key] = array('property'=>$prop_key, 'content'=>$prop_value);
$this->metadata[$prop_key] = array('property'=>$prop_key, 'content'=>htmlspecialchars($prop_value, ENT_QUOTES));
}
// If it this is a standard meta data type
} else {
if (in_array($key, $header_tag_http_equivs)) {
$this->metadata[$key] = array('http_equiv'=>$key, 'content'=>$value);
$this->metadata[$key] = array('http_equiv'=>$key, 'content'=>htmlspecialchars($value, ENT_QUOTES));
} else {
$this->metadata[$key] = array('name'=>$key, 'content'=>$value);
$this->metadata[$key] = array('name'=>$key, 'content'=>htmlspecialchars($value, ENT_QUOTES));
}
}
}
@@ -966,9 +1040,13 @@ class Page
*/
public function url($include_host = false)
{
/** @var Pages $pages */
$pages = self::$grav['pages'];
/** @var Uri $uri */
$uri = self::$grav['uri'];
$rootUrl = $uri->rootUrl($include_host);
$rootUrl = $uri->rootUrl($include_host) . $pages->base();
$url = $rootUrl.'/'.trim($this->route(), '/');
// trim trailing / if not root
@@ -1209,6 +1287,7 @@ class Page
$this->modular_twig = (bool) $var;
if ($var) {
$this->process['twig'] = true;
$this->visible(false);
}
}
return $this->modular_twig;
@@ -1382,14 +1461,16 @@ class Page
* Helper method to return a page.
*
* @param string $url the url of the page
* @return Page page you were looking for if it exists
* @param bool $all
*
* @return \Grav\Common\Page\Page page you were looking for if it exists
* @deprecated
*/
public function find($url)
public function find($url, $all = false)
{
/** @var Pages $pages */
$pages = self::$grav['pages'];
return $pages->dispatch($url);
return $pages->dispatch($url, $all);
}
/**
@@ -1585,53 +1666,6 @@ class Page
return $file && $file->exists();
}
/**
* Process the Markdown if processing is enabled for it. If not, process as 'raw' which simply strips the
* header YAML from the raw, and sends back the content portion. i.e. the bit below the header.
*
* @return string the content for the page
*/
protected function processMarkdown()
{
// Process Markdown if required
$process_method = $this->shouldProcess('markdown') ? 'parseMarkdownContent' : 'rawContent';
$content = $this->$process_method($this->raw_content);
return $content;
}
/**
* Process the raw content. Basically just strips the headers out and returns the rest.
*
* @param string $content Input raw content
* @return string Output content after headers have been stripped
*/
protected function rawContent($content)
{
return $content;
}
/**
* Process the Markdown content. This strips the headers, the process the resulting content as Markdown.
*
* @param string $content Input raw content
* @return string Output content that has been processed as Markdown
*/
protected function parseMarkdownContent($content)
{
/** @var Config $config */
$config = self::$grav['config'];
// get the appropriate setting for markdown extra
if (isset($this->markdown_extra) ? $this->markdown_extra : $config->get('system.pages.markdown_extra')) {
$parsedown = new MarkdownExtra($this);
} else {
$parsedown = new Markdown($this);
}
$content = $parsedown->text($content);
return $content;
}
/**
* Cleans the path.
*

View File

@@ -10,6 +10,7 @@ use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Blueprints;
use Grav\Common\Filesystem\Folder;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
* GravPages is the class that is the entry point into the hierarchy of pages
@@ -34,6 +35,11 @@ class Pages
*/
protected $children;
/**
* @var string
*/
protected $base;
/**
* @var array|string[]
*/
@@ -67,6 +73,23 @@ class Pages
public function __construct(Grav $c)
{
$this->grav = $c;
$this->base = '';
}
/**
* Get or set base path for the pages.
*
* @param string $path
* @return string
*/
public function base($path = null)
{
if ($path !== null) {
$path = trim($path, '/');
$this->base = $path ? '/' . $path : null;
}
return $this->base;
}
/**
@@ -236,8 +259,6 @@ class Pages
// Fetch page if there's a defined route to it.
$page = isset($this->routes[$url]) ? $this->get($this->routes[$url]) : null;
// If the page cannot be reached, look into site wide redirects, routes + wildcards
if (!$all && (!$page || !$page->routable())) {
/** @var Config $config */
@@ -278,7 +299,10 @@ class Pages
*/
public function root()
{
return $this->instances[rtrim(PAGES_DIR, DS)];
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
return $this->instances[rtrim($locator->findResource('page://'), DS)];
}
/**
@@ -408,6 +432,10 @@ class Pages
/** @var Config $config */
$config = $this->grav['config'];
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
$pagesDir = $locator->findResource('page://');
if ($config->get('system.cache.enabled')) {
/** @var Cache $cache */
$cache = $this->grav['cache'];
@@ -421,10 +449,10 @@ class Pages
$last_modified = 0;
break;
case 'folder':
$last_modified = Folder::lastModifiedFolder(PAGES_DIR);
$last_modified = Folder::lastModifiedFolder($pagesDir);
break;
default:
$last_modified = Folder::lastModifiedFile(PAGES_DIR);
$last_modified = Folder::lastModifiedFile($pagesDir);
}
$page_cache_id = md5(USER_DIR.$last_modified.$config->checksum());
@@ -432,7 +460,7 @@ class Pages
list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cache->fetch($page_cache_id);
if (!$this->instances) {
$this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
$this->recurse();
$this->recurse($pagesDir);
$this->buildRoutes();
// save pages, routes, taxonomy, and sort to cache
@@ -446,7 +474,7 @@ class Pages
$taxonomy->taxonomy($taxonomy_map);
}
} else {
$this->recurse();
$this->recurse($pagesDir);
$this->buildRoutes();
}
}
@@ -460,7 +488,7 @@ class Pages
* @throws \RuntimeException
* @internal
*/
protected function recurse($directory = PAGES_DIR, Page &$parent = null)
protected function recurse($directory, Page &$parent = null)
{
$directory = rtrim($directory, DS);
$iterator = new \DirectoryIterator($directory);

View File

@@ -18,11 +18,17 @@ use RocketTheme\Toolbox\Blueprints\Blueprints;
class ConfigServiceProvider implements ServiceProviderInterface
{
private $environment;
private $setup;
public function register(Container $container)
{
$self = $this;
// Pre-load setup.php as it contains our initial configuration.
$file = GRAV_ROOT . '/setup.php';
$this->setup = is_file($file) ? (array) include $file : [];
$this->environment = isset($this->setup['environment']) ? $this->setup['environment'] : null;
$container['blueprints'] = function ($c) use ($self) {
return $self->loadMasterBlueprints($c);
};
@@ -45,9 +51,7 @@ class ConfigServiceProvider implements ServiceProviderInterface
}
if (!isset($config)) {
$file = GRAV_ROOT . '/setup.php';
$data = is_file($file) ? (array) include $file : [];
$config = new Config($data, $container, $environment);
$config = new Config($this->setup, $container, $environment);
}
return $config;

View File

@@ -11,52 +11,32 @@ use RocketTheme\Toolbox\StreamWrapper\StreamBuilder;
class StreamsServiceProvider implements ServiceProviderInterface
{
protected $schemes = [];
public function register(Container $container)
{
$self = $this;
$container['locator'] = function($c) use ($self) {
$locator = new UniformResourceLocator(ROOT_DIR);
$self->init($c, $locator);
/** @var Config $config */
$config = $c['config'];
$config->initializeLocator($locator);
return $locator;
};
$container['streams'] = function($c) use ($self) {
/** @var Config $config */
$config = $c['config'];
/** @var UniformResourceLocator $locator */
$locator = $c['locator'];
// Set locator to both streams.
Stream::setLocator($locator);
ReadOnlyStream::setLocator($locator);
return new StreamBuilder($this->schemes);
return new StreamBuilder($config->getStreams($c));
};
}
protected function init(Container $container, UniformResourceLocator $locator)
{
/** @var Config $config */
$config = $container['config'];
$schemes = (array) $config->get('streams.schemes', []);
foreach ($schemes as $scheme => $config) {
if (isset($config['paths'])) {
$locator->addPath($scheme, '', $config['paths']);
}
if (isset($config['prefixes'])) {
foreach ($config['prefixes'] as $prefix => $paths) {
$locator->addPath($scheme, $prefix, $paths);
}
}
$type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
if ($type[0] != '\\') {
$type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
}
$this->schemes[$scheme] = $type;
}
}
}

View File

@@ -125,9 +125,6 @@ class Twig
$this->grav->fireEvent('onTwigExtensions');
$theme = $config->get('system.pages.theme');
$themeUrl = $this->grav['base_url'] .'/'. USER_PATH . basename(THEMES_DIR) .'/'. $theme;
// Set some standard variables for twig
$this->twig_vars = array(
'grav' => $this->grav,
@@ -138,7 +135,7 @@ class Twig
'base_url_absolute' => $this->grav['base_url_absolute'],
'base_url_relative' => $this->grav['base_url_relative'],
'theme_dir' => $locator->findResource('theme://'),
'theme_url' => $themeUrl,
'theme_url' => $this->grav['base_url'] .'/'. $locator->findResource('theme://', false),
'site' => $config->get('site'),
'assets' => $this->grav['assets'],
'taxonomy' => $this->grav['taxonomy'],

View File

@@ -331,19 +331,32 @@ class TwigExtension extends \Twig_Extension
/**
* Return URL to the resource.
*
* @param string $input
* @param bool $domain
* @return string
* @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }}
*
* @param string $input Resource to be located.
* @param bool $domain True to include domain name.
* @return string|null Returns url to the resource or null if resource was not found.
*/
public function urlFunc($input, $domain = false)
{
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
if (!trim((string) $input)) {
return false;
}
if (strpos((string) $input, '://')) {
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
// Get relative path to the resource (or false if not found).
$resource = $locator->findResource((string) $input, false);
} else {
$resource = (string) $input;
}
/** @var Uri $uri */
$uri = $this->grav['uri'];
return $uri->rootUrl($domain) .'/'. $locator->findResource($input, false);
return $resource ? rtrim($uri->rootUrl($domain), '/') . '/' . $resource : null;
}
/**

View File

@@ -4,6 +4,7 @@ namespace Grav\Common\User;
use Grav\Common\Data\Blueprints;
use Grav\Common\Data\Data;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\GravTrait;
/**
* User object
@@ -13,6 +14,8 @@ use Grav\Common\File\CompiledYamlFile;
*/
class User extends Data
{
use GravTrait;
/**
* Load user account.
*
@@ -23,10 +26,13 @@ class User extends Data
*/
public static function load($username)
{
$locator = self::$grav['locator'];
// FIXME: validate directory name
$blueprints = new Blueprints('blueprints://user');
$blueprint = $blueprints->get('account');
$file = CompiledYamlFile::instance(ACCOUNTS_DIR . $username . YAML_EXT);
$file_path = $locator->findResource('account://' . $username . YAML_EXT);
$file = CompiledYamlFile::instance($file_path);
$content = $file->content();
if (!isset($content['username'])) {
$content['username'] = $username;

View File

@@ -73,6 +73,7 @@ class CleanCommand extends Command
'vendor/gregwar/image/Gregwar/Image/phpunit.xml',
'vendor/gregwar/image/Gregwar/Image/.gitignore',
'vendor/gregwar/image/Gregwar/Image/.git',
'vendor/gregwar/image/Gregwar/Image/doc',
'vendor/gregwar/image/Gregwar/Image/demo',
'vendor/gregwar/image/Gregwar/Image/tests',
'vendor/gregwar/cache/Gregwar/Cache/composer.json',
@@ -90,6 +91,10 @@ class CleanCommand extends Command
'vendor/maximebf/debugbar/composer.json',
'vendor/maximebf/debugbar/.bowerrc',
'vendor/maximebf/debugbar/src/Debugbar/Resources/vendor',
'vendor/maximebf/debugbar/demo',
'vendor/maximebf/debugbar/docs',
'vendor/maximebf/debugbar/tests',
'vendor/maximebf/debugbar/phpunit.xml.dist',
'vendor/monolog/monolog/composer.json',
'vendor/monolog/monolog/doc',
'vendor/monolog/monolog/phpunit.xml.dist',

View File

View File

@@ -0,0 +1,8 @@
<?php
namespace Grav;
class TestCase extends \PHPUnit_Framework_TestCase
{
}

View File

@@ -0,0 +1,6 @@
<?php
error_reporting(E_ALL);
date_default_timezone_set(@date_default_timezone_get());
require_once __DIR__.'/../../vendor/autoload.php';
require_once __DIR__.'/Grav/TestCase.php';

18
system/tests/phpunit.xml Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="./bootstrap.php"
>
<testsuites>
<testsuite name="Full Grav Test Suite">
<directory>./Grav/</directory>
</testsuite>
</testsuites>
</phpunit>