mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch '3.0' into 3.1
Conflicts: model/Versioned.php view/SSTemplateParser.php view/SSViewer.php
This commit is contained in:
commit
f9c44e4ceb
@ -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)) {
|
||||||
|
12
docs/en/changelogs/3.0.10.md
Normal file
12
docs/en/changelogs/3.0.10.md
Normal 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.
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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() {
|
||||||
|
@ -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'] : '';
|
||||||
|
|
||||||
|
@ -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'] : '';
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user