2016-08-19 00:51:35 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace SilverStripe\View;
|
|
|
|
|
2017-09-21 03:42:28 +02:00
|
|
|
use SilverStripe\Core\Config\Config;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Core\Config\Configurable;
|
|
|
|
use SilverStripe\Core\ClassInfo;
|
2017-02-23 20:39:57 +01:00
|
|
|
use Psr\SimpleCache\CacheInterface;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Core\Convert;
|
|
|
|
use SilverStripe\Core\Flushable;
|
|
|
|
use SilverStripe\Core\Injector\Injector;
|
2017-07-10 07:45:35 +02:00
|
|
|
use SilverStripe\Core\Injector\Injectable;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Control\Director;
|
|
|
|
use SilverStripe\Dev\Deprecation;
|
|
|
|
use SilverStripe\ORM\FieldType\DBField;
|
|
|
|
use SilverStripe\ORM\FieldType\DBHTMLText;
|
|
|
|
use SilverStripe\Security\Permission;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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>
|
|
|
|
*
|
2016-09-09 08:43:05 +02:00
|
|
|
* Compiled templates are cached via {@link Cache}, usually on the filesystem.
|
2016-08-19 00:51:35 +02:00
|
|
|
* If you put ?flush=1 on your URL, it will force the template to be recompiled.
|
|
|
|
*
|
|
|
|
* @see http://doc.silverstripe.org/themes
|
|
|
|
* @see http://doc.silverstripe.org/themes:developing
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
class SSViewer implements Flushable
|
|
|
|
{
|
|
|
|
use Configurable;
|
2017-07-10 07:45:35 +02:00
|
|
|
use Injectable;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Identifier for the default theme
|
|
|
|
*/
|
|
|
|
const DEFAULT_THEME = '$default';
|
|
|
|
|
2018-01-12 04:25:02 +01:00
|
|
|
/**
|
|
|
|
* Identifier for the public theme
|
|
|
|
*/
|
|
|
|
const PUBLIC_THEME = '$public';
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* A list (highest priority first) of themes to use
|
2017-09-19 18:23:53 +02:00
|
|
|
* Only used when {@link $theme_enabled} is set to TRUE.
|
2017-09-21 03:42:28 +02:00
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string
|
2017-09-19 18:23:53 +02:00
|
|
|
*/
|
|
|
|
private static $themes = [];
|
|
|
|
|
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* Overridden value of $themes config
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected static $current_themes = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The used "theme", which usually consists of templates, images and stylesheets.
|
|
|
|
* Only used when {@link $theme_enabled} is set to TRUE, and $themes is empty
|
|
|
|
*
|
2018-09-28 10:46:36 +02:00
|
|
|
* @deprecated 4.0.0:5.0.0
|
2017-09-19 18:23:53 +02:00
|
|
|
* @config
|
2017-09-21 03:42:28 +02:00
|
|
|
* @var string
|
2017-09-19 18:23:53 +02:00
|
|
|
*/
|
|
|
|
private static $theme = null;
|
|
|
|
|
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* Use the theme. Set to FALSE in order to disable themes,
|
2017-09-19 18:23:53 +02:00
|
|
|
* which can be useful for scenarios where theme overrides are temporarily undesired,
|
|
|
|
* such as an administrative interface separate from the website theme.
|
|
|
|
* It retains the theme settings to be re-enabled, for example when a website content
|
|
|
|
* needs to be rendered from within this administrative interface.
|
2017-09-21 03:42:28 +02:00
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var bool
|
2017-09-19 18:23:53 +02:00
|
|
|
*/
|
|
|
|
private static $theme_enabled = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default prepended cache key for partial caching
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $global_key = '$CurrentReadingMode, $CurrentUser.ID';
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* @config
|
2017-09-21 03:42:28 +02:00
|
|
|
* @var bool
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
private static $source_file_comments = false;
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* Set if hash links should be rewritten
|
|
|
|
*
|
2017-09-19 18:23:53 +02:00
|
|
|
* @config
|
2017-09-21 03:42:28 +02:00
|
|
|
* @var bool
|
2017-09-19 18:23:53 +02:00
|
|
|
*/
|
|
|
|
private static $rewrite_hash_links = true;
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* Overridden value of rewrite_hash_links config
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected static $current_rewrite_hash_links = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Instance variable to disable rewrite_hash_links (overrides global default)
|
|
|
|
* Leave null to use global state.
|
|
|
|
*
|
|
|
|
* @var bool|null
|
|
|
|
*/
|
|
|
|
protected $rewriteHashlinks = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
2016-11-29 00:31:16 +01:00
|
|
|
* @ignore
|
|
|
|
*/
|
|
|
|
private static $template_cache_flushed = false;
|
|
|
|
|
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* @internal
|
2016-11-29 00:31:16 +01:00
|
|
|
* @ignore
|
|
|
|
*/
|
|
|
|
private static $cacheblock_cache_flushed = false;
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* List of items being processed
|
|
|
|
*
|
|
|
|
* @var array
|
2017-09-19 18:23:53 +02:00
|
|
|
*/
|
|
|
|
protected static $topLevel = [];
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* List of templates to select from
|
|
|
|
*
|
|
|
|
* @var array
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-07-10 07:45:35 +02:00
|
|
|
protected $templates = null;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* Absolute path to chosen template file
|
|
|
|
*
|
|
|
|
* @var string
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-07-10 07:45:35 +02:00
|
|
|
protected $chosen = null;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* Templates to use when looking up 'Layout' or 'Content'
|
|
|
|
*
|
|
|
|
* @var array
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2021-08-05 12:10:06 +02:00
|
|
|
protected $subTemplates = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
2017-09-21 03:42:28 +02:00
|
|
|
* @var bool
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-09-19 18:23:53 +02:00
|
|
|
protected $includeRequirements = true;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
2017-09-19 18:23:53 +02:00
|
|
|
* @var TemplateParser
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-09-19 18:23:53 +02:00
|
|
|
protected $parser;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
2017-09-19 18:23:53 +02:00
|
|
|
* @var CacheInterface
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-09-19 18:23:53 +02:00
|
|
|
protected $partialCacheStore = null;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
2017-09-19 18:23:53 +02:00
|
|
|
* @param string|array $templates 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>
|
|
|
|
* @param TemplateParser $parser
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-09-19 18:23:53 +02:00
|
|
|
public function __construct($templates, TemplateParser $parser = null)
|
|
|
|
{
|
|
|
|
if ($parser) {
|
|
|
|
$this->setParser($parser);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
$this->setTemplate($templates);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
if (!$this->chosen) {
|
|
|
|
$message = 'None of the following templates could be found: ';
|
|
|
|
$message .= print_r($templates, true);
|
|
|
|
|
|
|
|
$themes = self::get_themes();
|
|
|
|
if (!$themes) {
|
|
|
|
$message .= ' (no theme in use)';
|
|
|
|
} else {
|
|
|
|
$message .= ' in themes "' . print_r($themes, true) . '"';
|
|
|
|
}
|
|
|
|
|
|
|
|
user_error($message, E_USER_WARNING);
|
|
|
|
}
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Triggered early in the request when someone requests a flush.
|
|
|
|
*/
|
|
|
|
public static function flush()
|
|
|
|
{
|
|
|
|
self::flush_template_cache(true);
|
|
|
|
self::flush_cacheblock_cache(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a template from a string instead of a .ss file
|
|
|
|
*
|
|
|
|
* @param string $content The template content
|
|
|
|
* @param bool|void $cacheTemplate Whether or not to cache the template from string
|
|
|
|
* @return SSViewer
|
|
|
|
*/
|
|
|
|
public static function fromString($content, $cacheTemplate = null)
|
|
|
|
{
|
2017-07-10 07:45:35 +02:00
|
|
|
$viewer = SSViewer_FromString::create($content);
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($cacheTemplate !== null) {
|
|
|
|
$viewer->setCacheTemplate($cacheTemplate);
|
|
|
|
}
|
|
|
|
return $viewer;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Assign the list of active themes to apply.
|
|
|
|
* If default themes should be included add $default as the last entry.
|
|
|
|
*
|
|
|
|
* @param array $themes
|
|
|
|
*/
|
|
|
|
public static function set_themes($themes = [])
|
|
|
|
{
|
2017-09-27 02:53:28 +02:00
|
|
|
static::$current_themes = $themes;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
/**
|
|
|
|
* Add to the list of active themes to apply
|
|
|
|
*
|
|
|
|
* @param array $themes
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public static function add_themes($themes = [])
|
|
|
|
{
|
2017-09-12 13:32:28 +02:00
|
|
|
$currentThemes = SSViewer::get_themes();
|
|
|
|
$finalThemes = array_merge($themes, $currentThemes);
|
|
|
|
// array_values is used to ensure sequential array keys as array_unique can leave gaps
|
2017-09-27 02:53:28 +02:00
|
|
|
static::set_themes(array_values(array_unique($finalThemes)));
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
/**
|
|
|
|
* Get the list of active themes
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public static function get_themes()
|
|
|
|
{
|
2018-01-12 04:25:02 +01:00
|
|
|
$default = [self::PUBLIC_THEME, self::DEFAULT_THEME];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-02-22 04:14:53 +01:00
|
|
|
if (!SSViewer::config()->uninherited('theme_enabled')) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Explicit list is assigned
|
2017-09-27 02:53:28 +02:00
|
|
|
$themes = static::$current_themes;
|
|
|
|
if (!isset($themes)) {
|
|
|
|
$themes = SSViewer::config()->uninherited('themes');
|
|
|
|
}
|
|
|
|
if ($themes) {
|
|
|
|
return $themes;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Support legacy behaviour
|
2017-02-22 04:14:53 +01:00
|
|
|
if ($theme = SSViewer::config()->uninherited('theme')) {
|
2018-01-12 04:25:02 +01:00
|
|
|
return [self::PUBLIC_THEME, $theme, self::DEFAULT_THEME];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-05-08 11:11:41 +02:00
|
|
|
* @deprecated 4.0.0:5.0.0 Use the "SSViewer#set_themes" instead
|
2016-11-29 00:31:16 +01:00
|
|
|
* @param string $theme The "base theme" name (without underscores).
|
|
|
|
*/
|
|
|
|
public static function set_theme($theme)
|
|
|
|
{
|
|
|
|
Deprecation::notice('4.0', 'Use the "SSViewer#set_themes" instead');
|
|
|
|
self::set_themes([$theme, self::DEFAULT_THEME]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Traverses the given the given class context looking for candidate template names
|
|
|
|
* which match each item in the class hierarchy. The resulting list of template candidates
|
|
|
|
* may or may not exist, but you can invoke {@see SSViewer::chooseTemplate} on any list
|
|
|
|
* to determine the best candidate based on the current themes.
|
|
|
|
*
|
|
|
|
* @param string|object $classOrObject Valid class name, or object
|
|
|
|
* @param string $suffix
|
|
|
|
* @param string $baseClass Class to halt ancestry search at
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public static function get_templates_by_class($classOrObject, $suffix = '', $baseClass = null)
|
|
|
|
{
|
|
|
|
// Figure out the class name from the supplied context.
|
|
|
|
if (!is_object($classOrObject) && !(
|
|
|
|
is_string($classOrObject) && class_exists($classOrObject)
|
|
|
|
)) {
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
'SSViewer::get_templates_by_class() expects a valid class name as its first parameter.'
|
|
|
|
);
|
|
|
|
}
|
2017-09-19 18:23:53 +02:00
|
|
|
|
|
|
|
$templates = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
$classes = array_reverse(ClassInfo::ancestry($classOrObject));
|
|
|
|
foreach ($classes as $class) {
|
|
|
|
$template = $class . $suffix;
|
|
|
|
$templates[] = $template;
|
|
|
|
$templates[] = ['type' => 'Includes', $template];
|
|
|
|
|
2016-12-30 00:27:20 +01:00
|
|
|
// If the class is "PageController" (PSR-2 compatibility) or "Page_Controller" (legacy), look for Page.ss
|
2017-01-11 02:49:09 +01:00
|
|
|
if (preg_match('/^(?<name>.+[^\\\\])_?Controller$/iU', $class, $matches)) {
|
2016-12-30 00:27:20 +01:00
|
|
|
$templates[] = $matches['name'] . $suffix;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($baseClass && $class == $baseClass) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2017-09-19 18:23:53 +02:00
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
return $templates;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-09-19 18:23:53 +02:00
|
|
|
* Get the current item being processed
|
|
|
|
*
|
|
|
|
* @return ViewableData
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-09-19 18:23:53 +02:00
|
|
|
public static function topLevel()
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-09-19 18:23:53 +02:00
|
|
|
if (SSViewer::$topLevel) {
|
|
|
|
return SSViewer::$topLevel[sizeof(SSViewer::$topLevel)-1];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-09-19 18:23:53 +02:00
|
|
|
return null;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-09-21 03:42:28 +02:00
|
|
|
/**
|
|
|
|
* Check if rewrite hash links are enabled on this instance
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function getRewriteHashLinks()
|
|
|
|
{
|
|
|
|
if (isset($this->rewriteHashlinks)) {
|
|
|
|
return $this->rewriteHashlinks;
|
|
|
|
}
|
|
|
|
return static::getRewriteHashLinksDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set if hash links are rewritten for this instance
|
|
|
|
*
|
|
|
|
* @param bool $rewrite
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setRewriteHashLinks($rewrite)
|
|
|
|
{
|
|
|
|
$this->rewriteHashlinks = $rewrite;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get default value for rewrite hash links for all modules
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public static function getRewriteHashLinksDefault()
|
|
|
|
{
|
|
|
|
// Check if config overridden
|
2017-09-27 02:53:28 +02:00
|
|
|
if (isset(static::$current_rewrite_hash_links)) {
|
|
|
|
return static::$current_rewrite_hash_links;
|
2017-09-21 03:42:28 +02:00
|
|
|
}
|
|
|
|
return Config::inst()->get(static::class, 'rewrite_hash_links');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set default rewrite hash links
|
|
|
|
*
|
|
|
|
* @param bool $rewrite
|
|
|
|
*/
|
|
|
|
public static function setRewriteHashLinksDefault($rewrite)
|
|
|
|
{
|
2017-09-27 02:53:28 +02:00
|
|
|
static::$current_rewrite_hash_links = $rewrite;
|
2017-09-21 03:42:28 +02:00
|
|
|
}
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
/**
|
|
|
|
* @param string|array $templates
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public function setTemplate($templates)
|
|
|
|
{
|
|
|
|
$this->templates = $templates;
|
|
|
|
$this->chosen = $this->chooseTemplate($templates);
|
|
|
|
$this->subTemplates = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the template to use for a given list
|
|
|
|
*
|
|
|
|
* @param array|string $templates
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function chooseTemplate($templates)
|
|
|
|
{
|
2017-05-19 04:38:06 +02:00
|
|
|
return ThemeResourceLoader::inst()->findTemplate($templates, self::get_themes());
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the template parser that will be used in template generation
|
|
|
|
*
|
|
|
|
* @param TemplateParser $parser
|
|
|
|
*/
|
|
|
|
public function setParser(TemplateParser $parser)
|
|
|
|
{
|
|
|
|
$this->parser = $parser;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the parser that is set for template generation
|
|
|
|
*
|
|
|
|
* @return TemplateParser
|
|
|
|
*/
|
|
|
|
public function getParser()
|
|
|
|
{
|
|
|
|
if (!$this->parser) {
|
|
|
|
$this->setParser(Injector::inst()->get('SilverStripe\\View\\SSTemplateParser'));
|
|
|
|
}
|
|
|
|
return $this->parser;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if at least one of the listed templates exists.
|
|
|
|
*
|
2017-05-17 07:40:13 +02:00
|
|
|
* @param array|string $templates
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-09-21 03:42:28 +02:00
|
|
|
* @return bool
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public static function hasTemplate($templates)
|
|
|
|
{
|
2017-05-19 04:38:06 +02:00
|
|
|
return (bool)ThemeResourceLoader::inst()->findTemplate($templates, self::get_themes());
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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();
|
2017-09-21 03:42:28 +02:00
|
|
|
*
|
|
|
|
* @return $this
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function dontRewriteHashlinks()
|
|
|
|
{
|
2017-09-27 02:53:28 +02:00
|
|
|
return $this->setRewriteHashLinks(false);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public function exists()
|
|
|
|
{
|
|
|
|
return $this->chosen;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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 = null)
|
|
|
|
{
|
2017-05-19 04:38:06 +02:00
|
|
|
return ThemeResourceLoader::inst()->findTemplate(['type' => $type, $identifier], self::get_themes());
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears all parsed template files in the cache folder.
|
|
|
|
*
|
|
|
|
* Can only be called once per request (there may be multiple SSViewer instances).
|
|
|
|
*
|
|
|
|
* @param bool $force Set this to true to force a re-flush. If left to false, flushing
|
|
|
|
* may only be performed once a request.
|
|
|
|
*/
|
|
|
|
public static function flush_template_cache($force = false)
|
|
|
|
{
|
|
|
|
if (!self::$template_cache_flushed || $force) {
|
2017-10-09 01:41:34 +02:00
|
|
|
$dir = dir(TEMP_PATH);
|
2016-11-29 00:31:16 +01:00
|
|
|
while (false !== ($file = $dir->read())) {
|
|
|
|
if (strstr($file, '.cache')) {
|
2017-10-09 01:41:34 +02:00
|
|
|
unlink(TEMP_PATH . DIRECTORY_SEPARATOR . $file);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
self::$template_cache_flushed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears all partial cache blocks.
|
|
|
|
*
|
|
|
|
* Can only be called once per request (there may be multiple SSViewer instances).
|
|
|
|
*
|
|
|
|
* @param bool $force Set this to true to force a re-flush. If left to false, flushing
|
|
|
|
* may only be performed once a request.
|
|
|
|
*/
|
|
|
|
public static function flush_cacheblock_cache($force = false)
|
|
|
|
{
|
|
|
|
if (!self::$cacheblock_cache_flushed || $force) {
|
2017-02-23 20:39:57 +01:00
|
|
|
$cache = Injector::inst()->get(CacheInterface::class . '.cacheblock');
|
|
|
|
$cache->clear();
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
self::$cacheblock_cache_flushed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the cache object to use when storing / retrieving partial cache blocks.
|
|
|
|
*
|
2017-02-23 20:39:57 +01:00
|
|
|
* @param CacheInterface $cache
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function setPartialCacheStore($cache)
|
|
|
|
{
|
|
|
|
$this->partialCacheStore = $cache;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the cache object to use when storing / retrieving partial cache blocks.
|
|
|
|
*
|
2017-02-23 20:39:57 +01:00
|
|
|
* @return CacheInterface
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function getPartialCacheStore()
|
|
|
|
{
|
2017-09-19 18:23:53 +02:00
|
|
|
if ($this->partialCacheStore) {
|
|
|
|
return $this->partialCacheStore;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Injector::inst()->get(CacheInterface::class . '.cacheblock');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flag whether to include the requirements in this response.
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @param bool $incl
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function includeRequirements($incl = true)
|
|
|
|
{
|
|
|
|
$this->includeRequirements = $incl;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An internal utility function to set up variables in preparation for including a compiled
|
|
|
|
* template, then do the include
|
|
|
|
*
|
|
|
|
* Effectively this is the common code that both SSViewer#process and SSViewer_FromString#process call
|
|
|
|
*
|
|
|
|
* @param string $cacheFile The path to the file that contains the template compiled to PHP
|
|
|
|
* @param ViewableData $item The item to use as the root scope for the template
|
|
|
|
* @param array $overlay Any variables to layer on top of the scope
|
|
|
|
* @param array $underlay Any variables to layer underneath the scope
|
|
|
|
* @param ViewableData $inheritedScope The current scope of a parent template including a sub-template
|
|
|
|
* @return string The result of executing the template
|
|
|
|
*/
|
|
|
|
protected function includeGeneratedTemplate($cacheFile, $item, $overlay, $underlay, $inheritedScope = null)
|
|
|
|
{
|
|
|
|
if (isset($_GET['showtemplate']) && $_GET['showtemplate'] && Permission::check('ADMIN')) {
|
|
|
|
$lines = file($cacheFile);
|
|
|
|
echo "<h2>Template: $cacheFile</h2>";
|
|
|
|
echo "<pre>";
|
|
|
|
foreach ($lines as $num => $line) {
|
|
|
|
echo str_pad($num+1, 5) . htmlentities($line, ENT_COMPAT, 'UTF-8');
|
|
|
|
}
|
|
|
|
echo "</pre>";
|
|
|
|
}
|
|
|
|
|
|
|
|
$cache = $this->getPartialCacheStore();
|
|
|
|
$scope = new SSViewer_DataPresenter($item, $overlay, $underlay, $inheritedScope);
|
|
|
|
$val = '';
|
|
|
|
|
2017-09-20 05:09:49 +02:00
|
|
|
// Placeholder for values exposed to $cacheFile
|
|
|
|
[$cache, $scope, $val];
|
2016-11-29 00:31:16 +01:00
|
|
|
include($cacheFile);
|
|
|
|
|
|
|
|
return $val;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The process() method handles the "meat" of the template processing.
|
|
|
|
*
|
|
|
|
* It takes care of caching the output (via {@link 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 array|null $arguments Arguments to an included template
|
|
|
|
* @param ViewableData $inheritedScope The current scope of a parent template including a sub-template
|
|
|
|
* @return DBHTMLText Parsed template output.
|
|
|
|
*/
|
|
|
|
public function process($item, $arguments = null, $inheritedScope = null)
|
|
|
|
{
|
2017-09-21 03:42:28 +02:00
|
|
|
// Set hashlinks and temporarily modify global state
|
|
|
|
$rewrite = $this->getRewriteHashLinks();
|
|
|
|
$origRewriteDefault = static::getRewriteHashLinksDefault();
|
|
|
|
static::setRewriteHashLinksDefault($rewrite);
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
SSViewer::$topLevel[] = $item;
|
|
|
|
|
|
|
|
$template = $this->chosen;
|
|
|
|
|
2017-10-09 01:41:34 +02:00
|
|
|
$cacheFile = TEMP_PATH . DIRECTORY_SEPARATOR . '.cache'
|
2017-09-19 18:23:53 +02:00
|
|
|
. str_replace(['\\','/',':'], '.', Director::makeRelative(realpath($template)));
|
2016-11-29 00:31:16 +01:00
|
|
|
$lastEdited = filemtime($template);
|
|
|
|
|
|
|
|
if (!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited) {
|
|
|
|
$content = file_get_contents($template);
|
|
|
|
$content = $this->parseTemplateContent($content, $template);
|
|
|
|
|
|
|
|
$fh = fopen($cacheFile, 'w');
|
|
|
|
fwrite($fh, $content);
|
|
|
|
fclose($fh);
|
|
|
|
}
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
$underlay = ['I18NNamespace' => basename($template)];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Makes the rendered sub-templates available on the parent item,
|
|
|
|
// through $Content and $Layout placeholders.
|
2017-09-19 18:23:53 +02:00
|
|
|
foreach (['Content', 'Layout'] as $subtemplate) {
|
2017-09-20 05:09:49 +02:00
|
|
|
// Detect sub-template to use
|
|
|
|
$sub = $this->getSubtemplateFor($subtemplate);
|
|
|
|
if (!$sub) {
|
|
|
|
continue;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-09-20 05:09:49 +02:00
|
|
|
// Create lazy-evaluated underlay for this subtemplate
|
|
|
|
$underlay[$subtemplate] = function () use ($item, $arguments, $sub) {
|
2016-11-29 00:31:16 +01:00
|
|
|
$subtemplateViewer = clone $this;
|
|
|
|
// Disable requirements - this will be handled by the parent template
|
|
|
|
$subtemplateViewer->includeRequirements(false);
|
|
|
|
// Select the right template
|
|
|
|
$subtemplateViewer->setTemplate($sub);
|
|
|
|
|
2017-09-20 05:09:49 +02:00
|
|
|
// Render if available
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($subtemplateViewer->exists()) {
|
2017-09-20 05:09:49 +02:00
|
|
|
return $subtemplateViewer->process($item, $arguments);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-09-20 05:09:49 +02:00
|
|
|
return null;
|
|
|
|
};
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$output = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, $underlay, $inheritedScope);
|
|
|
|
|
|
|
|
if ($this->includeRequirements) {
|
|
|
|
$output = Requirements::includeInHTML($output);
|
|
|
|
}
|
|
|
|
|
|
|
|
array_pop(SSViewer::$topLevel);
|
|
|
|
|
|
|
|
// If we have our crazy base tag, then fix # links referencing the current page.
|
2017-09-21 03:42:28 +02:00
|
|
|
if ($rewrite) {
|
2016-11-29 00:31:16 +01:00
|
|
|
if (strpos($output, '<base') !== false) {
|
|
|
|
if ($rewrite === 'php') {
|
2017-09-19 18:23:53 +02:00
|
|
|
$thisURLRelativeToBase = <<<PHP
|
|
|
|
<?php echo \\SilverStripe\\Core\\Convert::raw2att(preg_replace("/^(\\\\/)+/", "/", \$_SERVER['REQUEST_URI'])); ?>
|
|
|
|
PHP;
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
|
|
|
$thisURLRelativeToBase = Convert::raw2att(preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI']));
|
|
|
|
}
|
|
|
|
|
|
|
|
$output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-20 05:09:49 +02:00
|
|
|
/** @var DBHTMLText $html */
|
|
|
|
$html = DBField::create_field('HTMLFragment', $output);
|
2017-09-21 03:42:28 +02:00
|
|
|
|
|
|
|
// Reset global state
|
|
|
|
static::setRewriteHashLinksDefault($origRewriteDefault);
|
2017-09-20 05:09:49 +02:00
|
|
|
return $html;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the appropriate template to use for the named sub-template, or null if none are appropriate
|
|
|
|
*
|
|
|
|
* @param string $subtemplate Sub-template to use
|
|
|
|
*
|
|
|
|
* @return array|null
|
|
|
|
*/
|
|
|
|
protected function getSubtemplateFor($subtemplate)
|
|
|
|
{
|
|
|
|
// Get explicit subtemplate name
|
|
|
|
if (isset($this->subTemplates[$subtemplate])) {
|
|
|
|
return $this->subTemplates[$subtemplate];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't apply sub-templates if type is already specified (e.g. 'Includes')
|
|
|
|
if (isset($this->templates['type'])) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Filter out any other typed templates as we can only add, not change type
|
|
|
|
$templates = array_filter(
|
|
|
|
(array)$this->templates,
|
|
|
|
function ($template) {
|
|
|
|
return !isset($template['type']);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
if (empty($templates)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set type to subtemplate
|
|
|
|
$templates['type'] = $subtemplate;
|
|
|
|
return $templates;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
* @param Object $scope
|
2017-12-22 17:19:40 +01:00
|
|
|
* @param bool $globalRequirements
|
|
|
|
*
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return string Evaluated result
|
|
|
|
*/
|
2017-12-22 17:19:40 +01:00
|
|
|
public static function execute_template($template, $data, $arguments = null, $scope = null, $globalRequirements = false)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-07-10 07:45:35 +02:00
|
|
|
$v = SSViewer::create($template);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-12-22 17:19:40 +01:00
|
|
|
if ($globalRequirements) {
|
|
|
|
$v->includeRequirements(false);
|
|
|
|
} else {
|
|
|
|
//nest a requirements backend for our template rendering
|
|
|
|
$origBackend = Requirements::backend();
|
|
|
|
Requirements::set_backend(Requirements_Backend::create());
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
return $v->process($data, $arguments, $scope);
|
|
|
|
} finally {
|
|
|
|
if (!$globalRequirements) {
|
|
|
|
Requirements::set_backend($origBackend);
|
|
|
|
}
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2017-12-22 17:19:40 +01:00
|
|
|
* @param bool $globalRequirements
|
|
|
|
*
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return string Evaluated result
|
|
|
|
*/
|
2017-12-22 17:19:40 +01:00
|
|
|
public static function execute_string($content, $data, $arguments = null, $globalRequirements = false)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
$v = SSViewer::fromString($content);
|
|
|
|
|
2017-12-22 17:19:40 +01:00
|
|
|
if ($globalRequirements) {
|
|
|
|
$v->includeRequirements(false);
|
|
|
|
} else {
|
|
|
|
//nest a requirements backend for our template rendering
|
|
|
|
$origBackend = Requirements::backend();
|
|
|
|
Requirements::set_backend(Requirements_Backend::create());
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
return $v->process($data, $arguments);
|
|
|
|
} finally {
|
|
|
|
if (!$globalRequirements) {
|
|
|
|
Requirements::set_backend($origBackend);
|
|
|
|
}
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-09-19 18:23:53 +02:00
|
|
|
/**
|
|
|
|
* Parse given template contents
|
|
|
|
*
|
|
|
|
* @param string $content The template contents
|
|
|
|
* @param string $template The template file name
|
2017-09-21 03:42:28 +02:00
|
|
|
* @return string
|
2017-09-19 18:23:53 +02:00
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public function parseTemplateContent($content, $template = "")
|
|
|
|
{
|
|
|
|
return $this->getParser()->compileString(
|
|
|
|
$content,
|
|
|
|
$template,
|
2017-02-22 04:14:53 +01:00
|
|
|
Director::isDev() && SSViewer::config()->uninherited('source_file_comments')
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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'
|
2017-09-19 18:23:53 +02:00
|
|
|
*
|
|
|
|
* @return array
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function templates()
|
|
|
|
{
|
|
|
|
return array_merge(['main' => $this->chosen], $this->subTemplates);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $type "Layout" or "main"
|
|
|
|
* @param string $file Full system path to the template file
|
|
|
|
*/
|
|
|
|
public function setTemplateFile($type, $file)
|
|
|
|
{
|
|
|
|
if (!$type || $type == 'main') {
|
|
|
|
$this->chosen = $file;
|
|
|
|
} else {
|
|
|
|
$this->subTemplates[$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 string $contentGeneratedSoFar The content of the template generated so far; it should contain
|
|
|
|
* the DOCTYPE declaration.
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public 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]-->";
|
|
|
|
}
|
|
|
|
}
|
2016-08-19 00:51:35 +02:00
|
|
|
}
|