3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * Fix for bug CRM-392. Not sure if this is the best fix or it will impact
20 * other similar PEAR packages. doubt it
22 if (!class_exists('Smarty')) {
23 require_once 'Smarty/Smarty.class.php';
29 class CRM_Core_Smarty
extends Smarty
{
31 // use print.tpl and bypass the CMS. Civi prints a valid html file
33 // this and all the below bypasses the CMS html surrounding it and assumes we will embed this within other pages
35 // sends the generated html to the chosen pdf engine
37 // this options also skips the enclosing form html and does not
38 // generate any of the hidden fields, most notably qfKey
39 // this is typically used in ajax scripts to embed form snippets based on user choices
41 // this prints a complete form and also generates a qfKey, can we replace this with
42 // snippet = 2?? Does the constant _NOFFORM do anything?
44 // Note: added in v 4.3 with the value '6'
45 // Value changed in 4.5 to 'json' for better readability
46 // @see CRM_Core_Page_AJAX::returnJsonResponse
50 * We only need one instance of this object. So we use the singleton
51 * pattern and cache the instance in this variable
55 static private $_singleton = NULL;
60 * A list of variables ot save temporarily in format (string $name => mixed $value).
64 private $backupFrames = [];
66 private function initialize() {
67 $config = CRM_Core_Config
::singleton();
69 if (isset($config->customTemplateDir
) && $config->customTemplateDir
) {
70 $this->template_dir
= array_merge([$config->customTemplateDir
],
75 $this->template_dir
= $config->templateDir
;
77 $this->compile_dir
= CRM_Utils_File
::addTrailingSlash(CRM_Utils_File
::addTrailingSlash($config->templateCompileDir
) . $this->getLocale());
78 CRM_Utils_File
::createDir($this->compile_dir
);
79 CRM_Utils_File
::restrictAccess($this->compile_dir
);
81 // check and ensure it is writable
82 // else we sometime suppress errors quietly and this results
83 // in blank emails etc
84 if (!is_writable($this->compile_dir
)) {
85 echo "CiviCRM does not have permission to write temp files in {$this->compile_dir}, Exiting";
89 $this->use_sub_dirs
= TRUE;
91 $customPluginsDir = NULL;
92 if (!empty($config->customPHPPathDir
) ||
$config->customPHPPathDir
=== '0') {
94 = $config->customPHPPathDir
. DIRECTORY_SEPARATOR
.
95 'CRM' . DIRECTORY_SEPARATOR
.
96 'Core' . DIRECTORY_SEPARATOR
.
97 'Smarty' . DIRECTORY_SEPARATOR
.
98 'plugins' . DIRECTORY_SEPARATOR
;
99 if (!file_exists($customPluginsDir)) {
100 $customPluginsDir = NULL;
104 $pkgsDir = Civi
::paths()->getVariable('civicrm.packages', 'path');
105 $smartyDir = $pkgsDir . DIRECTORY_SEPARATOR
. 'Smarty' . DIRECTORY_SEPARATOR
;
106 $pluginsDir = __DIR__
. DIRECTORY_SEPARATOR
. 'Smarty' . DIRECTORY_SEPARATOR
. 'plugins' . DIRECTORY_SEPARATOR
;
108 if ($customPluginsDir) {
109 $this->plugins_dir
= [$customPluginsDir, $smartyDir . 'plugins', $pluginsDir];
112 $this->plugins_dir
= [$smartyDir . 'plugins', $pluginsDir];
115 $this->compile_check
= $this->isCheckSmartyIsCompiled();
117 // add the session and the config here
118 $session = CRM_Core_Session
::singleton();
120 $this->assign_by_ref('config', $config);
121 $this->assign_by_ref('session', $session);
123 $tsLocale = CRM_Core_I18n
::getLocale();
124 $this->assign('tsLocale', $tsLocale);
126 // CRM-7163 hack: we don’t display langSwitch on upgrades anyway
127 if (!CRM_Core_Config
::isUpgradeMode()) {
128 $this->assign('langSwitch', CRM_Core_I18n
::uiLanguages());
131 $this->register_function('crmURL', ['CRM_Utils_System', 'crmURL']);
132 if (CRM_Utils_Constant
::value('CIVICRM_SMARTY_DEFAULT_ESCAPE')) {
133 // When default escape is enabled if the core escape is called before
134 // any custom escaping is done the modifier_escape function is not
135 // found, so require_once straight away. Note this was hit on the basic
136 // contribution dashboard from RecentlyViewed.tpl
137 require_once 'Smarty/plugins/modifier.escape.php';
138 if (!isset($this->_plugins
['modifier']['escape'])) {
139 $this->register_modifier('escape', ['CRM_Core_Smarty', 'escape']);
141 $this->default_modifiers
[] = 'escape:"htmlall"';
143 $this->load_filter('pre', 'resetExtScope');
145 $this->assign('crmPermissions', new CRM_Core_Smarty_Permissions());
147 if ($config->debug
) {
148 $this->error_reporting
= E_ALL
;
153 * Static instance provider.
155 * Method providing static instance of SmartTemplate, as
156 * in Singleton pattern.
158 * @return \CRM_Core_Smarty
160 public static function &singleton() {
161 if (!isset(self
::$_singleton)) {
162 self
::$_singleton = new CRM_Core_Smarty();
163 self
::$_singleton->initialize();
165 self
::registerStringResource();
167 return self
::$_singleton;
171 * Executes & returns or displays the template results
173 * @param string $resource_name
174 * @param string $cache_id
175 * @param string $compile_id
176 * @param bool $display
178 * @return bool|mixed|string
180 * @noinspection PhpDocMissingThrowsInspection
181 * @noinspection PhpUnhandledExceptionInspection
183 public function fetch($resource_name, $cache_id = NULL, $compile_id = NULL, $display = FALSE) {
184 if (preg_match('/^(\s+)?string:/', $resource_name)) {
185 $old_security = $this->security
;
186 $this->security
= TRUE;
189 $output = parent
::fetch($resource_name, $cache_id, $compile_id, $display);
192 if (isset($old_security)) {
193 $this->security
= $old_security;
200 * Ensure these variables are set to make it easier to access them without e-notice.
202 * @param array $variables
204 public function ensureVariablesAreAssigned(array $variables): void
{
205 foreach ($variables as $variable) {
206 if (!isset($this->get_template_vars()[$variable])) {
207 $this->assign($variable);
213 * Avoid e-notices on pages with tabs,
214 * by ensuring tabHeader items contain the necessary keys
216 public function addExpectedTabHeaderKeys(): void
{
225 $tabs = $this->get_template_vars('tabHeader');
226 foreach ((array) $tabs as $i => $tab) {
227 $tabs[$i] = array_merge($defaults, $tab);
229 $this->assign('tabHeader', $tabs);
233 * Fetch a template (while using certain variables)
235 * @param string $resource_name
237 * (string $name => mixed $value) variables to export to Smarty.
239 * @return bool|mixed|string
241 public function fetchWith($resource_name, $vars) {
242 $this->pushScope($vars);
244 $result = $this->fetch($resource_name);
246 catch (Exception
$e) {
247 // simulate try { ... } finally { ... }
256 * @param string $name
259 public function appendValue($name, $value) {
260 $currentValue = $this->get_template_vars($name);
261 if (!$currentValue) {
262 $this->assign($name, $value);
265 if (strpos($currentValue, $value) === FALSE) {
266 $this->assign($name, $currentValue . $value);
271 public function clearTemplateVars() {
272 foreach (array_keys($this->_tpl_vars
) as $key) {
273 if ($key == 'config' ||
$key == 'session') {
276 unset($this->_tpl_vars
[$key]);
280 public static function registerStringResource() {
281 require_once 'CRM/Core/Smarty/resources/String.php';
282 civicrm_smarty_register_string_resource();
288 public function addTemplateDir($path) {
289 if (is_array($this->template_dir
)) {
290 array_unshift($this->template_dir
, $path);
293 $this->template_dir
= [$path, $this->template_dir
];
299 * Temporarily assign a list of variables.
302 * $smarty->pushScope(array(
303 * 'first_name' => 'Alice',
304 * 'last_name' => 'roberts',
306 * $html = $smarty->fetch('view-contact.tpl');
307 * $smarty->popScope();
311 * (string $name => mixed $value).
312 * @return CRM_Core_Smarty
315 public function pushScope($vars) {
316 $oldVars = $this->get_template_vars();
318 foreach ($vars as $key => $value) {
319 $backupFrame[$key] = $oldVars[$key] ??
NULL;
321 $this->backupFrames
[] = $backupFrame;
323 $this->assignAll($vars);
329 * Remove any values that were previously pushed.
331 * @return CRM_Core_Smarty
334 public function popScope() {
335 $this->assignAll(array_pop($this->backupFrames
));
341 * (string $name => mixed $value).
342 * @return CRM_Core_Smarty
344 public function assignAll($vars) {
345 foreach ($vars as $key => $value) {
346 $this->assign($key, $value);
352 * Get the locale for translation.
356 private function getLocale() {
357 $tsLocale = CRM_Core_I18n
::getLocale();
358 if (!empty($tsLocale)) {
362 $config = CRM_Core_Config
::singleton();
363 if (!empty($config->lcMessages
)) {
364 return $config->lcMessages
;
371 * Get the compile_check value.
375 private function isCheckSmartyIsCompiled() {
376 // check for define in civicrm.settings.php as FALSE, otherwise returns TRUE
377 return CRM_Utils_Constant
::value('CIVICRM_TEMPLATE_COMPILE_CHECK', TRUE);
381 * Smarty escape modifier plugin.
383 * This replaces the core smarty modifier and basically does a lot of
384 * early-returning before calling the core function.
386 * It early returns on patterns that are common 'no-escape' patterns
387 * in CiviCRM - this list can be honed over time.
389 * It also logs anything that is actually escaped. Since this only kicks
390 * in when CIVICRM_SMARTY_DEFAULT_ESCAPE is defined it is ok to be aggressive
391 * about logging as we mostly care about developers using it at this stage.
393 * Note we don't actually use 'htmlall' anywhere in our tpl layer yet so
394 * anything coming in with this be happening because of the default modifier.
396 * Also note the right way to opt a field OUT of escaping is
397 * ``{$fieldName|smarty:nodefaults}``
398 * This should be used for fields with known html AND for fields where
399 * we are doing empty or isset checks - as otherwise the value is passed for
400 * escaping first so you still get an enotice for 'empty' or a fatal for 'isset'
404 * Purpose: Escape the string according to escapement type
406 * @link http://smarty.php.net/manual/en/language.modifier.escape.php
407 * escape (Smarty online manual)
408 * @author Monte Ohrt <monte at ohrt dot com>
410 * @param string $string
411 * @param string $esc_type
412 * @param string $char_set
416 public static function escape($string, $esc_type = 'html', $char_set = 'UTF-8') {
417 // CiviCRM variables are often arrays - just handle them.
418 // The early return on booleans & numbers is mostly to prevent them being
419 // logged as 'changed' when they are cast to a string.
420 if (!is_scalar($string) ||
empty($string) ||
is_bool($string) ||
is_numeric($string) ||
$esc_type === 'none') {
423 if ($esc_type === 'htmlall') {
424 // 'htmlall' is the nothing-specified default.
425 // Don't escape things we think quickform added.
426 if (strpos($string, '<input') === 0
427 ||
strpos($string, '<select') === 0
428 // Not handling as yet but these ones really should get some love.
429 ||
strpos($string, '<label') === 0
430 ||
strpos($string, '<button') === 0
431 ||
strpos($string, '<span class="crm-frozen-field">') === 0
432 ||
strpos($string, '<textarea') === 0
434 // The ones below this point are hopefully here short term.
435 ||
strpos($string, '<a') === 0
436 // Message templates screen
437 ||
strpos($string, '<span><a href') === 0
438 // Not sure how big a pattern this is - used in Pledge view tab
439 // not sure if it needs escaping
440 ||
strpos($string, ' action="/civicrm/') === 0
441 // eg. Tag edit page, civicrm/admin/financial/financialType/accounts?action=add&reset=1&aid=1
442 ||
strpos($string, ' action="" method="post"') === 0
443 // This seems to be urls...
444 ||
strpos($string, '/civicrm/') === 0
445 // Validation error message - eg. <span class="crm-error">Tournament Fees is a required field.</span>
447 <span class="crm-error">') === 0
448 // e.g from participant tab class="action-item" href=/civicrm/contact/view/participant?reset=1&action=add&cid=142&context=participant
449 ||
strpos($string, 'class="action-item" href=/civicrm/"') === 0
451 // Do not escape the above common patterns.
456 $value = smarty_modifier_escape($string, $esc_type, $char_set);
457 if ($value !== $string) {
458 Civi
::log()->debug('smarty escaping original {original}, escaped {escaped} type {type} charset {charset}', [
459 'original' => $string,
462 'charset' => $char_set,