Skip to content

Commit

Permalink
Merge branch '3.0' into 3.1
Browse files Browse the repository at this point in the history
Conflicts:
	model/Versioned.php
	view/SSTemplateParser.php
	view/SSViewer.php
  • Loading branch information
simonwelsh committed Mar 30, 2014
2 parents 398e5bc + 3e05ccb commit f9c44e4
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 23 deletions.
12 changes: 7 additions & 5 deletions control/HTTPResponse.php
Expand Up @@ -234,12 +234,14 @@ public function output() {
}

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
"<p>Redirecting to <a href=\"$url\" title=\"Click this link if your browser does not redirect you\">"
. "$url... (output started on $file, line $line)</a></p>
<meta http-equiv=\"refresh\" content=\"1; url=$url\" />
<script type=\"text/javascript\">setTimeout('window.location.href = \"$url\"', 50);</script>";
"<p>Redirecting to <a href=\"$urlATT\" title=\"Click this link if your browser does not redirect you\">"
. "$urlATT... (output started on $file, line $line)</a></p>
<meta http-equiv=\"refresh\" content=\"1; url=$urlATT\" />
<script type=\"text/javascript\">setTimeout(function(){ window.location.href = \"$urlJS\"; }, 50);</script>";
} else {
$line = $file = null;
if(!headers_sent($file, $line)) {
Expand Down
12 changes: 12 additions & 0 deletions docs/en/changelogs/3.0.10.md
@@ -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.
2 changes: 1 addition & 1 deletion docs/en/reference/form-field-types.md
Expand Up @@ -28,7 +28,7 @@ This is a highlevel overview of available `[api:FormField]` subclasses. An autom
* `[api:DatetimeField]`: Combined date- and time field.
* `[api:EmailField]`: Text input field with validation for correct email format according to RFC 2822.
* `[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:NumericField]`: Text input field with validation for numeric values.
* `[api:OptionsetField]`: Set of radio buttons designed to emulate a dropdown.
Expand Down
13 changes: 13 additions & 0 deletions docs/en/reference/partial-caching.md
Expand Up @@ -51,6 +51,19 @@ From a block that shows a summary of the page edits if administrator, nothing if
<% 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

Often you want to invalidate a cache when any in a set of objects change, or when the objects in a relationship change.
Expand Down
9 changes: 7 additions & 2 deletions model/Versioned.php
Expand Up @@ -8,8 +8,7 @@
* @package framework
* @subpackage model
*/
class Versioned extends DataExtension {

class Versioned extends DataExtension implements TemplateGlobalProvider {
/**
* An array of possible stages.
* @var array
Expand Down Expand Up @@ -1339,6 +1338,12 @@ public function getVersionedStages() {
public function getDefaultStage() {
return $this->defaultStage;
}

public static function get_template_global_variables() {
return array(
'CurrentReadingMode' => 'get_reading_mode'
);
}
}

/**
Expand Down
96 changes: 93 additions & 3 deletions tests/view/SSViewerCacheBlockTest.php
Expand Up @@ -20,9 +20,29 @@ public function False() {
}
}

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 {

protected $extraDataObjects = array('SSViewerCacheBlockTest_Model');
protected $extraDataObjects = array(
'SSViewerCacheBlockTest_Model',
'SSViewerCacheBlockTest_VersionedModel'
);

protected $data = null;

Expand All @@ -37,8 +57,7 @@ protected function _runtemplate($template, $data = null) {
if ($data === null) $data = $this->data;
if (is_array($data)) $data = $this->data->customise($data);

$viewer = SSViewer::fromString($template);
return $viewer->process($data);
return SSViewer::execute_string($template, $data);
}

public function testParsing() {
Expand Down Expand Up @@ -104,6 +123,77 @@ public function testBlocksCache() {
$this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 1)), '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
Expand Down
3 changes: 1 addition & 2 deletions tests/view/SSViewerTest.php
Expand Up @@ -85,9 +85,8 @@ private function assertExpectedStrings($result, $expected) {
* Small helper to render templates from strings
*/
public function render($templateString, $data = null) {
$t = SSViewer::fromString($templateString);
if(!$data) $data = new SSViewerTestFixture();
return $t->process($data);
return SSViewer::execute_string($templateString, $data);
}

public function testRequirements() {
Expand Down
25 changes: 20 additions & 5 deletions view/SSTemplateParser.php
Expand Up @@ -67,7 +67,7 @@ function __construct($message, $parser) {
*
* Angle Bracket: angle brackets "<" and ">" are used to eat whitespace between template elements
* N: eats white space including newlines (using in legacy _t support)
*
*
* @package framework
* @subpackage view
*/
Expand Down Expand Up @@ -2938,10 +2938,25 @@ function CacheBlock_UncachedBlock(&$res, $sub){
function CacheBlock_CacheBlockTemplate(&$res, $sub){
// Get the block counter
$block = ++$res['subblocks'];
// Build the key for this block from the passed cache key, the block index, and the sha hash of the template
// itself
$key = "'" . sha1($sub['php']) . (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") .
".'_$block'";
// Build the key for this block from the global key (evaluated in a closure within the template),
// the passed cache key, the block index, and the sha hash of the template.
$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
$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
$condition = isset($res['condition']) ? $res['condition'] : '';

Expand Down
23 changes: 19 additions & 4 deletions view/SSTemplateParser.php.inc
Expand Up @@ -673,10 +673,25 @@ class SSTemplateParser extends Parser implements TemplateParser {
function CacheBlock_CacheBlockTemplate(&$res, $sub){
// Get the block counter
$block = ++$res['subblocks'];
// Build the key for this block from the passed cache key, the block index, and the sha hash of the template
// itself
$key = "'" . sha1($sub['php']) . (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") .
".'_$block'";
// Build the key for this block from the global key (evaluated in a closure within the template),
// the passed cache key, the block index, and the sha hash of the template.
$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
$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
$condition = isset($res['condition']) ? $res['condition'] : '';

Expand Down
31 changes: 30 additions & 1 deletion view/SSViewer.php
Expand Up @@ -48,7 +48,6 @@ class SSViewer_Scope {

private $localIndex;


public function __construct($item, $inheritedScope = null) {
$this->item = $item;
$this->localIndex = 0;
Expand Down Expand Up @@ -631,6 +630,14 @@ public static function get_source_file_comments() {
*/
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
*
Expand Down Expand Up @@ -1083,13 +1090,35 @@ public function process($item, $arguments = null, $inheritedScope = null) {
/**
* Execute the given template, passing it the given data.
* 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) {
$v = new SSViewer($template);
$v->includeRequirements(false);

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="") {
return $this->parser->compileString(
Expand Down

0 comments on commit f9c44e4

Please sign in to comment.