Merge branch '3.0' into 3.1

Conflicts:
	model/Versioned.php
	view/SSTemplateParser.php
	view/SSViewer.php
This commit is contained in:
Simon Welsh 2014-03-30 18:15:12 +13:00
commit f9c44e4ceb
10 changed files with 203 additions and 23 deletions

View File

@ -234,12 +234,14 @@ class SS_HTTPResponse {
} }
if(in_array($this->statusCode, self::$redirect_codes) && headers_sent($file, $line)) { if(in_array($this->statusCode, self::$redirect_codes) && headers_sent($file, $line)) {
$url = $this->headers['Location']; $url = (string)$this->headers['Location'];
$urlATT = Convert::raw2htmlatt($url);
$urlJS = Convert::raw2js($url);
echo echo
"<p>Redirecting to <a href=\"$url\" title=\"Click this link if your browser does not redirect you\">" "<p>Redirecting to <a href=\"$urlATT\" title=\"Click this link if your browser does not redirect you\">"
. "$url... (output started on $file, line $line)</a></p> . "$urlATT... (output started on $file, line $line)</a></p>
<meta http-equiv=\"refresh\" content=\"1; url=$url\" /> <meta http-equiv=\"refresh\" content=\"1; url=$urlATT\" />
<script type=\"text/javascript\">setTimeout('window.location.href = \"$url\"', 50);</script>"; <script type=\"text/javascript\">setTimeout(function(){ window.location.href = \"$urlJS\"; }, 50);</script>";
} else { } else {
$line = $file = null; $line = $file = null;
if(!headers_sent($file, $line)) { if(!headers_sent($file, $line)) {

View File

@ -0,0 +1,12 @@
# 3.0.10
## Overview
* Security: Partially cached content from stage or other reading modes is no longer emitted to live
## Upgrading
* If relying on partial caching of content between logged in users, be aware that the cache is now automatically
segmented based on both the current member ID, and the versioned reading mode. If this is not an appropriate
method (such as if the same content is served to logged in users within partial caching) then it is necessary
to adjust the config value of `SSViewer::global_key` to something more or less sensitive.

View File

@ -28,7 +28,7 @@ This is a highlevel overview of available `[api:FormField]` subclasses. An autom
* `[api:DatetimeField]`: Combined date- and time field. * `[api:DatetimeField]`: Combined date- and time field.
* `[api:EmailField]`: Text input field with validation for correct email format according to RFC 2822. * `[api:EmailField]`: Text input field with validation for correct email format according to RFC 2822.
* `[api:GroupedDropdownField]`: Grouped dropdown, using <optgroup> tags. * `[api:GroupedDropdownField]`: Grouped dropdown, using <optgroup> tags.
* `[api:HTMLEditorField]. * `[api:HtmlEditorField]`.
* `[api:MoneyField]`: A form field that can save into a `[api:Money]` database field. * `[api:MoneyField]`: A form field that can save into a `[api:Money]` database field.
* `[api:NumericField]`: Text input field with validation for numeric values. * `[api:NumericField]`: Text input field with validation for numeric values.
* `[api:OptionsetField]`: Set of radio buttons designed to emulate a dropdown. * `[api:OptionsetField]`: Set of radio buttons designed to emulate a dropdown.

View File

@ -51,6 +51,19 @@ From a block that shows a summary of the page edits if administrator, nothing if
<% cached 'loginblock', LastEdited, CurrentMember.isAdmin %> <% cached 'loginblock', LastEdited, CurrentMember.isAdmin %>
An additional global key is incorporated in the cache lookup. The default value for this is
`$CurrentReadingMode, $CurrentUser.ID`, which ensures that the current `[api:Versioned]` state and user ID are
used. This may be configured by changing the config value of `SSViewer.global_key`. It is also necessary
to flush the template caching when modifying this config, as this key is cached within the template itself.
For example, to ensure that the cache is configured to respect another variable, and if the current logged in
user does not influence your template content, you can update this key as below;
:::yaml
SSViewer:
global_key: '$CurrentReadingMode, $Locale'
## Aggregates ## Aggregates
Often you want to invalidate a cache when any in a set of objects change, or when the objects in a relationship change. Often you want to invalidate a cache when any in a set of objects change, or when the objects in a relationship change.

View File

@ -8,8 +8,7 @@
* @package framework * @package framework
* @subpackage model * @subpackage model
*/ */
class Versioned extends DataExtension { class Versioned extends DataExtension implements TemplateGlobalProvider {
/** /**
* An array of possible stages. * An array of possible stages.
* @var array * @var array
@ -1339,6 +1338,12 @@ class Versioned extends DataExtension {
public function getDefaultStage() { public function getDefaultStage() {
return $this->defaultStage; return $this->defaultStage;
} }
public static function get_template_global_variables() {
return array(
'CurrentReadingMode' => 'get_reading_mode'
);
}
} }
/** /**

View File

@ -20,9 +20,29 @@ class SSViewerCacheBlockTest_Model extends DataObject implements TestOnly {
} }
} }
class SSViewerCacheBlockTest_VersionedModel extends DataObject implements TestOnly {
protected $entropy = 'default';
public static $extensions = array(
"Versioned('Stage', 'Live')"
);
public function setEntropy($entropy) {
$this->entropy = $entropy;
}
public function Inspect() {
return $this->entropy . ' ' . Versioned::get_reading_mode();
}
}
class SSViewerCacheBlockTest extends SapphireTest { class SSViewerCacheBlockTest extends SapphireTest {
protected $extraDataObjects = array('SSViewerCacheBlockTest_Model'); protected $extraDataObjects = array(
'SSViewerCacheBlockTest_Model',
'SSViewerCacheBlockTest_VersionedModel'
);
protected $data = null; protected $data = null;
@ -37,8 +57,7 @@ class SSViewerCacheBlockTest extends SapphireTest {
if ($data === null) $data = $this->data; if ($data === null) $data = $this->data;
if (is_array($data)) $data = $this->data->customise($data); if (is_array($data)) $data = $this->data->customise($data);
$viewer = SSViewer::fromString($template); return SSViewer::execute_string($template, $data);
return $viewer->process($data);
} }
public function testParsing() { public function testParsing() {
@ -105,6 +124,77 @@ class SSViewerCacheBlockTest extends SapphireTest {
$this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 2)), '1'); $this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 2)), '1');
} }
public function testVersionedCache() {
$origStage = Versioned::current_stage();
// Run without caching in stage to prove data is uncached
$this->_reset(false);
Versioned::reading_stage("Stage");
$data = new SSViewerCacheBlockTest_VersionedModel();
$data->setEntropy('default');
$this->assertEquals(
'default Stage.Stage',
SSViewer::execute_string('<% cached %>$Inspect<% end_cached %>', $data)
);
$data = new SSViewerCacheBlockTest_VersionedModel();
$data->setEntropy('first');
$this->assertEquals(
'first Stage.Stage',
SSViewer::execute_string('<% cached %>$Inspect<% end_cached %>', $data)
);
// Run without caching in live to prove data is uncached
$this->_reset(false);
Versioned::reading_stage("Live");
$data = new SSViewerCacheBlockTest_VersionedModel();
$data->setEntropy('default');
$this->assertEquals(
'default Stage.Live',
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
);
$data = new SSViewerCacheBlockTest_VersionedModel();
$data->setEntropy('first');
$this->assertEquals(
'first Stage.Live',
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
);
// Then with caching, initially in draft, and then in live, to prove that
// changing the versioned reading mode doesn't cache between modes, but it does
// within them
$this->_reset(true);
Versioned::reading_stage("Stage");
$data = new SSViewerCacheBlockTest_VersionedModel();
$data->setEntropy('default');
$this->assertEquals(
'default Stage.Stage',
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
);
$data = new SSViewerCacheBlockTest_VersionedModel();
$data->setEntropy('first');
$this->assertEquals(
'default Stage.Stage', // entropy should be ignored due to caching
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
);
Versioned::reading_stage('Live');
$data = new SSViewerCacheBlockTest_VersionedModel();
$data->setEntropy('first');
$this->assertEquals(
'first Stage.Live', // First hit in live, so display current entropy
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
);
$data = new SSViewerCacheBlockTest_VersionedModel();
$data->setEntropy('second');
$this->assertEquals(
'first Stage.Live', // entropy should be ignored due to caching
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
);
Versioned::reading_stage($origStage);
}
/** /**
* Test that cacheblocks conditionally cache with if * Test that cacheblocks conditionally cache with if
*/ */

View File

@ -85,9 +85,8 @@ class SSViewerTest extends SapphireTest {
* Small helper to render templates from strings * Small helper to render templates from strings
*/ */
public function render($templateString, $data = null) { public function render($templateString, $data = null) {
$t = SSViewer::fromString($templateString);
if(!$data) $data = new SSViewerTestFixture(); if(!$data) $data = new SSViewerTestFixture();
return $t->process($data); return SSViewer::execute_string($templateString, $data);
} }
public function testRequirements() { public function testRequirements() {

View File

@ -2938,10 +2938,25 @@ class SSTemplateParser extends Parser implements TemplateParser {
function CacheBlock_CacheBlockTemplate(&$res, $sub){ function CacheBlock_CacheBlockTemplate(&$res, $sub){
// Get the block counter // Get the block counter
$block = ++$res['subblocks']; $block = ++$res['subblocks'];
// Build the key for this block from the passed cache key, the block index, and the sha hash of the template // Build the key for this block from the global key (evaluated in a closure within the template),
// itself // the passed cache key, the block index, and the sha hash of the template.
$key = "'" . sha1($sub['php']) . (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") . $res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
".'_$block'"; $res['php'] .= '$val = \'\';' . PHP_EOL;
if($globalKey = Config::inst()->get('SSViewer', 'global_key')) {
// Embed the code necessary to evaluate the globalKey directly into the template,
// so that SSTemplateParser only needs to be called during template regeneration.
// Warning: If the global key is changed, it's necessary to flush the template cache.
$parser = new SSTemplateParser($globalKey);
$result = $parser->match_Template();
if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $parser);
$res['php'] .= $result['php'] . PHP_EOL;
}
$res['php'] .= 'return $val;' . PHP_EOL;
$res['php'] .= '};' . PHP_EOL;
$key = 'sha1($keyExpression())' // Global key
. '.\'_' . sha1($sub['php']) // sha of template
. (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") // Passed key
. ".'_$block'"; // block index
// Get any condition // Get any condition
$condition = isset($res['condition']) ? $res['condition'] : ''; $condition = isset($res['condition']) ? $res['condition'] : '';

View File

@ -673,10 +673,25 @@ class SSTemplateParser extends Parser implements TemplateParser {
function CacheBlock_CacheBlockTemplate(&$res, $sub){ function CacheBlock_CacheBlockTemplate(&$res, $sub){
// Get the block counter // Get the block counter
$block = ++$res['subblocks']; $block = ++$res['subblocks'];
// Build the key for this block from the passed cache key, the block index, and the sha hash of the template // Build the key for this block from the global key (evaluated in a closure within the template),
// itself // the passed cache key, the block index, and the sha hash of the template.
$key = "'" . sha1($sub['php']) . (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") . $res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
".'_$block'"; $res['php'] .= '$val = \'\';' . PHP_EOL;
if($globalKey = Config::inst()->get('SSViewer', 'global_key')) {
// Embed the code necessary to evaluate the globalKey directly into the template,
// so that SSTemplateParser only needs to be called during template regeneration.
// Warning: If the global key is changed, it's necessary to flush the template cache.
$parser = new SSTemplateParser($globalKey);
$result = $parser->match_Template();
if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $parser);
$res['php'] .= $result['php'] . PHP_EOL;
}
$res['php'] .= 'return $val;' . PHP_EOL;
$res['php'] .= '};' . PHP_EOL;
$key = 'sha1($keyExpression())' // Global key
. '.\'_' . sha1($sub['php']) // sha of template
. (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") // Passed key
. ".'_$block'"; // block index
// Get any condition // Get any condition
$condition = isset($res['condition']) ? $res['condition'] : ''; $condition = isset($res['condition']) ? $res['condition'] : '';

View File

@ -48,7 +48,6 @@ class SSViewer_Scope {
private $localIndex; private $localIndex;
public function __construct($item, $inheritedScope = null) { public function __construct($item, $inheritedScope = null) {
$this->item = $item; $this->item = $item;
$this->localIndex = 0; $this->localIndex = 0;
@ -631,6 +630,14 @@ class SSViewer {
*/ */
protected $parser; protected $parser;
/*
* Default prepended cache key for partial caching
*
* @var string
* @config
*/
private static $global_key = '$CurrentReadingMode, $CurrentUser.ID';
/** /**
* Create a template from a string instead of a .ss file * Create a template from a string instead of a .ss file
* *
@ -1083,6 +1090,11 @@ class SSViewer {
/** /**
* Execute the given template, passing it the given data. * Execute the given template, passing it the given data.
* Used by the <% include %> template tag to process templates. * Used by the <% include %> template tag to process templates.
*
* @param string $template Template name
* @param mixed $data Data context
* @param array $arguments Additional arguments
* @return string Evaluated result
*/ */
public static function execute_template($template, $data, $arguments = null, $scope = null) { public static function execute_template($template, $data, $arguments = null, $scope = null) {
$v = new SSViewer($template); $v = new SSViewer($template);
@ -1091,6 +1103,23 @@ class SSViewer {
return $v->process($data, $arguments, $scope); return $v->process($data, $arguments, $scope);
} }
/**
* Execute the evaluated string, passing it the given data.
* Used by partial caching to evaluate custom cache keys expressed using
* template expressions
*
* @param string $content Input string
* @param mixed $data Data context
* @param array $arguments Additional arguments
* @return string Evaluated result
*/
public static function execute_string($content, $data, $arguments = null) {
$v = SSViewer::fromString($content);
$v->includeRequirements(false);
return $v->process($data, $arguments);
}
public function parseTemplateContent($content, $template="") { public function parseTemplateContent($content, $template="") {
return $this->parser->compileString( return $this->parser->compileString(
$content, $content,