silverstripe-framework/core/SSViewer.php
2011-02-21 13:50:10 +13:00

919 lines
29 KiB
PHP
Executable File

<?php
/**
* This tracks the current scope for an SSViewer instance. It has three goals:
* - Handle entering & leaving sub-scopes in loops and withs
* - Track Up and Top
* - (As a side effect) Inject data that needs to be available globally (used to live in ViewableData)
*
* In order to handle up, rather than tracking it using a tree, which would involve constructing new objects
* for each step, we use indexes into the itemStack (which already has to exist).
*
* Each item has three indexes associated with it
*
* - Pop. Which item should become the scope once the current scope is popped out of
* - Up. Which item is up from this item
* - Current. Which item is the first time this object has appeared in the stack
*
* We also keep the index of the current starting point for lookups. A lookup is a sequence of obj calls -
* when in a loop or with tag the end result becomes the new scope, but for injections, we throw away the lookup
* and revert back to the original scope once we've got the value we're after
*
*/
class SSViewer_DataPresenter {
// The stack of previous "global" items
// And array of item, itemIterator, pop_index, up_index, current_index
private $itemStack = array();
private $item; // The current "global" item (the one any lookup starts from)
private $itemIterator; // If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
private $popIndex; // A pointer into the item stack for which item should be scope on the next pop call
private $upIndex; // A pointer into the item stack for which item is "up" from this one
private $currentIndex; // A pointer into the item stack for which item is this one (or null if not in stack yet)
private $localIndex;
function __construct($item){
$this->item = $item;
$this->localIndex=0;
$this->itemStack[] = array($this->item, null, null, null, 0);
}
function getItem(){
return $this->itemIterator ? $this->itemIterator->current() : $this->item;
}
function resetLocalScope(){
list($this->item, $this->itemIterator, $this->popIndex, $this->upIndex, $this->currentIndex) = $this->itemStack[$this->localIndex];
array_splice($this->itemStack, $this->localIndex+1);
}
function obj($name){
switch ($name) {
case 'Up':
list($this->item, $this->itemIterator, $unused2, $this->upIndex, $this->currentIndex) = $this->itemStack[$this->upIndex];
break;
case 'Top':
list($this->item, $this->itemIterator, $unused2, $this->upIndex, $this->currentIndex) = $this->itemStack[0];
break;
default:
$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
$this->item = call_user_func_array(array($on, 'obj'), func_get_args());
$this->itemIterator = null;
$this->upIndex = $this->currentIndex ? $this->currentIndex : count($this->itemStack)-1;
$this->currentIndex = count($this->itemStack);
break;
}
$this->itemStack[] = array($this->item, $this->itemIterator, null, $this->upIndex, $this->currentIndex);
return $this;
}
function pushScope(){
$newLocalIndex = count($this->itemStack)-1;
$this->popIndex = $this->itemStack[$newLocalIndex][2] = $this->localIndex;
$this->localIndex = $newLocalIndex;
// We normally keep any previous itemIterator around, so local $Up calls reference the right element. But
// once we enter a new global scope, we need to make sure we use a new one
$this->itemIterator = $this->itemStack[$newLocalIndex][1] = null;
return $this;
}
function popScope(){
$this->localIndex = $this->popIndex;
$this->resetLocalScope();
return $this;
}
function next(){
if (!$this->item) return false;
if (!$this->itemIterator) {
if (is_array($this->item)) $this->itemIterator = new ArrayIterator($this->item);
else $this->itemIterator = $this->item->getIterator();
$this->itemStack[$this->localIndex][1] = $this->itemIterator;
$this->itemIterator->rewind();
}
else {
$this->itemIterator->next();
}
$this->resetLocalScope();
if (!$this->itemIterator->valid()) return false;
return $this->itemIterator->key();
}
function __call($name, $arguments) {
$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
$retval = call_user_func_array(array($on, $name), $arguments);
$this->resetLocalScope();
return $retval;
}
}
/**
* Parses a template file with an *.ss file extension.
*
* In addition to a full template in the templates/ folder, a template in
* templates/Content or templates/Layout will be rendered into $Content and
* $Layout, respectively.
*
* A single template can be parsed by multiple nested {@link SSViewer} instances
* through $Layout/$Content placeholders, as well as <% include MyTemplateFile %> template commands.
*
* <b>Themes</b>
*
* See http://doc.silverstripe.org/themes and http://doc.silverstripe.org/themes:developing
*
* <b>Caching</b>
*
* Compiled templates are cached via {@link SS_Cache}, usually on the filesystem.
* If you put ?flush=all on your URL, it will force the template to be recompiled.
*
* <b>Manifest File and Structure</b>
*
* Works with the global $_TEMPLATE_MANIFEST which is compiled by {@link ManifestBuilder->getTemplateManifest()}.
* This associative array lists all template filepaths by "identifier", meaning the name
* of the template without its path or extension.
*
* Example:
* <code>
* array(
* 'LeftAndMain' =>
* array (
* 'main' => '/my/system/path/cms/templates/LeftAndMain.ss',
* ),
* 'CMSMain_left' =>
* array (
* 'Includes' => '/my/system/path/cms/templates/Includes/CMSMain_left.ss',
* ),
* 'Page' =>
* array (
* 'themes' =>
* array (
* 'blackcandy' =>
* array (
* 'Layout' => '/my/system/path/themes/blackcandy/templates/Layout/Page.ss',
* 'main' => '/my/system/path/themes/blackcandy/templates/Page.ss',
* ),
* 'blue' =>
* array (
* 'Layout' => '/my/system/path/themes/mysite/templates/Layout/Page.ss',
* 'main' => '/my/system/path/themes/mysite/templates/Page.ss',
* ),
* ),
* ),
* // ...
* )
* </code>
*
* @see http://doc.silverstripe.org/themes
* @see http://doc.silverstripe.org/themes:developing
*
* @package sapphire
* @subpackage view
*/
class SSViewer {
/**
* @var boolean $source_file_comments
*/
protected static $source_file_comments = false;
/**
* Set whether HTML comments indicating the source .SS file used to render this page should be
* included in the output. This is enabled by default
*
* @param boolean $val
*/
static function set_source_file_comments($val) {
self::$source_file_comments = $val;
}
/**
* @return boolean
*/
static function get_source_file_comments() {
return self::$source_file_comments;
}
/**
* @var array $chosenTemplates Associative array for the different
* template containers: "main" and "Layout". Values are absolute file paths to *.ss files.
*/
private $chosenTemplates = array();
/**
* @var boolean
*/
protected $rewriteHashlinks = true;
/**
* @var string
*/
protected static $current_theme = null;
/**
* @var string
*/
protected static $current_custom_theme = null;
/**
* Create a template from a string instead of a .ss file
*
* @return SSViewer
*/
static function fromString($content) {
return new SSViewer_FromString($content);
}
/**
* @param string $theme The "base theme" name (without underscores).
*/
static function set_theme($theme) {
self::$current_theme = $theme;
//Static publishing needs to have a theme set, otherwise it defaults to the content controller theme
if(!is_null($theme))
self::$current_custom_theme=$theme;
}
/**
* @return string
*/
static function current_theme() {
return self::$current_theme;
}
/**
* Returns the path to the theme folder
*
* @return String
*/
static function get_theme_folder() {
return self::current_theme() ? THEMES_DIR . "/" . self::current_theme() : project();
}
/**
* @return string
*/
static function current_custom_theme(){
return self::$current_custom_theme;
}
/**
* @param string|array $templateList If passed as a string with .ss extension, used as the "main" template.
* If passed as an array, it can be used for template inheritance (first found template "wins").
* Usually the array values are PHP class names, which directly correlate to template names.
* <code>
* array('MySpecificPage', 'MyPage', 'Page')
* </code>
*/
public function __construct($templateList) {
global $_TEMPLATE_MANIFEST;
// flush template manifest cache if requested
if (isset($_GET['flush']) && $_GET['flush'] == 'all') {
if(Director::isDev() || Director::is_cli() || Permission::check('ADMIN')) {
self::flush_template_cache();
} else {
return Security::permissionFailure(null, 'Please log in as an administrator to flush the template cache.');
}
}
if(substr((string) $templateList,-3) == '.ss') {
$this->chosenTemplates['main'] = $templateList;
} else {
if(!is_array($templateList)) $templateList = array($templateList);
if(isset($_GET['debug_request'])) Debug::message("Selecting templates from the following list: " . implode(", ", $templateList));
foreach($templateList as $template) {
// if passed as a partial directory (e.g. "Layout/Page"), split into folder and template components
if(strpos($template,'/') !== false) list($templateFolder, $template) = explode('/', $template, 2);
else $templateFolder = null;
// Use the theme template if available
if(self::current_theme() && isset($_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()])) {
$this->chosenTemplates = array_merge(
$_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()],
$this->chosenTemplates
);
if(isset($_GET['debug_request'])) Debug::message("Found template '$template' from main theme '" . self::current_theme() . "': " . var_export($_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()], true));
}
// Fall back to unthemed base templates
if(isset($_TEMPLATE_MANIFEST[$template]) && (array_keys($_TEMPLATE_MANIFEST[$template]) != array('themes'))) {
$this->chosenTemplates = array_merge(
$_TEMPLATE_MANIFEST[$template],
$this->chosenTemplates
);
if(isset($_GET['debug_request'])) Debug::message("Found template '$template' from main template archive, containing the following items: " . var_export($_TEMPLATE_MANIFEST[$template], true));
unset($this->chosenTemplates['themes']);
}
if($templateFolder) {
$this->chosenTemplates['main'] = $this->chosenTemplates[$templateFolder];
unset($this->chosenTemplates[$templateFolder]);
}
}
if(isset($_GET['debug_request'])) Debug::message("Final template selections made: " . var_export($this->chosenTemplates, true));
}
if(!$this->chosenTemplates) user_error("None of these templates can be found in theme '"
. self::current_theme() . "': ". implode(".ss, ", $templateList) . ".ss", E_USER_WARNING);
}
/**
* Returns true if at least one of the listed templates exists
*/
static function hasTemplate($templateList) {
if(!is_array($templateList)) $templateList = array($templateList);
global $_TEMPLATE_MANIFEST;
foreach($templateList as $template) {
if(strpos($template,'/') !== false) list($templateFolder, $template) = explode('/', $template, 2);
if(isset($_TEMPLATE_MANIFEST[$template])) return true;
}
return false;
}
/**
* Set a global rendering option.
* The following options are available:
* - rewriteHashlinks: If true (the default), <a href="#..."> will be rewritten to contain the
* current URL. This lets it play nicely with our <base> tag.
* - If rewriteHashlinks = 'php' then, a piece of PHP script will be inserted before the hash
* links: "<?php echo $_SERVER['REQUEST_URI']; ?>". This is useful if you're generating a
* page that will be saved to a .php file and may be accessed from different URLs.
*/
public static function setOption($optionName, $optionVal) {
SSViewer::$options[$optionName] = $optionVal;
}
protected static $options = array(
'rewriteHashlinks' => true,
);
protected static $topLevel = array();
public static function topLevel() {
if(SSViewer::$topLevel) {
return SSViewer::$topLevel[sizeof(SSViewer::$topLevel)-1];
}
}
/**
* Call this to disable rewriting of <a href="#xxx"> links. This is useful in Ajax applications.
* It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process();
*/
public function dontRewriteHashlinks() {
$this->rewriteHashlinks = false;
self::$options['rewriteHashlinks'] = false;
return $this;
}
public function exists() {
return $this->chosenTemplates;
}
/**
* Searches for a template name in the current theme:
* - themes/mytheme/templates
* - themes/mytheme/templates/Includes
* Falls back to unthemed template files.
*
* Caution: Doesn't search in any /Layout folders.
*
* @param string $identifier A template name without '.ss' extension or path.
* @return string Full system path to a template file
*/
public static function getTemplateFile($identifier) {
global $_TEMPLATE_MANIFEST;
$includeTemplateFile = self::getTemplateFileByType($identifier, 'Includes');
if($includeTemplateFile) return $includeTemplateFile;
$mainTemplateFile = self::getTemplateFileByType($identifier, 'main');
if($mainTemplateFile) return $mainTemplateFile;
return false;
}
/**
* @param string $identifier A template name without '.ss' extension or path
* @param string $type The template type, either "main", "Includes" or "Layout"
* @return string Full system path to a template file
*/
public static function getTemplateFileByType($identifier, $type) {
global $_TEMPLATE_MANIFEST;
if(self::current_theme() && isset($_TEMPLATE_MANIFEST[$identifier]['themes'][self::current_theme()][$type])) {
return $_TEMPLATE_MANIFEST[$identifier]['themes'][self::current_theme()][$type];
} else if(isset($_TEMPLATE_MANIFEST[$identifier][$type])){
return $_TEMPLATE_MANIFEST[$identifier][$type];
} else {
return false;
}
}
/**
* Used by <% include Identifier %> statements to get the full
* unparsed content of a template file.
*
* @uses getTemplateFile()
* @param string $identifier A template name without '.ss' extension or path.
* @return string content of template
*/
public static function getTemplateContent($identifier) {
if(!SSViewer::getTemplateFile($identifier)) {
return null;
}
$content = file_get_contents(SSViewer::getTemplateFile($identifier));
// $content = "<!-- getTemplateContent() :: identifier: $identifier -->". $content;
// Adds an i18n namespace to all _t(...) calls without an existing one
// to avoid confusion when using the include in different contexts.
// Entities without a namespace are deprecated, but widely used.
$content = ereg_replace('<' . '% +_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '<?= _t(\''. $identifier . '.ss' . '.\\2\\3\'\\4) ?>', $content);
// Remove UTF-8 byte order mark
// This is only necessary if you don't have zend-multibyte enabled.
if(substr($content, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
$content = substr($content, 3);
}
return $content;
}
/**
* @ignore
*/
static private $flushed = false;
/**
* Clears all parsed template files in the cache folder.
*
* Can only be called once per request (there may be multiple SSViewer instances).
*/
static function flush_template_cache() {
if (!self::$flushed) {
$dir = dir(TEMP_FOLDER);
while (false !== ($file = $dir->read())) {
if (strstr($file, '.cache')) { unlink(TEMP_FOLDER.'/'.$file); }
}
self::$flushed = true;
}
}
/**
* The process() method handles the "meat" of the template processing.
* It takes care of caching the output (via {@link SS_Cache}),
* as well as replacing the special "$Content" and "$Layout"
* placeholders with their respective subtemplates.
* The method injects extra HTML in the header via {@link Requirements::includeInHTML()}.
*
* Note: You can call this method indirectly by {@link ViewableData->renderWith()}.
*
* @param ViewableData $item
* @param SS_Cache $cache Optional cache backend
* @return String Parsed template output.
*/
public function process($item, $cache = null) {
SSViewer::$topLevel[] = $item;
if (!$cache) $cache = SS_Cache::factory('cacheblock');
if(isset($this->chosenTemplates['main'])) {
$template = $this->chosenTemplates['main'];
} else {
$template = $this->chosenTemplates[ reset($dummy = array_keys($this->chosenTemplates)) ];
}
if(isset($_GET['debug_profile'])) Profiler::mark("SSViewer::process", " for $template");
$cacheFile = TEMP_FOLDER . "/.cache" . str_replace(array('\\','/',':'), '.', Director::makeRelative(realpath($template)));
$lastEdited = filemtime($template);
if(!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited || isset($_GET['flush'])) {
if(isset($_GET['debug_profile'])) Profiler::mark("SSViewer::process - compile", " for $template");
$content = file_get_contents($template);
$content = SSViewer::parseTemplateContent($content, $template);
$fh = fopen($cacheFile,'w');
fwrite($fh, $content);
fclose($fh);
if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process - compile", " for $template");
}
if(isset($_GET['showtemplate']) && !Director::isLive()) {
$lines = file($cacheFile);
echo "<h2>Template: $cacheFile</h2>";
echo "<pre>";
foreach($lines as $num => $line) {
echo str_pad($num+1,5) . htmlentities($line);
}
echo "</pre>";
}
// Makes the rendered sub-templates available on the parent item,
// through $Content and $Layout placeholders.
foreach(array('Content', 'Layout') as $subtemplate) {
if(isset($this->chosenTemplates[$subtemplate])) {
$subtemplateViewer = new SSViewer($this->chosenTemplates[$subtemplate]);
$item = $item->customise(array(
$subtemplate => $subtemplateViewer->process($item, $cache)
));
}
}
$scope = new SSViewer_DataPresenter($item);
$val = ""; $valStack = array();
include($cacheFile);
$output = Requirements::includeInHTML($template, $val);
array_pop(SSViewer::$topLevel);
if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process", " for $template");
// If we have our crazy base tag, then fix # links referencing the current page.
if($this->rewriteHashlinks && self::$options['rewriteHashlinks']) {
if(strpos($output, '<base') !== false) {
if(SSViewer::$options['rewriteHashlinks'] === 'php') {
$thisURLRelativeToBase = "<?php echo \$_SERVER['REQUEST_URI']; ?>";
} else {
$thisURLRelativeToBase = Director::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
}
$output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);
}
}
return $output;
}
/**
* Execute the given template, passing it the given data.
* Used by the <% include %> template tag to process templates.
*/
static function execute_template($template, $data) {
$v = new SSViewer($template);
return $v->process($data);
}
static function parseTemplateContent($content, $template="") {
return SSTemplateParser::compileString($content, $template, Director::isDev() && self::$source_file_comments);
}
/**
* Returns the filenames of the template that will be rendered. It is a map that may contain
* 'Content' & 'Layout', and will have to contain 'main'
*/
public function templates() {
return $this->chosenTemplates;
}
/**
* @param string $type "Layout" or "main"
* @param string $file Full system path to the template file
*/
public function setTemplateFile($type, $file) {
$this->chosenTemplates[$type] = $file;
}
/**
* Return an appropriate base tag for the given template.
* It will be closed on an XHTML document, and unclosed on an HTML document.
*
* @param $contentGeneratedSoFar The content of the template generated so far; it should contain
* the DOCTYPE declaration.
*/
static function get_base_tag($contentGeneratedSoFar) {
$base = Director::absoluteBaseURL();
// Is the document XHTML?
if(preg_match('/<!DOCTYPE[^>]+xhtml/i', $contentGeneratedSoFar)) {
return "<base href=\"$base\" />";
} else {
return "<base href=\"$base\"><!--[if lte IE 6]></base><![endif]-->";
}
}
}
/**
* Special SSViewer that will process a template passed as a string, rather than a filename.
* @package sapphire
* @subpackage view
*/
class SSViewer_FromString extends SSViewer {
protected $content;
public function __construct($content) {
$this->content = $content;
}
public function process($item, $cache = null) {
$template = SSViewer::parseTemplateContent($this->content, "string sha1=".sha1($this->content));
$tmpFile = tempnam(TEMP_FOLDER,"");
$fh = fopen($tmpFile, 'w');
fwrite($fh, $template);
fclose($fh);
if(isset($_GET['showtemplate']) && $_GET['showtemplate']) {
$lines = file($tmpFile);
echo "<h2>Template: $tmpFile</h2>";
echo "<pre>";
foreach($lines as $num => $line) {
echo str_pad($num+1,5) . htmlentities($line);
}
echo "</pre>";
}
$scope = new SSViewer_DataPresenter($item);
$val = "";
$valStack = array();
$cache = SS_Cache::factory('cacheblock');
include($tmpFile);
unlink($tmpFile);
return $val;
}
}
/**
* Handle the parsing for cacheblock tags.
*
* Needs to be handled differently from the other tags, because cacheblock can take any number of arguments
*
* This shouldn't be used as an example of how to add functionality to SSViewer - the eventual plan is to re-write
* SSViewer using a proper parser (probably http://github.com/hafriedlander/php-peg), so that extra functionality
* can be added without relying on ad-hoc parsers like this.
*
* @package sapphire
* @subpackage view
*/
class SSViewer_PartialParser {
static $tag = '/< % [ \t]+ (cached|cacheblock|uncached|end_cached|end_cacheblock|end_uncached) [ \t]+ ([^%]+ [ \t]+)? % >/xS';
static $argument_splitter = '/^\s*
# The argument itself
(
(?P<conditional> if | unless ) | # The if or unless keybreak
(?P<property> (?P<identifier> \w+) \s* # A property lookup or a function call
( \( (?P<arguments> [^\)]*) \) )?
) |
(?P<sqstring> \' (\\\'|[^\'])+ \' ) | # A string surrounded by \'
(?P<dqstring> " (\\"|[^"])+ " ) # A string surrounded by "
)
# Some seperator after the argument
(
\s*(?P<comma>,)\s* | # A comma (maybe with whitespace before or after)
(?P<fullstop>\.) # A period (no whitespace before)
)?
/xS';
static function process($template, $content) {
$parser = new SSViewer_PartialParser($template, $content, 0);
$parser->parse();
return $parser->generate();
}
function __construct($template, $content, $offset) {
$this->template = $template;
$this->content = $content;
$this->offset = $offset;
$this->blocks = array();
}
function controlcheck($text) {
// NOP - hook for Cached_PartialParser
}
function parse() {
$current_tag_offset = 0;
while (preg_match(self::$tag, $this->content, $matches, PREG_OFFSET_CAPTURE, $this->offset)) {
$tag = $matches[1][0];
$startpos = $matches[0][1];
$endpos = $matches[0][1] + strlen($matches[0][0]);
switch($tag) {
case 'cached':
case 'uncached':
case 'cacheblock':
$pretext = substr($this->content, $this->offset, $startpos - $this->offset);
$this->controlcheck($pretext);
$this->blocks[] = $pretext;
if ($tag == 'cached' || $tag == 'cacheblock') {
list($keyparts, $conditional, $condition) = $this->parseargs(@$matches[2][0]);
$parser = new SSViewer_Cached_PartialParser($this->template, $this->content, $endpos, $keyparts, $conditional, $condition);
}
else {
$parser = new SSViewer_PartialParser($this->template, $this->content, $endpos);
}
$parser->parse();
$this->blocks[] = $parser;
$this->offset = $parser->offset;
break;
case 'end_cached':
case 'end_cacheblock':
case 'end_uncached':
$this->blocks[] = substr($this->content, $this->offset, $startpos - $this->offset);
$this->content = null;
$this->offset = $endpos;
return $this;
}
}
$this->blocks[] = substr($this->content, $this->offset);
$this->content = null;
}
function parseargs($string) {
preg_match_all(self::$argument_splitter, $string, $matches, PREG_SET_ORDER);
$parts = array();
$conditional = null; $condition = null;
$current = '$item->';
while (strlen($string) && preg_match(self::$argument_splitter, $string, $match)) {
$string = substr($string, strlen($match[0]));
// If this is a conditional keyword, break, and the next loop will grab the conditional
if (@$match['conditional']) {
$conditional = $match['conditional'];
continue;
}
// If it's a property lookup or a function call
if (@$match['property']) {
// Get the property
$what = $match['identifier'];
$args = array();
// Extract any arguments passed to the function call
if (@$match['arguments']) {
foreach (explode(',', $match['arguments']) as $arg) {
$args[] = is_numeric($arg) ? (string)$arg : '"'.$arg.'"';
}
}
$args = empty($args) ? 'null' : 'array('.implode(',',$args).')';
// If this fragment ended with '.', then there's another lookup coming, so return an obj for that lookup
if (@$match['fullstop']) {
$current .= "obj('$what', $args, true)->";
}
// Otherwise this is the end of the lookup chain, so add the resultant value to the key array and reset the key-get php fragement
else {
$accessor = $current . "XML_val('$what', $args, true)"; $current = '$item->';
// If we've hit a conditional already, this is the condition. Set it and be done.
if ($conditional) {
$condition = $accessor;
break;
}
// Otherwise we're another key component. Add it to array.
else $parts[] = $accessor;
}
}
// Else it's a quoted string of some kind
else if (@$match['sqstring']) $parts[] = $match['sqstring'];
else if (@$match['dqstring']) $parts[] = $match['dqstring'];
}
if ($conditional && !$condition) {
throw new Exception("You need to have a condition after the conditional $conditional in your cache block");
}
return array($parts, $conditional, $condition);
}
function generate() {
$res = array();
foreach ($this->blocks as $i => $block) {
if ($block instanceof SSViewer_PartialParser)
$res[] = $block->generate();
else {
$res[] = $block;
}
}
return implode('', $res);
}
}
/**
* @package sapphire
* @subpackage view
*/
class SSViewer_Cached_PartialParser extends SSViewer_PartialParser {
function __construct($template, $content, $offset, $keyparts, $conditional, $condition) {
$this->keyparts = $keyparts;
$this->conditional = $conditional;
$this->condition = $condition;
parent::__construct($template, $content, $offset);
}
function controlcheck($text) {
$ifs = preg_match_all('/<'.'% +if +/', $text, $matches);
$end_ifs = preg_match_all('/<'.'% +end_if +/', $text, $matches);
if ($ifs != $end_ifs) throw new Exception('You can\'t have cached or uncached blocks within condition structures');
$controls = preg_match_all('/<'.'% +control +/', $text, $matches);
$end_controls = preg_match_all('/<'.'% +end_control +/', $text, $matches);
if ($controls != $end_controls) throw new Exception('You can\'t have cached or uncached blocks within control structures');
}
function key() {
if (empty($this->keyparts)) return "''";
return 'sha1(' . implode(".'_'.", $this->keyparts) . ')';
}
function generate() {
$res = array();
$key = $this->key();
$condition = "";
switch ($this->conditional) {
case 'if':
$condition = "{$this->condition} && ";
break;
case 'unless':
$condition = "!({$this->condition}) && ";
break;
}
/* Output this set of blocks */
foreach ($this->blocks as $i => $block) {
if ($block instanceof SSViewer_PartialParser)
$res[] = $block->generate();
else {
// Include the template name and this cache block's current contents as a sha hash, so we get auto-seperation
// of cache blocks, and invalidation of the cache when the template changes
$partialkey = "'".sha1($this->template . $block)."_'.$key.'_$i'";
// Try to load from cache
$res[] = "<?\n".'if ('.$condition.' ($partial = $cache->load('.$partialkey.'))) $val .= $partial;'."\n";
// Cache miss - regenerate
$res[] = "else {\n";
$res[] = '$oldval = $val; $val = "";'."\n";
$res[] = "\n?>" . $block . "<?\n";
$res[] = $condition . ' $cache->save($val); $val = $oldval . $val ;'."\n";
$res[] = "}\n?>";
}
}
return implode('', $res);
}
}
function supressOutput() {
return "";
}
?>