diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49789ba71d..54d02ea79a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+# v0.9.24
+## 04/15/2015
+
+1. [](#new)
+ * Added support for chunked downloads of Assets
+ * Added new `onBeforeDownload()` event
+ * Added new `download()` and `getMimeType()` methods to Utils class
+ * Added configuration option for supported page types
+ * Added assets and media timestamp options (off by default)
+ * Added page expires configuration option
+2. [](#bugfix)
+ * Fixed issue with Nginx/Gzip and `ob_flush()` throwing error
+ * Fixed assets actions on 'direct media' URLs
+ * Fix for 'direct assets` with any parameters
+
# v0.9.23
## 04/09/2015
diff --git a/composer.json b/composer.json
index 53bcf01130..6d4f3362b0 100644
--- a/composer.json
+++ b/composer.json
@@ -16,7 +16,7 @@
"maximebf/debugbar": "dev-master",
"filp/whoops": "1.2.*@dev",
"monolog/monolog": "~1.0",
- "gregwar/image": "~2.0",
+ "gregwar/image": "~2.0",
"ircmaxell/password-compat": "1.0.*",
"mrclay/minify": "dev-master",
"donatj/phpuseragentparser": "dev-master",
diff --git a/system/config/system.yaml b/system/config/system.yaml
index 59f4b36a9f..a62b37b804 100644
--- a/system/config/system.yaml
+++ b/system/config/system.yaml
@@ -30,6 +30,8 @@ pages:
special_chars: # List of special characters to automatically convert to entities
'>': '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)
cache:
enabled: true # Set to true to enable caching
@@ -40,6 +42,7 @@ cache:
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
debug: false # Enable Twig debug
@@ -55,6 +58,7 @@ assets: # Configuration for Assets Manager (JS, C
css_rewrite: true # Rewrite any CSS relative URLs during pipelining
js_pipeline: false # The JS pipeline is the unification of multiple JS resources into one file
js_minify: true # Minify the JS during pipelining
+ enable_asset_timestamp: false # Enable asset timetsamps
collections:
jquery: system://assets/jquery/jquery-2.1.3.min.js
@@ -71,3 +75,6 @@ debugger:
images:
default_image_quality: 85 # Default image quality to use when resampling images (85%)
debug: false # Show an overlay over images indicating the pixel depth of the image when working with retina for example
+
+media:
+ enable_media_timestamp: false # Enable media timetsamps
diff --git a/system/defines.php b/system/defines.php
index a9f6bc5e4c..04f8ac6dfb 100644
--- a/system/defines.php
+++ b/system/defines.php
@@ -2,7 +2,7 @@
// Some standard defines
define('GRAV', true);
-define('GRAV_VERSION', '0.9.23');
+define('GRAV_VERSION', '0.9.24');
define('DS', '/');
// Directories and Paths
diff --git a/system/src/Grav/Common/Assets.php b/system/src/Grav/Common/Assets.php
index a63a08d3d3..3b232f4576 100644
--- a/system/src/Grav/Common/Assets.php
+++ b/system/src/Grav/Common/Assets.php
@@ -71,6 +71,7 @@ class Assets
// Some configuration variables
protected $config;
protected $base_url;
+ protected $timestamp = '';
// Default values for pipeline settings
protected $css_minify = true;
@@ -82,7 +83,6 @@ class Assets
protected $css_no_pipeline = array();
protected $js_no_pipeline = array();
-
public function __construct(array $options = array())
{
// Forward config options
@@ -154,6 +154,12 @@ public function config(array $config)
}
}
+ // Set timestamp
+ if (isset($config['enable_asset_timestamp']) && $config['enable_asset_timestamp'] === true) {
+ $this->timestamp = '?' . self::getGrav()['cache']->getKey();
+ }
+
+
return $this;
}
@@ -422,11 +428,11 @@ public function css($attributes = [])
$output .= '' . "\n";
foreach ($this->css_no_pipeline as $file) {
- $output .= '' . "\n";
+ $output .= '' . "\n";
}
} else {
foreach ($this->css as $file) {
- $output .= '' . "\n";
+ $output .= '' . "\n";
}
}
@@ -480,11 +486,11 @@ public function js($attributes = [])
if ($this->js_pipeline) {
$output .= '' . "\n";
foreach ($this->js_no_pipeline as $file) {
- $output .= '' . "\n";
+ $output .= '' . "\n";
}
} else {
foreach ($this->js as $file) {
- $output .= '' . "\n";
+ $output .= '' . "\n";
}
}
diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php
index e0a91a06a3..df584fe4b1 100644
--- a/system/src/Grav/Common/Grav.php
+++ b/system/src/Grav/Common/Grav.php
@@ -2,6 +2,7 @@
namespace Grav\Common;
use Grav\Common\Filesystem\Folder;
+use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Pages;
use Grav\Common\Service\ConfigServiceProvider;
use Grav\Common\Service\ErrorServiceProvider;
@@ -10,7 +11,6 @@
use RocketTheme\Toolbox\DI\Container;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\Event\EventDispatcher;
-use Grav\Common\Page\Medium\Medium;
/**
* Grav
@@ -99,32 +99,34 @@ protected static function load(array $values)
/** @var Pages $pages */
$pages = $c['pages'];
- // If base URI is set, we want to remove it from the URL.
- $path = '/' . ltrim(Folder::getRelativePath($c['uri']->route(), $pages->base()), '/');
+ /** @var Uri $uri */
+ $uri = $c['uri'];
+
+ $path = $uri->path();
$page = $pages->dispatch($path);
if (!$page || !$page->routable()) {
-
- // special case where a media file is requested
$path_parts = pathinfo($path);
-
$page = $c['pages']->dispatch($path_parts['dirname'], true);
if ($page) {
$media = $page->media()->all();
- $media_file = urldecode($path_parts['basename']);
+
+ $parsed_url = parse_url(urldecode($uri->basename()));
+
+ $media_file = $parsed_url['path'];
+
+ // if this is a media object, try actions first
if (isset($media[$media_file])) {
$medium = $media[$media_file];
-
- // loop through actions for the image and call them
- foreach ($c['uri']->query(null, true) as $action => $params) {
- if (in_array($action, Medium::$valid_actions)) {
+ foreach ($uri->query(null, true) as $action => $params) {
+ if (in_array($action, ImageMedium::$magic_actions)) {
call_user_func_array(array(&$medium, $action), explode(',', $params));
}
}
- header('Content-type: '. $medium->get('mime'));
- echo file_get_contents($medium->path());
- die;
+ Utils::download($medium->path(), false);
+ } else {
+ Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), true);
}
}
@@ -296,6 +298,8 @@ public function header()
$extension = $this['uri']->extension();
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')));
+
// Set debugger data in headers
if (!($extension === null || $extension == 'html')) {
$this['debugger']->enabled(false);
@@ -345,7 +349,7 @@ public function shutdown()
header("Connection: close\r\n");
ob_end_flush(); // regular buffer
- ob_flush();
+ @ob_flush();
flush();
if (function_exists('fastcgi_finish_request')) {
diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php
index ede065f09a..17a9663dec 100644
--- a/system/src/Grav/Common/Page/Medium/ImageMedium.php
+++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php
@@ -42,7 +42,7 @@ class ImageMedium extends Medium
public static $magic_actions = [
'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
- 'smooth', 'sharp', 'edge', 'colorize', 'sepia'
+ 'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive'
];
/**
@@ -127,7 +127,7 @@ public function url($reset = true)
$this->reset();
}
- return self::$grav['base_url'] . $output . $this->urlHash();
+ return self::$grav['base_url'] . $output . $this->querystring() . $this->urlHash();
}
@@ -299,7 +299,7 @@ public function __call($method, $args)
}
if (!in_array($method, self::$magic_actions)) {
- return $this;
+ return parent::__call($method, $args);
}
// Always initialize image.
diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php
index e001ec3c6c..5c210e563d 100644
--- a/system/src/Grav/Common/Page/Medium/Medium.php
+++ b/system/src/Grav/Common/Page/Medium/Medium.php
@@ -60,6 +60,10 @@ public function __construct($items = [], Blueprint $blueprint = null)
{
parent::__construct($items, $blueprint);
+ if (self::getGrav()['config']->get('media.enable_media_timestamp', true)) {
+ $this->querystring('&' . self::getGrav()['cache']->getKey());
+ }
+
$this->def('mime', 'application/octet-stream');
$this->reset();
}
@@ -129,7 +133,33 @@ public function url($reset = true)
$this->reset();
}
- return self::$grav['base_url'] . $output . $this->urlHash();
+ return self::$grav['base_url'] . $output . $this->querystring() . $this->urlHash();
+ }
+
+ /**
+ * Get/set querystring for the file's url
+ *
+ * @param string $hash
+ * @param boolean $withHash
+ * @return string
+ */
+ public function querystring($querystring = null, $withQuestionmark = true)
+ {
+ if ($querystring) {
+ $this->set('querystring', ltrim($querystring, '?&'));
+
+ foreach ($this->alternatives as $alt) {
+ $alt->querystring($querystring, $withQuestionmark);
+ }
+ }
+
+ $querystring = $this->get('querystring', '');
+
+ if ($withQuestionmark && !empty($querystring)) {
+ return '?' . $querystring;
+ } else {
+ return $querystring;
+ }
}
/**
@@ -337,6 +367,17 @@ public function lightbox($width = null, $height = null, $reset = true)
*/
public function __call($method, $args)
{
+ $qs = $method;
+ if (count($args) > 1 || (count($args) == 1 && !empty($args[0]))) {
+ $qs .= '=' . implode(',', array_map(function ($a) { return urlencode($a); }, $args));
+ }
+
+ if (!empty($qs)) {
+ $this->querystring($this->querystring(null, false) . '&' . $qs);
+ }
+
+ self::$grav['debugger']->addMessage($this->querystring());
+
return $this;
}
diff --git a/system/src/Grav/Common/Page/Medium/MediumFactory.php b/system/src/Grav/Common/Page/Medium/MediumFactory.php
index 9ff05d4b95..ffa6b8f50f 100644
--- a/system/src/Grav/Common/Page/Medium/MediumFactory.php
+++ b/system/src/Grav/Common/Page/Medium/MediumFactory.php
@@ -131,8 +131,7 @@ public static function scaledFromMedium($medium, $from, $to)
$debug = $medium->get('debug');
$medium->set('debug', false);
- $file = $medium->resize($width, $height)->setPrettyName($basename)->url();
- $file = preg_replace('|'. preg_quote(self::getGrav()['base_url_relative']) .'$|', '', GRAV_ROOT) . $file;
+ $file = $medium->resize($width, $height)->path();
$medium->set('debug', $debug);
diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php
index d1dad9fda7..e2b7b66704 100644
--- a/system/src/Grav/Common/Uri.php
+++ b/system/src/Grav/Common/Uri.php
@@ -11,6 +11,7 @@ class Uri
{
public $url;
+ protected $basename;
protected $base;
protected $root;
protected $bits;
@@ -64,6 +65,7 @@ public function __construct()
$this->base = $base;
$this->root = $base . $root_path;
$this->url = $base . $uri;
+
}
/**
@@ -84,7 +86,11 @@ public function init()
// remove the extension if there is one set
$parts = pathinfo($uri);
- if (preg_match("/\.(txt|xml|html|json|rss|atom)$/", $parts['basename'])) {
+
+ // set the original basename
+ $this->basename = $parts['basename'];
+
+ if (preg_match("/\.(".$config->get('system.pages.types').")$/", $parts['basename'])) {
$uri = rtrim($parts['dirname'], '/').'/'.$parts['filename'];
$this->extension = $parts['extension'];
}
@@ -282,6 +288,17 @@ public function environment()
return $this->host();
}
+
+ /**
+ * Return the basename of the URI
+ *
+ * @return String The basename of the URI
+ */
+ public function basename()
+ {
+ return $this->basename;
+ }
+
/**
* Return the base of the URI
*
diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php
index c82aabf333..4d7163e357 100644
--- a/system/src/Grav/Common/Utils.php
+++ b/system/src/Grav/Common/Utils.php
@@ -1,6 +1,8 @@
fireEvent('onBeforeDownload', new Event(['file' => $file]));
+
+ $file_parts = pathinfo($file);
+ $filesize = filesize($file);
+ $range = false;
+
+ set_time_limit(0);
+ ignore_user_abort(false);
+ ini_set('output_buffering', 0);
+ ini_set('zlib.output_compression', 0);
+
+ if ($force_download) {
+ header('Content-Description: File Transfer');
+ header('Content-Type: application/octet-stream');
+ header('Content-Disposition: attachment; filename='.$file_parts['basename']);
+ header('Content-Transfer-Encoding: binary');
+ header('Expires: 0');
+ header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+ header('Pragma: public');
+ } else {
+ header("Content-Type: " . Utils::getMimeType($file_parts['extension']));
+ }
+ header('Content-Length: ' . $filesize);
+
+ // 8kb chunks for now
+ $chunk = 8 * 1024;
+
+ $fh = fopen($file, "rb");
+
+ if ($fh === false) {
+ return;
+ }
+
+ // Repeat reading until EOF
+ while (!feof($fh)) {
+ echo fread($fh, $chunk);
+
+ ob_flush(); // flush output
+ flush();
+ }
+
+ exit;
+ }
+ }
+
+ /**
+ * Return the mimetype based on filename
+ *
+ * @param $extension Extension of file (eg .txt)
+ *
+ * @return string
+ */
+ public static function getMimeType($extension)
+ {
+ $extension = strtolower($extension);
+
+ switch($extension)
+ {
+ case "js":
+ return "application/x-javascript";
+
+ case "json":
+ return "application/json";
+
+ case "jpg":
+ case "jpeg":
+ case "jpe":
+ return "image/jpg";
+
+ case "png":
+ case "gif":
+ case "bmp":
+ case "tiff":
+ return "image/" . $extension;
+
+ case "css":
+ return "text/css";
+
+ case "xml":
+ return "application/xml";
+
+ case "doc":
+ case "docx":
+ return "application/msword";
+
+ case "xls":
+ case "xlt":
+ case "xlm":
+ case "xld":
+ case "xla":
+ case "xlc":
+ case "xlw":
+ case "xll":
+ return "application/vnd.ms-excel";
+
+ case "ppt":
+ case "pps":
+ return "application/vnd.ms-powerpoint";
+
+ case "rtf":
+ return "application/rtf";
+
+ case "pdf":
+ return "application/pdf";
+
+ case "html":
+ case "htm":
+ case "php":
+ return "text/html";
+
+ case "txt":
+ return "text/plain";
+
+ case "mpeg":
+ case "mpg":
+ case "mpe":
+ return "video/mpeg";
+
+ case "mp3":
+ return "audio/mpeg3";
+
+ case "wav":
+ return "audio/wav";
+
+ case "aiff":
+ case "aif":
+ return "audio/aiff";
+
+ case "avi":
+ return "video/msvideo";
+
+ case "wmv":
+ return "video/x-ms-wmv";
+
+ case "mov":
+ return "video/quicktime";
+
+ case "zip":
+ return "application/zip";
+
+ case "tar":
+ return "application/x-tar";
+
+ case "swf":
+ return "application/x-shockwave-flash";
+
+ default:
+ return "application/octet-stream";
+ }
+ }
}