File: /home/accemeff/vendor/craftcms/cms/src/web/View.php
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/
namespace craft\web;
use Craft;
use craft\base\Element;
use craft\events\RegisterTemplateRootsEvent;
use craft\events\TemplateEvent;
use craft\helpers\ElementHelper;
use craft\helpers\FileHelper;
use craft\helpers\Html as HtmlHelper;
use craft\helpers\Json;
use craft\helpers\Path;
use craft\helpers\StringHelper;
use craft\web\twig\Environment;
use craft\web\twig\Extension;
use craft\web\twig\Template;
use craft\web\twig\TemplateLoader;
use Twig_ExtensionInterface;
use yii\base\Arrayable;
use yii\base\Exception;
use yii\base\Model;
use yii\helpers\Html;
use yii\web\AssetBundle as YiiAssetBundle;
/**
* @inheritdoc
* @property string $templateMode the current template mode (either `site` or `cp`)
* @property string $templatesPath the base path that templates should be found in
* @property string|null $namespace the active namespace
* @property-read array $cpTemplateRoots any registered CP template roots
* @property-read array $siteTemplateRoots any registered site template roots
* @property-read bool $isRenderingPageTemplate whether a page template is currently being rendered
* @property-read bool $isRenderingTemplate whether a template is currently being rendered
* @property-read Environment $twig the Twig environment
* @property-read string $bodyHtml the content to be inserted at the end of the body section
* @property-read string $headHtml the content to be inserted in the head section
* @property-write string[] $registeredAssetBundles the asset bundle names that should be marked as already registered
* @property-write string[] $registeredJsFiles the JS files that should be marked as already registered
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.0
*/
class View extends \yii\web\View
{
// Constants
// =========================================================================
/**
* @event RegisterTemplateRootsEvent The event that is triggered when registering CP template roots
*/
const EVENT_REGISTER_CP_TEMPLATE_ROOTS = 'registerCpTemplateRoots';
/**
* @event RegisterTemplateRootsEvent The event that is triggered when registering site template roots
*/
const EVENT_REGISTER_SITE_TEMPLATE_ROOTS = 'registerSiteTemplateRoots';
/**
* @event TemplateEvent The event that is triggered before a template gets rendered
*/
const EVENT_BEFORE_RENDER_TEMPLATE = 'beforeRenderTemplate';
/**
* @event TemplateEvent The event that is triggered after a template gets rendered
*/
const EVENT_AFTER_RENDER_TEMPLATE = 'afterRenderTemplate';
/**
* @event TemplateEvent The event that is triggered before a page template gets rendered
*/
const EVENT_BEFORE_RENDER_PAGE_TEMPLATE = 'beforeRenderPageTemplate';
/**
* @event TemplateEvent The event that is triggered after a page template gets rendered
*/
const EVENT_AFTER_RENDER_PAGE_TEMPLATE = 'afterRenderPageTemplate';
/**
* @const TEMPLATE_MODE_CP
*/
const TEMPLATE_MODE_CP = 'cp';
/**
* @const TEMPLATE_MODE_SITE
*/
const TEMPLATE_MODE_SITE = 'site';
// Properties
// =========================================================================
/**
* @var array The sizes that element thumbnails should be rendered in
*/
private static $_elementThumbSizes = [30, 60, 100, 200];
/**
* @var Environment|null The Twig environment instance used for CP templates
*/
private $_cpTwig;
/**
* @var Environment|null The Twig environment instance used for site templates
*/
private $_siteTwig;
/**
* @var
*/
private $_twigOptions;
/**
* @var Twig_ExtensionInterface[] List of Twig extensions registered with [[registerTwigExtension()]]
*/
private $_twigExtensions = [];
/**
* @var
*/
private $_templatePaths;
/**
* @var
*/
private $_objectTemplates;
/**
* @var string|null
*/
private $_templateMode;
/**
* @var array|null
*/
private $_cpTemplateRoots;
/**
* @var array|null
*/
private $_siteTemplateRoots;
/**
* @var array|null
*/
private $_templateRoots;
/**
* @var string|null The root path to look for templates in
*/
private $_templatesPath;
/**
* @var
*/
private $_defaultTemplateExtensions;
/**
* @var
*/
private $_indexTemplateFilenames;
/**
* @var
*/
private $_namespace;
/**
* @var array
*/
private $_jsBuffers = [];
/**
* @var array the registered generic `<script>` code blocks
* @see registerScript()
*/
private $_scripts;
/**
* @var
*/
private $_hooks;
/**
* @var
*/
private $_textareaMarkers;
/**
* @var
*/
private $_renderingTemplate;
/**
* @var
*/
private $_isRenderingPageTemplate = false;
/**
* @var string[]
* @see registerAssetFiles()
* @see setRegisteredAssetBundles()
*/
private $_registeredAssetBundles = [];
/**
* @var string[]
* @see registerJsFile()
* @see setRegisteredJsfiles()
*/
private $_registeredJsFiles = [];
// Public Methods
// =========================================================================
/**
* @inheritdoc
*/
public function init()
{
parent::init();
// Set the initial template mode based on whether this is a CP or Site request
$request = Craft::$app->getRequest();
if ($request->getIsConsoleRequest() || $request->getIsCpRequest()) {
$this->setTemplateMode(self::TEMPLATE_MODE_CP);
} else {
$this->setTemplateMode(self::TEMPLATE_MODE_SITE);
}
// Register the cp.elements.element hook
$this->hook('cp.elements.element', [$this, '_getCpElementHtml']);
}
/**
* Returns the Twig environment.
*
* @return Environment
*/
public function getTwig(): Environment
{
return $this->_templateMode === self::TEMPLATE_MODE_CP
? $this->_cpTwig ?? ($this->_cpTwig = $this->createTwig())
: $this->_siteTwig ?? ($this->_siteTwig = $this->createTwig());
}
/**
* Creates a new Twig environment.
*
* @return Environment
*/
public function createTwig(): Environment
{
$twig = new Environment(new TemplateLoader($this), $this->_getTwigOptions());
$twig->addExtension(new \Twig_Extension_StringLoader());
$twig->addExtension(new Extension($this, $twig));
if (YII_DEBUG) {
$twig->addExtension(new \Twig_Extension_Debug());
}
// Add plugin-supplied extensions
foreach ($this->_twigExtensions as $extension) {
$twig->addExtension($extension);
}
// Set our timezone
/** @var \Twig_Extension_Core $core */
$core = $twig->getExtension(\Twig_Extension_Core::class);
$core->setTimezone(Craft::$app->getTimeZone());
return $twig;
}
/**
* Registers a new Twig extension, which will be added on existing environments and queued up for future environments.
*
* @param Twig_ExtensionInterface $extension
*/
public function registerTwigExtension(Twig_ExtensionInterface $extension)
{
// Make sure this extension isn't already registered
$class = get_class($extension);
if (isset($this->_twigExtensions[$class])) {
return;
}
$this->_twigExtensions[$class] = $extension;
// Add it to any existing Twig environments
if ($this->_cpTwig !== null) {
$this->_cpTwig->addExtension($extension);
}
if ($this->_siteTwig !== null) {
$this->_siteTwig->addExtension($extension);
}
}
/**
* Returns whether a template is currently being rendered.
*
* @return bool Whether a template is currently being rendered.
*/
public function getIsRenderingTemplate(): bool
{
return $this->_renderingTemplate !== null;
}
/**
* Renders a Twig template.
*
* @param string $template The name of the template to load
* @param array $variables The variables that should be available to the template
* @return string the rendering result
* @throws \Twig_Error_Loader if the template doesn’t exist
* @throws Exception in case of failure
* @throws \RuntimeException in case of failure
*/
public function renderTemplate(string $template, array $variables = []): string
{
if (!$this->beforeRenderTemplate($template, $variables)) {
return '';
}
Craft::debug("Rendering template: $template", __METHOD__);
// Render and return
$renderingTemplate = $this->_renderingTemplate;
$this->_renderingTemplate = $template;
Craft::beginProfile($template, __METHOD__);
try {
$output = $this->getTwig()->render($template, $variables);
} catch (\RuntimeException $e) {
if (!YII_DEBUG) {
// Throw a generic exception instead
throw new Exception('An error occurred when rendering a template.', 0, $e);
}
throw $e;
}
Craft::endProfile($template, __METHOD__);
$this->_renderingTemplate = $renderingTemplate;
$this->afterRenderTemplate($template, $variables, $output);
return $output;
}
/**
* Returns whether a page template is currently being rendered.
*
* @return bool Whether a page template is currently being rendered.
*/
public function getIsRenderingPageTemplate(): bool
{
return $this->_isRenderingPageTemplate;
}
/**
* Renders a Twig template that represents an entire web page.
*
* @param string $template The name of the template to load
* @param array $variables The variables that should be available to the template
* @return string the rendering result
*/
public function renderPageTemplate(string $template, array $variables = []): string
{
if (!$this->beforeRenderPageTemplate($template, $variables)) {
return '';
}
ob_start();
ob_implicit_flush(false);
$isRenderingPageTemplate = $this->_isRenderingPageTemplate;
$this->_isRenderingPageTemplate = true;
$this->beginPage();
echo $this->renderTemplate($template, $variables);
$this->endPage();
$this->_isRenderingPageTemplate = $isRenderingPageTemplate;
$output = ob_get_clean();
$this->afterRenderPageTemplate($template, $variables, $output);
return $output;
}
/**
* Renders a macro within a given Twig template.
*
* @param string $template The name of the template the macro lives in.
* @param string $macro The name of the macro.
* @param array $args Any arguments that should be passed to the macro.
* @return string The rendered macro output.
* @throws Exception in case of failure
* @throws \RuntimeException in case of failure
*/
public function renderTemplateMacro(string $template, string $macro, array $args = []): string
{
$twig = $this->getTwig();
$twigTemplate = $twig->loadTemplate($template);
$renderingTemplate = $this->_renderingTemplate;
$this->_renderingTemplate = $template;
try {
$output = call_user_func_array([$twigTemplate, 'macro_' . $macro], $args);
} catch (\RuntimeException $e) {
if (!YII_DEBUG) {
// Throw a generic exception instead
throw new Exception('An error occurred when rendering a template.', 0, $e);
}
throw $e;
}
$this->_renderingTemplate = $renderingTemplate;
return (string)$output;
}
/**
* Renders a template defined in a string.
*
* @param string $template The source template string.
* @param array $variables Any variables that should be available to the template.
* @return string The rendered template.
*/
public function renderString(string $template, array $variables = []): string
{
$twig = $this->getTwig();
$twig->setDefaultEscaperStrategy(false);
$lastRenderingTemplate = $this->_renderingTemplate;
$this->_renderingTemplate = 'string:' . $template;
$result = $twig->createTemplate($template)->render($variables);
$this->_renderingTemplate = $lastRenderingTemplate;
$twig->setDefaultEscaperStrategy();
return $result;
}
/**
* Renders an object template.
*
* The passed-in `$object` will be available to the template as an `object` variable.
*
* The template will be parsed for “property tags” (e.g. `{foo}`), which will get replaced with
* full Twig output tags (e.g. `{{ object.foo|raw }}`.
*
* If `$object` is an instance of [[Arrayable]], any attributes returned by its [[Arrayable::fields()|fields()]] or
* [[Arrayable::extraFields()|extraFields()]] methods will also be available as variables to the template.
*
* @param string $template the source template string
* @param mixed $object the object that should be passed into the template
* @param array $variables any additional variables that should be available to the template
* @return string The rendered template.
* @throws Exception in case of failure
* @throws \Throwable in case of failure
*/
public function renderObjectTemplate(string $template, $object, array $variables = []): string
{
// If there are no dynamic tags, just return the template
if (strpos($template, '{') === false) {
return $template;
}
$twig = $this->getTwig();
// Is this the first time we've parsed this template?
$cacheKey = md5($template);
if (!isset($this->_objectTemplates[$cacheKey])) {
// Replace shortcut "{var}"s with "{{object.var}}"s, without affecting normal Twig tags
$template = $this->normalizeObjectTemplate($template);
$this->_objectTemplates[$cacheKey] = $twig->createTemplate($template);
}
// Get the variables to pass to the template
if ($object instanceof Model) {
foreach ($object->attributes() as $name) {
if (!isset($variables[$name]) && strpos($template, $name) !== false) {
$variables[$name] = $object->$name;
}
}
}
if ($object instanceof Arrayable) {
// See if we should be including any of the extra fields
$extra = [];
foreach ($object->extraFields() as $field => $definition) {
if (is_int($field)) {
$field = $definition;
}
if (strpos($template, $field) !== false) {
$extra[] = $field;
}
}
$variables = array_merge($object->toArray([], $extra, false), $variables);
}
$variables['object'] = $object;
$variables['_variables'] = $variables;
// Temporarily disable strict variables if it's enabled
$strictVariables = $twig->isStrictVariables();
if ($strictVariables) {
$twig->disableStrictVariables();
}
// Render it!
$twig->setDefaultEscaperStrategy(false);
$lastRenderingTemplate = $this->_renderingTemplate;
$this->_renderingTemplate = 'string:' . $template;
/** @var Template $templateObj */
$templateObj = $this->_objectTemplates[$cacheKey];
$e = null;
try {
$output = $templateObj->render($variables);
} catch (\Throwable $e) {
}
$this->_renderingTemplate = $lastRenderingTemplate;
$twig->setDefaultEscaperStrategy();
// Re-enable strict variables
if ($strictVariables) {
$twig->enableStrictVariables();
}
if ($e !== null) {
if (!YII_DEBUG) {
// Throw a generic exception instead
throw new Exception('An error occurred when rendering a template.', 0, $e);
}
throw $e;
}
return $output;
}
/**
* Normalizes an object template for [[renderObjectTemplate()]].
*
* @param string $template
* @return string
*/
public function normalizeObjectTemplate(string $template): string
{
// Tokenize objects (call preg_replace_callback() multiple times in case there are nested objects)
$tokens = [];
while (true) {
$template = preg_replace_callback('/\{\s*([\'"]?)\w+\1\s*:[^\{]+?\}/', function(array $matches) use (&$tokens) {
$token = 'tok_' . StringHelper::randomString(10);
$tokens[$token] = $matches[0];
return $token;
}, $template, -1, $count);
if ($count === 0) {
break;
}
}
// Swap out the remaining {xyz} tags with {{object.xyz}}
$template = preg_replace('/(?<!\{)\{\s*(\w+)([^\{]*?)\}/', '{{ (_variables.$1 ?? object.$1)$2|raw }}', $template);
// Bring the objects back
foreach (array_reverse($tokens) as $token => $value) {
$template = str_replace($token, $value, $template);
}
return $template;
}
/**
* Returns whether a template exists.
*
* Internally, this will just call [[resolveTemplate()]] with the given template name, and return whether that
* method found anything.
*
* @param string $name The name of the template.
* @return bool Whether the template exists.
*/
public function doesTemplateExist(string $name): bool
{
try {
return ($this->resolveTemplate($name) !== false);
} catch (\Twig_Error_Loader $e) {
// _validateTemplateName() han an issue with it
return false;
}
}
/**
* Finds a template on the file system and returns its path.
*
* All of the following files will be searched for, in this order:
*
* - TemplateName
* - TemplateName.html
* - TemplateName.twig
* - TemplateName/index.html
* - TemplateName/index.twig
*
* If this is a front-end request, the actual list of file extensions and index filenames are configurable via the
* [[\craft\config\GeneralConfig::defaultTemplateExtensions|defaultTemplateExtensions]] and
* [[\craft\config\GeneralConfig::indexTemplateFilenames|indexTemplateFilenames]] config settings.
*
* For example if you set the following in config/general.php:
*
* ```php
* 'defaultTemplateExtensions' => ['htm'],
* 'indexTemplateFilenames' => ['default'],
* ```
*
* then the following files would be searched for instead:
*
* - TemplateName
* - TemplateName.htm
* - TemplateName/default.htm
*
* The actual directory that those files will depend on the current [[setTemplateMode()|template mode]]
* (probably `templates/` if it’s a front-end site request, and `vendor/craftcms/cms/src/templates/` if it’s a Control
* Panel request).
*
* If this is a front-end site request, a folder named after the current site handle will be checked first.
*
* - templates/SiteHandle/...
* - templates/...
*
* And finally, if this is a Control Panel request _and_ the template name includes multiple segments _and_ the first
* segment of the template name matches a plugin’s handle, then Craft will look for a template named with the
* remaining segments within that plugin’s templates/ subfolder.
*
* To put it all together, here’s where Craft would look for a template named “foo/bar”, depending on the type of
* request it is:
*
* - Front-end site requests:
* - templates/SiteHandle/foo/bar
* - templates/SiteHandle/foo/bar.html
* - templates/SiteHandle/foo/bar.twig
* - templates/SiteHandle/foo/bar/index.html
* - templates/SiteHandle/foo/bar/index.twig
* - templates/foo/bar
* - templates/foo/bar.html
* - templates/foo/bar.twig
* - templates/foo/bar/index.html
* - templates/foo/bar/index.twig
* - Control Panel requests:
* - vendor/craftcms/cms/src/templates/foo/bar
* - vendor/craftcms/cms/src/templates/foo/bar.html
* - vendor/craftcms/cms/src/templates/foo/bar.twig
* - vendor/craftcms/cms/src/templates/foo/bar/index.html
* - vendor/craftcms/cms/src/templates/foo/bar/index.twig
* - path/to/fooplugin/templates/bar
* - path/to/fooplugin/templates/bar.html
* - path/to/fooplugin/templates/bar.twig
* - path/to/fooplugin/templates/bar/index.html
* - path/to/fooplugin/templates/bar/index.twig
*
* @param string $name The name of the template.
* @return string|false The path to the template if it exists, or `false`.
*/
public function resolveTemplate(string $name)
{
// Normalize the template name
$name = trim(preg_replace('#/{2,}#', '/', str_replace('\\', '/', StringHelper::convertToUtf8($name))), '/');
$key = $this->_templatesPath . ':' . $name;
// Is this template path already cached?
if (isset($this->_templatePaths[$key])) {
return $this->_templatePaths[$key];
}
// Validate the template name
$this->_validateTemplateName($name);
// Look for the template in the main templates folder
$basePaths = [];
// Should we be looking for a localized version of the template?
if ($this->_templateMode === self::TEMPLATE_MODE_SITE && Craft::$app->getIsInstalled()) {
/** @noinspection PhpUnhandledExceptionInspection */
$sitePath = $this->_templatesPath . DIRECTORY_SEPARATOR . Craft::$app->getSites()->getCurrentSite()->handle;
if (is_dir($sitePath)) {
$basePaths[] = $sitePath;
}
}
$basePaths[] = $this->_templatesPath;
foreach ($basePaths as $basePath) {
if (($path = $this->_resolveTemplate($basePath, $name)) !== null) {
return $this->_templatePaths[$key] = $path;
}
}
unset($basePaths);
// Check any registered template roots
if ($this->_templateMode === self::TEMPLATE_MODE_CP) {
$roots = $this->getCpTemplateRoots();
} else {
$roots = $this->getSiteTemplateRoots();
}
if (!empty($roots)) {
foreach ($roots as $templateRoot => $basePaths) {
/** @var string[] $basePaths */
$templateRootLen = strlen($templateRoot);
if (strncasecmp($templateRoot . '/', $name . '/', $templateRootLen + 1) === 0) {
$subName = strlen($name) === $templateRootLen ? '' : substr($name, $templateRootLen + 1);
foreach ($basePaths as $basePath) {
if (($path = $this->_resolveTemplate($basePath, $subName)) !== null) {
return $this->_templatePaths[$key] = $path;
}
}
}
}
}
return false;
}
/**
* Returns any registered CP template roots.
*
* @return array
*/
public function getCpTemplateRoots(): array
{
return $this->_getTemplateRoots('cp');
}
/**
* Returns any registered site template roots.
*
* @return array
*/
public function getSiteTemplateRoots(): array
{
return $this->_getTemplateRoots('site');
}
/**
* Registers a hi-res CSS code block.
*
* @param string $css the CSS code block to be registered
* @param array $options the HTML attributes for the style tag.
* @param string|null $key the key that identifies the CSS code block. If null, it will use
* $css as the key. If two CSS code blocks are registered with the same key, the latter
* will overwrite the former.
* @deprecated in 3.0. Use [[registerCss()]] and type your own media selector.
*/
public function registerHiResCss(string $css, array $options = [], string $key = null)
{
Craft::$app->getDeprecator()->log('registerHiResCss', 'craft\\web\\View::registerHiResCss() has been deprecated. Use registerCss() instead, and type your own media selector.');
$css = "@media only screen and (-webkit-min-device-pixel-ratio: 1.5),\n" .
"only screen and ( -moz-min-device-pixel-ratio: 1.5),\n" .
"only screen and ( -o-min-device-pixel-ratio: 3/2),\n" .
"only screen and ( min-device-pixel-ratio: 1.5),\n" .
"only screen and ( min-resolution: 1.5dppx){\n" .
$css . "\n" .
'}';
$this->registerCss($css, $options, $key);
}
/**
* @inheritdoc
*/
public function registerJs($js, $position = self::POS_READY, $key = null)
{
// Trim any whitespace and ensure it ends with a semicolon.
$js = StringHelper::ensureRight(trim($js, " \t\n\r\0\x0B"), ';');
parent::registerJs($js, $position, $key);
}
/**
* Starts a JavaScript buffer.
*
* JavaScript buffers work similarly to [output buffers](http://php.net/manual/en/intro.outcontrol.php) in PHP.
* Once you’ve started a JavaScript buffer, any JavaScript code included with [[registerJs()]] will be included
* in a buffer, and you will have the opportunity to fetch all of that code via [[clearJsBuffer()]] without
* having it actually get output to the page.
*/
public function startJsBuffer()
{
// Save any currently queued JS into a new buffer, and reset the active JS queue
$this->_jsBuffers[] = $this->js;
$this->js = [];
}
/**
* Clears and ends a JavaScript buffer, returning whatever JavaScript code was included while the buffer was active.
*
* @param bool $scriptTag Whether the JavaScript code should be wrapped in a `<script>` tag. Defaults to `true`.
* @return string|false The JS code that was included in the active JS buffer, or `false` if there isn’t one
*/
public function clearJsBuffer(bool $scriptTag = true)
{
if (empty($this->_jsBuffers)) {
return false;
}
// Combine the JS
$js = '';
foreach ([self::POS_HEAD, self::POS_BEGIN, self::POS_END, self::POS_LOAD, self::POS_READY] as $pos) {
if (!empty($this->js[$pos])) {
$js .= implode("\n", $this->js[$pos]) . "\n";
}
}
// Set the active queue to the last one
$this->js = array_pop($this->_jsBuffers);
if ($scriptTag === true && !empty($js)) {
return Html::script($js, ['type' => 'text/javascript']);
}
return $js;
}
/**
* @inheritdoc
*/
public function registerJsFile($url, $options = [], $key = null)
{
// If 'depends' is specified, ignore it for now because the file will
// get registered as an asset bundle
if (empty($options['depends'])) {
$key = $key ?: $url;
if (isset($this->_registeredJsFiles[$key])) {
return;
}
$this->_registeredJsFiles[$key] = true;
}
parent::registerJsFile($url, $options, $key);
}
/**
* Registers a generic `<script>` code block.
*
* @param string $script the generic `<script>` code block to be registered
* @param int $position the position at which the generic `<script>` code block should be inserted
* in a page. The possible values are:
* - [[POS_HEAD]]: in the head section
* - [[POS_BEGIN]]: at the beginning of the body section
* - [[POS_END]]: at the end of the body section
* @param array $options the HTML attributes for the `<script>` tag.
* @param string $key the key that identifies the generic `<script>` code block. If null, it will use
* $script as the key. If two generic `<script>` code blocks are registered with the same key, the latter
* will overwrite the former.
*/
public function registerScript($script, $position = self::POS_END, $options = [], $key = null)
{
$key = $key ?: md5($script);
$this->_scripts[$position][$key] = Html::script($script, $options);
}
/**
* @inheritdoc
*/
public function endBody()
{
$this->registerAssetFlashes();
parent::endBody();
}
/**
* Returns the content to be inserted in the head section.
*
* This includes:
* - Meta tags registered using [[registerMetaTag()]]
* - Link tags registered with [[registerLinkTag()]]
* - CSS code registered with [[registerCss()]]
* - CSS files registered with [[registerCssFile()]]
* - JS code registered with [[registerJs()]] with the position set to [[POS_HEAD]]
* - JS files registered with [[registerJsFile()]] with the position set to [[POS_HEAD]]
*
* @param bool $clear Whether the content should be cleared from the queue (default is true)
* @return string the rendered content
*/
public function getHeadHtml(bool $clear = true): string
{
// Register any asset bundles
$this->registerAllAssetFiles();
$html = $this->renderHeadHtml();
if ($clear === true) {
$this->metaTags = [];
$this->linkTags = [];
$this->css = [];
$this->cssFiles = [];
unset($this->jsFiles[self::POS_HEAD], $this->js[self::POS_HEAD]);
}
return $html;
}
/**
* Returns the content to be inserted at the end of the body section.
*
* This includes:
* - JS code registered with [[registerJs()]] with the position set to [[POS_BEGIN]], [[POS_END]], [[POS_READY]], or [[POS_LOAD]]
* - JS files registered with [[registerJsFile()]] with the position set to [[POS_BEGIN]] or [[POS_END]]
*
* @param bool $clear Whether the content should be cleared from the queue (default is true)
* @return string the rendered content
*/
public function getBodyHtml(bool $clear = true): string
{
// Register any asset bundles
$this->registerAllAssetFiles();
// Get the rendered body begin+end HTML
$html = $this->renderBodyBeginHtml() .
$this->renderBodyEndHtml(true);
// Clear out the queued up files
if ($clear === true) {
unset(
$this->jsFiles[self::POS_BEGIN],
$this->jsFiles[self::POS_END],
$this->js[self::POS_BEGIN],
$this->js[self::POS_END],
$this->js[self::POS_READY],
$this->js[self::POS_LOAD]
);
}
return $html;
}
/**
* Translates messages for a given translation category, so they will be
* available for `Craft.t()` calls in the Control Panel.
* Note this should always be called *before* any JavaScript is registered
* that will need to use the translations, unless the JavaScript is
* registered at [[self::POS_READY]].
*
* @param string $category The category the messages are in
* @param string[] $messages The messages to be translated
*/
public function registerTranslations(string $category, array $messages)
{
$jsCategory = Json::encode($category);
$js = '';
foreach ($messages as $message) {
$translation = Craft::t($category, $message);
if ($translation !== $message) {
$jsMessage = Json::encode($message);
$jsTranslation = Json::encode($translation);
$js .= ($js !== '' ? "\n" : '') . "Craft.translations[{$jsCategory}][{$jsMessage}] = {$jsTranslation};";
}
}
if ($js === '') {
return;
}
$js = <<<JS
if (typeof Craft.translations[{$jsCategory}] === 'undefined') {
Craft.translations[{$jsCategory}] = {};
}
{$js}
JS;
$this->registerJs($js, self::POS_BEGIN);
}
/**
* Returns the active namespace.
*
* This is the default namespaces that will be used when [[namespaceInputs()]], [[namespaceInputName()]],
* and [[namespaceInputId()]] are called, if their $namespace arguments are null.
*
* @return string|null The namespace.
*/
public function getNamespace()
{
return $this->_namespace;
}
/**
* Sets the active namespace.
*
* This is the default namespaces that will be used when [[namespaceInputs()]], [[namespaceInputName()]],
* and [[namespaceInputId()]] are called, if their|null $namespace arguments are null.
*
* @param string|null $namespace The new namespace. Set to null to remove the namespace.
*/
public function setNamespace(string $namespace = null)
{
$this->_namespace = $namespace;
}
/**
* Returns the current template mode (either `site` or `cp`).
*
* @return string Either `site` or `cp`.
*/
public function getTemplateMode(): string
{
return $this->_templateMode;
}
/**
* Sets the current template mode.
*
* The template mode defines:
* - the base path that templates should be looked for in
* - the default template file extensions that should be automatically added when looking for templates
* - the "index" template filenames that sholud be checked when looking for templates
*
* @param string $templateMode Either 'site' or 'cp'
* @throws Exception if $templateMode is invalid
*/
public function setTemplateMode(string $templateMode)
{
// Validate
if (!in_array($templateMode, [
self::TEMPLATE_MODE_CP,
self::TEMPLATE_MODE_SITE
], true)
) {
throw new Exception('"' . $templateMode . '" is not a valid template mode');
}
// Set the new template mode
$this->_templateMode = $templateMode;
// Update everything
if ($templateMode == self::TEMPLATE_MODE_CP) {
$this->setTemplatesPath(Craft::$app->getPath()->getCpTemplatesPath());
$this->_defaultTemplateExtensions = ['html', 'twig'];
$this->_indexTemplateFilenames = ['index'];
} else {
$this->setTemplatesPath(Craft::$app->getPath()->getSiteTemplatesPath());
$generalConfig = Craft::$app->getConfig()->getGeneral();
$this->_defaultTemplateExtensions = $generalConfig->defaultTemplateExtensions;
$this->_indexTemplateFilenames = $generalConfig->indexTemplateFilenames;
}
}
/**
* Returns the base path that templates should be found in.
*
* @return string
*/
public function getTemplatesPath(): string
{
return $this->_templatesPath;
}
/**
* Sets the base path that templates should be found in.
*
* @param string $templatesPath
*/
public function setTemplatesPath(string $templatesPath)
{
$this->_templatesPath = rtrim($templatesPath, '/\\');
}
/**
* Renames HTML input names so they belong to a namespace.
*
* This method will go through the passed-in $html looking for `name=` attributes, and renaming their values such
* that they will live within the passed-in $namespace (or the [[getNamespace()|active namespace]]).
* By default, any `id=`, `for=`, `list=`, `data-target=`, `data-reverse-target=`, and `data-target-prefix=`
* attributes will get namespaced as well, by prepending the namespace and a dash to their values.
* For example, the following HTML:
*
* ```html
* <label for="title">Title</label>
* <input type="text" name="title" id="title">
* ```
*
* would become this, if it were namespaced with “foo”:
*
* ```html
* <label for="foo-title">Title</label>
* <input type="text" name="foo[title]" id="foo-title">
* ```
*
* Attributes that are already namespaced will get double-namespaced. For example, the following HTML:
*
* ```html
* <label for="bar-title">Title</label>
* <input type="text" name="bar[title]" id="title">
* ```
*
* would become:
*
* ```html
* <label for="foo-bar-title">Title</label>
* <input type="text" name="foo[bar][title]" id="foo-bar-title">
* ```
*
* @param string $html The template with the inputs.
* @param string|null $namespace The namespace. Defaults to the [[getNamespace()|active namespace]].
* @param bool $otherAttributes Whether id=, for=, etc., should also be namespaced. Defaults to `true`.
* @return string The HTML with namespaced input names.
*/
public function namespaceInputs(string $html, string $namespace = null, bool $otherAttributes = true): string
{
if ($html === '') {
return '';
}
if ($namespace === null) {
$namespace = $this->getNamespace();
}
if ($namespace !== null) {
// Protect the textarea content
$this->_textareaMarkers = [];
$html = preg_replace_callback('/(<textarea\b[^>]*>)(.*?)(<\/textarea>)/is',
[$this, '_createTextareaMarker'], $html);
// name= attributes
$html = preg_replace('/(?<![\w\-])(name=(\'|"))([^\'"\[\]]+)([^\'"]*)\2/i', '$1' . $namespace . '[$3]$4$2', $html);
// id= and for= attributes
if ($otherAttributes) {
$idNamespace = $this->formatInputId($namespace);
$html = preg_replace('/(?<![\w\-])((id|for|list|aria\-labelledby|data\-target|data\-reverse\-target|data\-target\-prefix)=(\'|")#?)([^\.\'"][^\'"]*)?\3/i', '$1' . $idNamespace . '-$4$3', $html);
}
// Bring back the textarea content
$html = str_replace(array_keys($this->_textareaMarkers), array_values($this->_textareaMarkers), $html);
}
return $html;
}
/**
* Namespaces an input name.
*
* This method applies the same namespacing treatment that [[namespaceInputs()]] does to `name=` attributes,
* but only to a single value, which is passed directly into this method.
*
* @param string $inputName The input name that should be namespaced.
* @param string|null $namespace The namespace. Defaults to the [[getNamespace()|active namespace]].
* @return string The namespaced input name.
*/
public function namespaceInputName(string $inputName, string $namespace = null): string
{
if ($namespace === null) {
$namespace = $this->getNamespace();
}
if ($namespace !== null) {
$inputName = preg_replace('/([^\'"\[\]]+)([^\'"]*)/', $namespace . '[$1]$2', $inputName);
}
return $inputName;
}
/**
* Namespaces an input ID.
*
* This method applies the same namespacing treatment that [[namespaceInputs()]] does to `id=` attributes,
* but only to a single value, which is passed directly into this method.
*
* @param string $inputId The input ID that should be namespaced.
* @param string|null $namespace The namespace. Defaults to the [[getNamespace()|active namespace]].
* @return string The namespaced input ID.
*/
public function namespaceInputId(string $inputId, string $namespace = null): string
{
if ($namespace === null) {
$namespace = $this->getNamespace();
}
if ($namespace !== null) {
$inputId = $this->formatInputId($namespace) . '-' . $inputId;
}
return $inputId;
}
/**
* Formats an ID out of an input name.
*
* This method takes a given input name and returns a valid ID based on it.
* For example, if given the following input name:
* foo[bar][title]
* the following ID would be returned:
* foo-bar-title
*
* @param string $inputName The input name.
* @return string The input ID.
*/
public function formatInputId(string $inputName): string
{
return rtrim(preg_replace('/[\[\]\\\]+/', '-', $inputName), '-');
}
/**
* Queues up a method to be called by a given template hook.
*
* For example, if you place this in your plugin’s [[BasePlugin::init()|init()]] method:
*
* ```php
* Craft::$app->view->hook('myAwesomeHook', function(&$context) {
* $context['foo'] = 'bar';
* return 'Hey!';
* });
* ```
*
* you would then be able to add this to any template:
*
* ```twig
* {% hook "myAwesomeHook" %}
* ```
*
* When the hook tag gets invoked, your template hook function will get called. The $context argument will be the
* current Twig context array, which you’re free to manipulate. Any changes you make to it will be available to the
* template following the tag. Whatever your template hook function returns will be output in place of the tag in
* the template as well.
*
* @param string $hook The hook name.
* @param callback $method The callback function.
*/
public function hook(string $hook, $method)
{
$this->_hooks[$hook][] = $method;
}
/**
* Invokes a template hook.
*
* This is called by [[HookNode|`{% hook %}` tags]].
*
* @param string $hook The hook name.
* @param array &$context The current template context.
* @return string Whatever the hooks returned.
*/
public function invokeHook(string $hook, array &$context): string
{
$return = '';
if (isset($this->_hooks[$hook])) {
foreach ($this->_hooks[$hook] as $method) {
$return .= $method($context);
}
}
return $return;
}
/**
* Sets the JS files that should be marked as already registered.
*
* @param string[] $keys
*/
public function setRegisteredJsFiles(array $keys)
{
$this->_registeredJsFiles = array_flip($keys);
}
/**
* Sets the asset bundle names that should be marked as already registered.
*
* @param string[] $names Asset bundle names
*/
public function setRegisteredAssetBundles(array $names)
{
$this->_registeredAssetBundles = array_flip($names);
}
/**
* @inheritdoc
*/
public function endPage($ajaxMode = false)
{
if (!$ajaxMode && Craft::$app->getRequest()->getIsCpRequest()) {
$this->_registeredJs('registeredJsFiles', $this->_registeredJsFiles);
$this->_registeredJs('registeredAssetBundles', $this->_registeredAssetBundles);
}
parent::endPage($ajaxMode);
}
// Events
// -------------------------------------------------------------------------
/**
* Performs actions before a template is rendered.
*
* @param mixed $template The name of the template to render
* @param array &$variables The variables that should be available to the template
* @return bool Whether the template should be rendered
*/
public function beforeRenderTemplate(string $template, array &$variables): bool
{
// Fire a 'beforeRenderTemplate' event
$event = new TemplateEvent([
'template' => $template,
'variables' => $variables,
]);
$this->trigger(self::EVENT_BEFORE_RENDER_TEMPLATE, $event);
$variables = $event->variables;
return $event->isValid;
}
/**
* Performs actions after a template is rendered.
*
* @param mixed $template The name of the template that was rendered
* @param array $variables The variables that were available to the template
* @param string $output The template’s rendering result
*/
public function afterRenderTemplate(string $template, array $variables, string &$output)
{
// Fire an 'afterRenderTemplate' event
if ($this->hasEventHandlers(self::EVENT_AFTER_RENDER_TEMPLATE)) {
$event = new TemplateEvent([
'template' => $template,
'variables' => $variables,
'output' => $output,
]);
$this->trigger(self::EVENT_AFTER_RENDER_TEMPLATE, $event);
$output = $event->output;
}
}
/**
* Performs actions before a page template is rendered.
*
* @param mixed $template The name of the template to render
* @param array &$variables The variables that should be available to the template
* @return bool Whether the template should be rendered
*/
public function beforeRenderPageTemplate(string $template, array &$variables): bool
{
// Fire a 'beforeRenderPageTemplate' event
$event = new TemplateEvent([
'template' => $template,
'variables' => &$variables,
]);
$this->trigger(self::EVENT_BEFORE_RENDER_PAGE_TEMPLATE, $event);
$variables = $event->variables;
return $event->isValid;
}
/**
* Performs actions after a page template is rendered.
*
* @param mixed $template The name of the template that was rendered
* @param array $variables The variables that were available to the template
* @param string $output The template’s rendering result
*/
public function afterRenderPageTemplate(string $template, array $variables, string &$output)
{
// Fire an 'afterRenderPageTemplate' event
if ($this->hasEventHandlers(self::EVENT_AFTER_RENDER_PAGE_TEMPLATE)) {
$event = new TemplateEvent([
'template' => $template,
'variables' => $variables,
'output' => $output,
]);
$this->trigger(self::EVENT_AFTER_RENDER_PAGE_TEMPLATE, $event);
$output = $event->output;
}
}
// Protected Methods
// =========================================================================
/**
* @inheritdoc
*/
protected function renderHeadHtml()
{
$lines = [];
if (!empty($this->title)) {
$lines[] = '<title>' . Html::encode($this->title) . '</title>';
}
if (!empty($this->_scripts[self::POS_HEAD])) {
$lines[] = implode("\n", $this->_scripts[self::POS_HEAD]);
}
$html = parent::renderHeadHtml();
return empty($lines) ? $html : implode("\n", $lines) . $html;
}
/**
* @inheritdoc
*/
protected function renderBodyBeginHtml()
{
$lines = [];
if (!empty($this->_scripts[self::POS_BEGIN])) {
$lines[] = implode("\n", $this->_scripts[self::POS_BEGIN]);
}
$html = parent::renderBodyBeginHtml();
return empty($lines) ? $html : implode("\n", $lines) . $html;
}
/**
* @inheritdoc
*/
protected function renderBodyEndHtml($ajaxMode)
{
$lines = [];
if (!empty($this->_scripts[self::POS_END])) {
$lines[] = implode("\n", $this->_scripts[self::POS_END]);
}
$html = parent::renderBodyEndHtml($ajaxMode);
return empty($lines) ? $html : implode("\n", $lines) . $html;
}
/**
* Registers any asset bundles and JS code that were queued-up in the session flash data.
*
* @throws Exception if any of the registered asset bundles are not actually asset bundles
*/
protected function registerAssetFlashes()
{
if (Craft::$app->getRequest()->getIsConsoleRequest()) {
return;
}
$session = Craft::$app->getSession();
if ($session->getIsActive()) {
foreach ($session->getAssetBundleFlashes(true) as $name => $position) {
if (!is_subclass_of($name, YiiAssetBundle::class)) {
throw new Exception("$name is not an asset bundle");
}
$this->registerAssetBundle($name, $position);
}
foreach ($session->getJsFlashes(true) as list($js, $position, $key)) {
$this->registerJs($js, $position, $key);
}
}
}
/**
* Registers all files provided by all registered asset bundles, including depending bundles files.
*
* Removes a bundle from [[assetBundles]] once files are registered.
*/
protected function registerAllAssetFiles()
{
foreach ($this->assetBundles as $bundleName => $bundle) {
$this->registerAssetFiles($bundleName);
}
}
/**
* @inheritdoc
*/
protected function registerAssetFiles($name)
{
// Don't re-register bundles
if (isset($this->_registeredAssetBundles[$name])) {
return;
}
$this->_registeredAssetBundles[$name] = true;
parent::registerAssetFiles($name);
}
// Private Methods
// =========================================================================
/**
* Ensures that a template name isn't null, and that it doesn't lead outside the template folder. Borrowed from
* [[Twig_Loader_Filesystem]].
*
* @param string $name
* @throws \Twig_Error_Loader
*/
private function _validateTemplateName(string $name)
{
if (StringHelper::contains($name, "\0")) {
throw new \Twig_Error_Loader(Craft::t('app', 'A template name cannot contain NUL bytes.'));
}
if (Path::ensurePathIsContained($name) === false) {
Craft::error('Someone tried to load a template outside the templates folder: ' . $name);
throw new \Twig_Error_Loader(Craft::t('app', 'Looks like you are trying to load a template outside the template folder.'));
}
}
/**
* Searches for a template files, and returns the first match if there is one.
*
* @param string $basePath The base path to be looking in.
* @param string $name The name of the template to be looking for.
* @return string|null The matching file path, or `null`.
*/
private function _resolveTemplate(string $basePath, string $name)
{
// Normalize the path and name
$basePath = FileHelper::normalizePath($basePath);
$name = trim(FileHelper::normalizePath($name), '/');
// $name could be an empty string (e.g. to load the homepage template)
if ($name !== '') {
// Maybe $name is already the full file path
$testPath = $basePath . DIRECTORY_SEPARATOR . $name;
if (is_file($testPath)) {
return $testPath;
}
foreach ($this->_defaultTemplateExtensions as $extension) {
$testPath = $basePath . DIRECTORY_SEPARATOR . $name . '.' . $extension;
if (is_file($testPath)) {
return $testPath;
}
}
}
foreach ($this->_indexTemplateFilenames as $filename) {
foreach ($this->_defaultTemplateExtensions as $extension) {
$testPath = $basePath . ($name !== '' ? DIRECTORY_SEPARATOR . $name : '') . DIRECTORY_SEPARATOR . $filename . '.' . $extension;
if (is_file($testPath)) {
return $testPath;
}
}
}
return null;
}
/**
* Returns the Twig environment options
*
* @return array
*/
private function _getTwigOptions(): array
{
if ($this->_twigOptions !== null) {
return $this->_twigOptions;
}
$this->_twigOptions = [
'base_template_class' => Template::class,
// See: https://github.com/twigphp/Twig/issues/1951
'cache' => Craft::$app->getPath()->getCompiledTemplatesPath(),
'auto_reload' => true,
'charset' => Craft::$app->charset,
];
if (YII_DEBUG) {
$this->_twigOptions['debug'] = true;
$this->_twigOptions['strict_variables'] = true;
}
return $this->_twigOptions;
}
/**
* Returns any registered template roots.
*
* @param string $which 'cp' or 'site'
* @return array
*/
private function _getTemplateRoots(string $which): array
{
if (isset($this->_templateRoots[$which])) {
return $this->_templateRoots[$which];
}
if ($which === 'cp') {
$name = self::EVENT_REGISTER_CP_TEMPLATE_ROOTS;
} else {
$name = self::EVENT_REGISTER_SITE_TEMPLATE_ROOTS;
}
$event = new RegisterTemplateRootsEvent();
$this->trigger($name, $event);
$roots = [];
foreach ($event->roots as $templatePath => $dir) {
$templatePath = strtolower(trim($templatePath, '/'));
$roots[$templatePath][] = $dir;
}
// Longest (most specific) first
krsort($roots, SORT_STRING);
return $this->_templateRoots[$which] = $roots;
}
/**
* Replaces textarea contents with a marker.
*
* @param array $matches
* @return string
*/
private function _createTextareaMarker(array $matches): string
{
$marker = '{marker:' . StringHelper::randomString() . '}';
$this->_textareaMarkers[$marker] = $matches[2];
return $matches[1] . $marker . $matches[3];
}
private function _registeredJs($property, $names)
{
if (empty($names)) {
return;
}
$js = "if (typeof Craft !== 'undefined') {\n";
foreach (array_keys($names) as $name) {
if ($name) {
$jsName = Json::encode($name);
$js .= " Craft.{$property}[{$jsName}] = true;\n";
}
}
$js .= '}';
$this->registerJs($js, self::POS_HEAD);
}
/**
* Returns the HTML for an element in the CP.
*
* @param array &$context
* @return string|null
*/
private function _getCpElementHtml(array &$context)
{
if (!isset($context['element'])) {
return null;
}
/** @var Element $element */
$element = $context['element'];
if (!isset($context['context'])) {
$context['context'] = 'index';
}
// How big is the element going to be?
if (isset($context['size']) && ($context['size'] === 'small' || $context['size'] === 'large')) {
$elementSize = $context['size'];
} else if (isset($context['viewMode']) && $context['viewMode'] === 'thumbs') {
$elementSize = 'large';
} else {
$elementSize = 'small';
}
// Create the thumb/icon image, if there is one
// ---------------------------------------------------------------------
$thumbUrl = $element->getThumbUrl(self::$_elementThumbSizes[0]);
if ($thumbUrl !== null) {
$srcsets = [];
foreach (self::$_elementThumbSizes as $i => $size) {
if ($i == 0) {
$srcset = $thumbUrl;
} else {
$srcset = $element->getThumbUrl($size);
}
$srcsets[] = $srcset . ' ' . $size . 'w';
}
$sizesHtml = ($elementSize === 'small' ? self::$_elementThumbSizes[0] : self::$_elementThumbSizes[2]) . 'px';
$srcsetHtml = implode(', ', $srcsets);
$imgHtml = "<div class='elementthumb' data-sizes='{$sizesHtml}' data-srcset='{$srcsetHtml}'></div>";
} else {
$imgHtml = '';
}
$htmlAttributes = array_merge(
$element->getHtmlAttributes($context['context']),
[
'class' => 'element ' . $elementSize,
'data-type' => get_class($element),
'data-id' => $element->id,
'data-site-id' => $element->siteId,
'data-status' => $element->getStatus(),
'data-label' => (string)$element,
'data-url' => $element->getUrl(),
'data-level' => $element->level,
]);
if ($context['context'] === 'field') {
$htmlAttributes['class'] .= ' removable';
}
if ($element::hasStatuses()) {
$htmlAttributes['class'] .= ' hasstatus';
}
if ($thumbUrl !== null) {
$htmlAttributes['class'] .= ' hasthumb';
}
$html = '<div';
foreach ($htmlAttributes as $attribute => $value) {
$html .= ' ' . $attribute . ($value !== null ? '="' . HtmlHelper::encode($value) . '"' : '');
}
if (ElementHelper::isElementEditable($element)) {
$html .= ' data-editable';
}
if ($element->trashed) {
$html .= ' data-trashed';
}
$html .= '>';
if ($context['context'] === 'field' && isset($context['name'])) {
$html .= '<input type="hidden" name="' . $context['name'] . '[]" value="' . $element->id . '">';
$html .= '<a class="delete icon" title="' . Craft::t('app', 'Remove') . '"></a> ';
}
if ($element::hasStatuses()) {
$status = $element->getStatus();
$statusClasses = $status . ' ' . ($element::statuses()[$status]['color'] ?? '');
$html .= '<span class="status ' . $statusClasses . '"></span>';
}
$html .= $imgHtml;
$html .= '<div class="label">';
$html .= '<span class="title">';
$label = HtmlHelper::encode($element);
if ($context['context'] === 'index' && !$element->trashed && ($cpEditUrl = $element->getCpEditUrl())) {
$cpEditUrl = HtmlHelper::encode($cpEditUrl);
$html .= "<a href=\"{$cpEditUrl}\">{$label}</a>";
} else {
$html .= $label;
}
$html .= '</span></div></div>';
return $html;
}
}