Merge branch '3.1'

Conflicts:
	tests/view/SSViewerTest.php
This commit is contained in:
Simon Welsh 2014-03-30 18:17:24 +13:00
commit fe8dc50ffc
29 changed files with 570 additions and 113 deletions

View File

@ -3,7 +3,6 @@
HtmlEditorConfig::get('cms')->setOptions(array(
'friendly_name' => 'Default CMS',
'priority' => '50',
'mode' => 'none', // initialized through LeftAndMain.EditFor.js logic
'body_class' => 'typography',
'document_base_url' => isset($_SERVER['HTTP_HOST']) ? Director::absoluteBaseURL() : null,

View File

@ -234,12 +234,14 @@ class SS_HTTPResponse {
}
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)) {

View File

@ -64,27 +64,26 @@ class SS_TemplateLoader {
public function findTemplates($templates, $theme = null) {
$result = array();
$project = project();
foreach ((array) $templates as $template) {
$found = false;
if (strpos($template, '/')) {
list($type, $template) = explode('/', $template, 2);
} else {
$type = null;
}
if ($found = $this->getManifest()->getCandidateTemplate($template, $theme)) {
if ($type && isset($found[$type])) {
$found = array(
'main' => $found[$type]
);
$found = array(
'main' => $found[$type]
);
}
$result = array_merge($found, $result);
}
}
return $result;
}

View File

@ -110,17 +110,23 @@ class SS_TemplateManifest {
* @return array
*/
public function getCandidateTemplate($name, $theme = null) {
$found = array();
$candidates = $this->getTemplate($name);
if ($this->project && isset($candidates[$this->project])) {
$found = $candidates[$this->project];
} else if ($theme && isset($candidates['themes'][$theme])) {
// theme overrides modules
if ($theme && isset($candidates['themes'][$theme])) {
$found = array_merge($candidates, $candidates['themes'][$theme]);
} else {
$found = $candidates;
}
if(isset($found['themes'])) unset($found['themes']);
// project overrides theme
if ($this->project && isset($candidates[$this->project])) {
$found = array_merge($found, $candidates[$this->project]);
}
$found = ($found) ? $found : $candidates;
if (isset($found['themes'])) unset($found['themes']);
if (isset($found[$this->project])) unset($found[$this->project]);
return $found;
}

View File

@ -53,3 +53,4 @@ Used in side panels and action tabs
.ss-upload .clear { clear: both; }
.ss-upload .ss-uploadfield-fromcomputer input { /* since we can't really style the file input, we use this hack to make it as big as the button and hide it */ position: absolute; top: 0; right: 0; margin: 0; opacity: 0; filter: alpha(opacity=0); transform: translate(-300px, 0) scale(4); font-size: 23px; direction: ltr; cursor: pointer; height: 30px; line-height: 30px; }
.ss-upload .loader { height: 94px; background: transparent url(../admin/images/spinner.gif) no-repeat 50% 50%; }

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

@ -83,10 +83,17 @@ a new instance of the class to the [api:GridFieldConfig] object. The `GridField`
manipulating the `GridFieldConfig` instance if required.
:::php
// option 1: creating a new GridField with the CustomAction
$config = GridFieldConfig::create();
$config->addComponent(new GridFieldCustomAction());
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
// option 2: adding the CustomAction to an exisitng GridField
$gridField->getConfig()->addComponent(new GridFieldCustomAction());
For documentation on adding a Component to a `GridField` created by `ModelAdmin`
please view the [ModelAdmin Reference](/reference/modeladmin#gridfield-customization) section `GridField Customization`
Now let's go back and dive through the `GridFieldCustomAction` class in more
detail.

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: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.

View File

@ -160,19 +160,42 @@ For example, we might want to have a checkbox which limits search results to exp
return $list;
}
}
### GridField Customization
To alter how the results are displayed (via `[api:GridField]`), you can also overload the `getEditForm()` method. For example, to add a new component.
:::php
class MyAdmin extends ModelAdmin {
private static $managed_models = array('Product','Category');
// ...
public function getEditForm($id = null, $fields = null) {
$form = parent::getEditForm($id, $fields);
$gridField = $form->Fields()->fieldByName($this->sanitiseClassName($this->modelClass));
// $gridFieldName is generated from the ModelClass, eg if the Class 'Product'
// is managed by this ModelAdmin, the GridField for it will also be named 'Product'
$gridFieldName = $this->sanitiseClassName($this->modelClass);
$gridField = $form->Fields()->fieldByName($gridFieldName);
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
return $form;
}
}
The above example will add the component to all `GridField`s (of all managed models). Alternatively we can also add it to only one specific `GridField`:
:::php
class MyAdmin extends ModelAdmin {
private static $managed_models = array('Product','Category');
// ...
public function getEditForm($id = null, $fields = null) {
$form = parent::getEditForm($id, $fields);
$gridFieldName = 'Product';
$gridField = $form->Fields()->fieldByName($gridFieldName);
if ($gridField) {
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
}
return $form;
}
}
## Managing Relationships

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 %>
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.

View File

@ -2,8 +2,7 @@
## Introduction
`[api:RestfulService]` enables connecting to remote web services which supports REST interface and consume those web services
(for example [Flickr](http://www.flickr.com/services/api/), [Youtube](http://code.google.com/apis/youtube/overview.html), Amazon and etc). `[api:RestfulService]` can parse the XML response (sorry no JSON support)
`[api:RestfulService]` uses the php curl library, enabling connections to remote web services which support a REST interface and consuming those web services. (Examples: [Flickr](http://www.flickr.com/services/api/), [Youtube](http://code.google.com/apis/youtube/overview.html), Amazon and etc). `[api:RestfulService]` can parse the XML response (sorry no JSON support)
returned from the web service. Further it supports caching of the response, and you can customize the cache interval.
To gain the functionality you can either create a new `[api:RestfulService]` object or create a class extending the

View File

@ -107,6 +107,10 @@ class File extends DataObject {
private static $extensions = array(
"Hierarchy",
);
private static $casting = array (
'TreeTitle' => 'HTMLText'
);
/**
* @config

View File

@ -24,10 +24,6 @@ class Folder extends File {
private static $plural_name = "Folders";
private static $default_sort = "\"Name\"";
private static $casting = array (
'TreeTitle' => 'HTMLText'
);
/**
*

View File

@ -69,7 +69,7 @@ class HtmlEditorConfig {
protected $settings = array(
'friendly_name' => '(Please set a friendly name for this config)',
'priority' => 0,
'mode' => "specific_textareas",
'mode' => "none", // initialized through HtmlEditorField.js redraw() logic
'editor_selector' => "htmleditor",
'width' => "100%",
'auto_resize' => false,

View File

@ -295,18 +295,30 @@
dialog.ssdialog('open');
},
attachFiles: function(ids, uploadedFileId) {
var self = this, config = this.getConfig();
$.post(
config['urlAttach'],
{'ids': ids},
function(data, status, xhr) {
var self = this,
config = this.getConfig(),
indicator = $('<div class="loader" />'),
target = (uploadedFileId) ? this.find(".ss-uploadfield-item[data-fileid='"+uploadedFileId+"']") : this.find('.ss-uploadfield-addfile');
target.children().hide();
target.append(indicator);
$.ajax({
type: "POST",
url: config['urlAttach'],
data: {'ids': ids},
complete: function(xhr, status) {
target.children().show();
indicator.remove();
},
success: function(data, status, xhr) {
self.fileupload('attach', {
files: data,
options: self.fileupload('option'),
replaceFileID: uploadedFileId
});
}
);
});
}
});
$('div.ss-upload *').entwine({

View File

@ -147,12 +147,18 @@ class DB {
/**
* Connect to a database.
* Given the database configuration, this method will create the correct subclass of SS_Database,
* and set it as the global connection.
*
* Given the database configuration, this method will create the correct
* subclass of {@link SS_Database}.
*
* @param array $database A map of options. The 'type' is the name of the subclass of SS_Database to use. For the
* rest of the options, see the specific class.
* @param string $name identifier for the connection
*
* @return SS_Database
*/
public static function connect($databaseConfig) {
public static function connect($databaseConfig, $label = 'default') {
// This is used by the "testsession" module to test up a test session using an alternative name
if($name = self::get_alternative_database_name()) {
$databaseConfig['database'] = $name;
@ -167,7 +173,9 @@ class DB {
$dbClass = $databaseConfig['type'];
$conn = new $dbClass($databaseConfig);
self::setConn($conn);
self::setConn($conn, $label);
return $conn;
}
/**

View File

@ -67,7 +67,7 @@ class MySQLDatabase extends SS_Database {
} else {
$this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password']);
}
if($this->dbConn->connect_error) {
$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
}
@ -86,6 +86,12 @@ class MySQLDatabase extends SS_Database {
}
}
public function __destruct() {
if($this->dbConn) {
mysqli_close($this->dbConn);
}
}
/**
* Not implemented, needed for PDO
*/

View File

@ -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
@ -1358,6 +1357,12 @@ class Versioned extends DataExtension {
public function getDefaultStage() {
return $this->defaultStage;
}
public static function get_template_global_variables() {
return array(
'CurrentReadingMode' => 'get_reading_mode'
);
}
}
/**

View File

@ -152,6 +152,28 @@ class Date extends DBField {
}
}
/**
* Return a date formatted as per a CMS user's settings.
*
* @param Member $member
* @return boolean | string A date formatted as per user-defined settings.
*/
public function FormatFromSettings($member = null) {
require_once 'Zend/Date.php';
if(!$member) {
if(!Member::currentUserID()) {
return false;
}
$member = Member::currentUser();
}
$formatD = $member->getDateFormat();
$zendDate = new Zend_Date($this->getValue());
return $zendDate->toString($formatD);
}
/*
* Return a string in the form "12 - 16 Sept" or "12 Aug - 16 Sept"
* @param Date $otherDateObj Another date object specifying the end of the range

View File

@ -92,6 +92,29 @@ class SS_Datetime extends Date implements TemplateGlobalProvider {
public function Time24() {
if($this->value) return $this->Format('H:i');
}
/**
* Return a date and time formatted as per a CMS user's settings.
*
* @param Member $member
* @return boolean | string A time and date pair formatted as per user-defined settings.
*/
public function FormatFromSettings($member = null) {
require_once 'Zend/Date.php';
if(!$member) {
if(!Member::currentUserID()) {
return false;
}
$member = Member::currentUser();
}
$formatD = $member->getDateFormat();
$formatT = $member->getTimeFormat();
$zendDate = new Zend_Date($this->getValue());
return $zendDate->toString($formatD).' '.$zendDate->toString($formatT);
}
public function requireField() {
$parts=Array('datatype'=>'datetime', 'arrayValue'=>$this->arrayValue);

View File

@ -284,4 +284,8 @@
line-height: 30px;
}
}
.loader {
height: 94px; // Approxmiately matches the height of the field once a file is attached, avoids a 'jump' in size
background: transparent url(../admin/images/spinner.gif) no-repeat 50% 50%;
}
}

View File

@ -6,64 +6,184 @@
* @subpackage tests
*/
class TemplateLoaderTest extends SapphireTest {
public function testFindTemplates() {
$base = dirname(__FILE__) . '/fixtures/templatemanifest';
$manifest = new SS_TemplateManifest($base, 'myproject', false, true);
$loader = new SS_TemplateLoader();
$manifest->regenerate(false);
$loader->pushManifest($manifest);
$expectPage = array(
'main' => "$base/module/templates/Page.ss",
'Layout' => "$base/module/templates/Layout/Page.ss"
);
$expectPageThemed = array(
'main' => "$base/themes/theme/templates/Page.ss",
'Layout' => "$base/themes/theme/templates/Layout/Page.ss"
);
$this->assertEquals($expectPage, $loader->findTemplates('Page'));
$this->assertEquals($expectPage, $loader->findTemplates(array('Foo', 'Page')));
$this->assertEquals($expectPage, $loader->findTemplates('PAGE'));
$this->assertEquals($expectPageThemed, $loader->findTemplates('Page', 'theme'));
$expectPageLayout = array('main' => "$base/module/templates/Layout/Page.ss");
$expectPageLayoutThemed = array('main' => "$base/themes/theme/templates/Layout/Page.ss");
$this->assertEquals($expectPageLayout, $loader->findTemplates('Layout/Page'));
$this->assertEquals($expectPageLayout, $loader->findTemplates('Layout/PAGE'));
$this->assertEquals($expectPageLayoutThemed, $loader->findTemplates('Layout/Page', 'theme'));
$expectCustomPage = array(
'main' => "$base/module/templates/Page.ss",
'Layout' => "$base/module/templates/Layout/CustomPage.ss"
);
$this->assertEquals($expectCustomPage, $loader->findTemplates(array('CustomPage', 'Page')));
// 'main' template only exists in theme, and 'Layout' template only exists in module
$expectCustomThemePage = array(
'main' => "$base/themes/theme/templates/CustomThemePage.ss",
'Layout' => "$base/module/templates/Layout/CustomThemePage.ss"
);
$this->assertEquals($expectCustomThemePage, $loader->findTemplates(array('CustomThemePage', 'Page'), 'theme'));
private $base;
private $manifest;
private $loader;
/**
* Set up manifest before each test
*/
public function setUp() {
parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
$this->manifest = new SS_TemplateManifest($this->base, 'myproject', false, true);
$this->loader = new SS_TemplateLoader();
$this->refreshLoader();
}
public function testFindTemplatesApplicationOverridesModule() {
$base = dirname(__FILE__) . '/fixtures/templatemanifest';
$manifest = new SS_TemplateManifest($base, 'myproject', false, true);
$loader = new SS_TemplateLoader();
$manifest->regenerate(false);
$loader->pushManifest($manifest);
$expectPage = array(
'main' => "$base/myproject/templates/CustomTemplate.ss"
/**
* Test that 'main' and 'Layout' templates are loaded from module
*/
public function testFindTemplatesInModule() {
$expect = array(
'main' => "$this->base/module/templates/Page.ss",
'Layout' => "$this->base/module/templates/Layout/Page.ss"
);
$this->assertEquals($expectPage, $loader->findTemplates('CustomTemplate'));
$this->assertEquals($expect, $this->loader->findTemplates('Page'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page')));
}
/**
* Test that 'main' and 'Layout' templates are loaded from set theme
*/
public function testFindTemplatesInTheme() {
$expect = array(
'main' => "$this->base/themes/theme/templates/Page.ss",
'Layout' => "$this->base/themes/theme/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE', 'theme'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page'), 'theme'));
}
/**
* Test that 'main' and 'Layout' templates are loaded from project without a set theme
*/
public function testFindTemplatesInApplication() {
$templates = array(
$this->base . '/myproject/templates/Page.ss',
$this->base . '/myproject/templates/Layout/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page')));
$this->removeTestTemplates($templates);
}
/**
* Test that 'Layout' template is loaded from module
*/
public function testFindTemplatesInModuleLayout() {
$expect = array(
'main' => "$this->base/module/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Layout/Page'));
}
/**
* Test that 'Layout' template is loaded from theme
*/
public function testFindTemplatesInThemeLayout() {
$expect = array(
'main' => "$this->base/themes/theme/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Layout/Page', 'theme'));
}
/**
* Test that 'main' template is found in theme and 'Layout' is found in module
*/
public function testFindTemplatesMainThemeLayoutModule() {
$expect = array(
'main' => "$this->base/themes/theme/templates/CustomThemePage.ss",
'Layout' => "$this->base/module/templates/Layout/CustomThemePage.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates(array('CustomThemePage', 'Page'), 'theme'));
}
/**
* Test that project template overrides module template of same name
*/
public function testFindTemplatesApplicationOverridesModule() {
$expect = array(
'main' => "$this->base/myproject/templates/CustomTemplate.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('CustomTemplate'));
}
/**
* Test that project templates overrides theme templates
*/
public function testFindTemplatesApplicationOverridesTheme() {
$templates = array(
$this->base . '/myproject/templates/Page.ss',
$this->base . '/myproject/templates/Layout/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page'), 'theme');
$this->removeTestTemplates($templates);
}
/**
* Test that project 'Layout' template overrides theme 'Layout' template
*/
public function testFindTemplatesApplicationLayoutOverridesThemeLayout() {
$templates = array(
$this->base . '/myproject/templates/Layout/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/themes/theme/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->removeTestTemplates($templates);
}
/**
* Test that project 'main' template overrides theme 'main' template
*/
public function testFindTemplatesApplicationMainOverridesThemeMain() {
$templates = array(
$this->base . '/myproject/templates/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/themes/theme/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->removeTestTemplates($templates);
}
protected function refreshLoader() {
$this->manifest->regenerate(false);
$this->loader->pushManifest($this->manifest);
}
protected function createTestTemplates($templates) {
foreach ($templates as $template) {
file_put_contents($template, '');
}
}
protected function removeTestTemplates($templates) {
foreach ($templates as $template) {
unlink($template);
}
}
}

View File

@ -201,5 +201,27 @@ class DateTest extends SapphireTest {
SS_Datetime::clear_mock_now();
}
public function testFormatFromSettings() {
$memberID = $this->logInWithPermission();
$member = DataObject::get_by_id('Member', $memberID);
$member->DateFormat = 'dd/MM/YYYY';
$member->write();
$fixtures = array(
'2000-12-31' => '31/12/2000',
'31-12-2000' => '31/12/2000',
'31/12/2000' => '31/12/2000'
);
foreach($fixtures as $from => $to) {
$date = DBField::create_field('Date', $from);
// With member
$this->assertEquals($to, $date->FormatFromSettings($member));
// Without member
$this->assertEquals($to, $date->FormatFromSettings());
}
}
}

View File

@ -148,5 +148,30 @@ class SS_DatetimeTest extends SapphireTest {
SS_Datetime::clear_mock_now();
}
public function testFormatFromSettings() {
$memberID = $this->logInWithPermission();
$member = DataObject::get_by_id('Member', $memberID);
$member->DateFormat = 'dd/MM/YYYY';
$member->TimeFormat = 'hh:mm:ss';
$member->write();
$fixtures = array(
'2000-12-31 10:11:01' => '31/12/2000 10:11:01',
'2000-12-31 1:11:01' => '31/12/2000 01:11:01',
'12/12/2000 1:11:01' => '12/12/2000 01:11:01',
'2000-12-31' => '31/12/2000 12:00:00',
'10:11:01' => date('d/m/Y').' 10:11:01'
);
foreach($fixtures as $from => $to) {
$date = DBField::create_field('Datetime', $from);
// With member
$this->assertEquals($to, $date->FormatFromSettings($member));
// Without member
$this->assertEquals($to, $date->FormatFromSettings());
}
}
}

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 {
protected $extraDataObjects = array('SSViewerCacheBlockTest_Model');
protected $extraDataObjects = array(
'SSViewerCacheBlockTest_Model',
'SSViewerCacheBlockTest_VersionedModel'
);
protected $data = null;
@ -37,8 +57,7 @@ class SSViewerCacheBlockTest extends SapphireTest {
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() {
@ -104,6 +123,77 @@ class SSViewerCacheBlockTest extends SapphireTest {
$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

View File

@ -67,7 +67,7 @@ class SSTemplateParseException extends Exception {
*
* 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
*/
@ -2938,10 +2938,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'] : '';

View File

@ -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'] : '';

View File

@ -48,7 +48,6 @@ class SSViewer_Scope {
private $localIndex;
public function __construct($item, $inheritedScope = null) {
$this->item = $item;
$this->localIndex = 0;
@ -631,6 +630,14 @@ class SSViewer {
*/
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
*
@ -1096,6 +1103,11 @@ class SSViewer {
/**
* 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);
@ -1103,6 +1115,23 @@ class SSViewer {
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(