From 889b06177d5b8e2a81c94833deac3e24b5020872 Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Sun, 3 Oct 2021 15:23:15 +1300 Subject: [PATCH] Split mechanism to derive token metadata from display --- CRM/Activity/Tokens.php | 41 ++++- CRM/Core/EntityTokens.php | 153 ++++++++++++++---- CRM/Event/ParticipantTokens.php | 2 +- CRM/Event/Tokens.php | 7 +- CRM/Member/Tokens.php | 26 +++ .../Form/Task/PDFLetterCommonTest.php | 20 +-- .../Contribute/ActionMapping/ByTypeTest.php | 46 ++++-- .../CRM/Utils/TokenConsistencyTest.php | 53 +++--- 8 files changed, 266 insertions(+), 82 deletions(-) diff --git a/CRM/Activity/Tokens.php b/CRM/Activity/Tokens.php index 222bf680a2..51c8d1be4f 100644 --- a/CRM/Activity/Tokens.php +++ b/CRM/Activity/Tokens.php @@ -36,6 +36,19 @@ class CRM_Activity_Tokens extends CRM_Core_EntityTokens { use CRM_Core_TokenTrait; + /** + * Class constructor. + * + * Overriding because the trait needs this to happen & trying to + * leave any changes that affect the trait out of scope here. + */ + public function __construct() { + $this->entity = 'activity'; + $this->tokenNames = array_merge( + $this->getBasicTokens(), + $this->getCustomFieldTokens()); + } + /** * Get the entity name for api v4 calls. * @@ -171,6 +184,32 @@ class CRM_Activity_Tokens extends CRM_Core_EntityTokens { return $tokens; } + /** + * Get fields Fieldshistorically not advertised for tokens. + * + * @return string[] + */ + protected function getSkippedFields(): array { + return array_merge(parent::getSkippedFields(), [ + 'source_record_id', + 'phone_id', + 'phone_number', + 'priority_id', + 'parent_id', + 'is_test', + 'medium_id', + 'is_auto', + 'relationship_id', + 'is_current_revision', + 'original_id', + 'result', + 'is_deleted', + 'engagement_level', + 'weight', + 'is_star', + ]); + } + /** * @inheritDoc */ @@ -183,7 +222,7 @@ class CRM_Activity_Tokens extends CRM_Core_EntityTokens { $activeTokens = []; // if message token contains '_\d+_', then treat as '_N_' foreach ($messageTokens[$this->entity] as $msgToken) { - if (array_key_exists($msgToken, $this->tokenNames)) { + if (array_key_exists($msgToken, $this->tokensMetadata)) { $activeTokens[] = $msgToken; } elseif (in_array($msgToken, ['campaign', 'activity_id', 'status', 'activity_type', 'case_id'])) { diff --git a/CRM/Core/EntityTokens.php b/CRM/Core/EntityTokens.php index 56c1428deb..8a4a0c22f6 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -29,11 +29,24 @@ use Civi\Token\TokenProcessor; */ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { + /** + * Metadata about all tokens. + * + * @var array + */ + protected $tokensMetadata = []; /** * @var array */ protected $prefetch = []; + /** + * Should permissions be checked when loading tokens. + * + * @var bool + */ + protected $checkPermissions = FALSE; + /** * Register the declared tokens. * @@ -44,17 +57,88 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { if (!$this->checkActive($e->getTokenProcessor())) { return; } - foreach ($this->getAllTokens() as $name => $label) { - if (!in_array($name, $this->getHiddenTokens(), TRUE)) { + foreach ($this->getTokenMetadata() as $field) { + if ($field['audience'] === 'user') { $e->register([ 'entity' => $this->entity, - 'field' => $name, - 'label' => $label, + 'field' => $field['name'], + 'label' => $field['title'], ]); } } } + /** + * Get the metadata about the available tokens + * + * @return array + */ + protected function getTokenMetadata(): array { + if (empty($this->tokensMetadata)) { + $cacheKey = __CLASS__ . 'token_metadata' . $this->getApiEntityName() . CRM_Core_Config::domainID() . '_' . CRM_Core_I18n::getLocale(); + if ($this->checkPermissions) { + $cacheKey .= '__' . CRM_Core_Session::getLoggedInContactID(); + } + if (Civi::cache('metadata')->has($cacheKey)) { + $this->tokensMetadata = Civi::cache('metadata')->get($cacheKey); + } + else { + foreach (array_merge($this->getFieldMetadata(), $this->getBespokeTokens()) as $field) { + if ( + $field['type'] === 'Custom' + || !empty($this->getBespokeTokens()[$field['name']]) + || in_array($field['name'], $this->getExposedFields(), TRUE) + ) { + $field['audience'] = 'user'; + if ($field['name'] === 'contact_id') { + // Since {contact.id} is almost always present don't confuse users + // by also adding (e.g {participant.contact_id) + $field['audience'] = 'sysadmin'; + } + if (!empty($this->getTokenMetadataOverrides()[$field['name']])) { + $field = array_merge($field, $this->getTokenMetadataOverrides()[$field['name']]); + } + if ($field['type'] === 'Custom') { + // Convert to apiv3 style for now. Later we can add v4 with + // portable naming & support for labels/ dates etc so let's leave + // the space open for that. + // Not the existing quickform widget has handling for the custom field + // format based on the title using this syntax. + $field['name'] = 'custom_' . $field['custom_field_id']; + $parts = explode(': ', $field['label']); + $field['title'] = "{$parts[1]} :: {$parts[0]}"; + } + if ( + ($field['options'] || !empty($field['suffixes'])) + // At the time of writing currency didn't have a label option - this may have changed. + && !in_array($field['name'], $this->getCurrencyFieldName(), TRUE) + ) { + $this->tokensMetadata[$field['name'] . ':label'] = $this->tokensMetadata[$field['name'] . ':name'] = $field; + $fieldLabel = $field['input_attrs']['label'] ?? $field['label']; + $this->tokensMetadata[$field['name'] . ':label']['name'] = $field['name'] . ':label'; + $this->tokensMetadata[$field['name'] . ':name']['name'] = $field['name'] . ':name'; + $this->tokensMetadata[$field['name'] . ':name']['audience'] = 'sysadmin'; + $this->tokensMetadata[$field['name'] . ':label']['title'] = $fieldLabel; + $this->tokensMetadata[$field['name'] . ':name']['title'] = ts('Machine name') . ': ' . $fieldLabel; + $field['audience'] = 'sysadmin'; + } + if ($field['data_type'] === 'Boolean') { + $this->tokensMetadata[$field['name'] . ':label'] = $field; + $this->tokensMetadata[$field['name'] . ':label']['name'] = $field['name'] . ':label'; + $field['audience'] = 'sysadmin'; + } + $this->tokensMetadata[$field['name']] = $field; + } + } + foreach ($this->getHiddenTokens() as $name) { + $this->tokensMetadata[$name]['audience'] = 'hidden'; + } + Civi::cache('metadata')->set($cacheKey, $this->tokensMetadata); + } + } + return $this->tokensMetadata; + } + /** * @inheritDoc * @throws \CRM_Core_Exception @@ -155,31 +239,6 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { return array_keys($this->getBasicTokens()); } - /** - * Get all the tokens supported by this processor. - * - * @return array|string[] - * @throws \API_Exception - */ - protected function getAllTokens(): array { - $basicTokens = $this->getBasicTokens(); - foreach (array_keys($basicTokens) as $fieldName) { - // The goal is to be able to render more complete tokens - // (eg. actual booleans, field names, raw ids) for a more - // advanced audiences - ie those using conditionals - // and to specify that audience in the api that retrieves. - // But, for now, let's not advertise, given that most of these fields - // aren't really needed even once... - if ($this->isBooleanField($fieldName)) { - unset($basicTokens[$fieldName]); - } - } - foreach ($this->getBespokeTokens() as $token) { - $basicTokens[$token['name']] = $token['title']; - } - return array_merge($basicTokens, $this->getPseudoTokens(), CRM_Utils_Token::getCustomFieldTokens($this->getApiEntityName())); - } - /** * Is the given field a boolean field. * @@ -360,8 +419,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * Class constructor. */ public function __construct() { - $tokens = $this->getAllTokens(); - parent::__construct($this->getEntityName(), $tokens); + parent::__construct($this->getEntityName(), []); } /** @@ -449,11 +507,11 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return string[] */ - public function getSkippedFields(): array { + protected function getSkippedFields(): array { // tags is offered in 'case' & is one of the only fields that is // 'not a real field' offered up by case - seems like an oddity // we should skip at the top level for now. - $fields = ['contact_id', 'tags']; + $fields = ['tags']; if (!CRM_Campaign_BAO_Campaign::isCampaignEnable()) { $fields[] = 'campaign_id'; } @@ -513,7 +571,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * @throws \API_Exception */ public function getPrefetchFields(TokenValueEvent $e): array { - $allTokens = array_keys($this->getAllTokens()); + $allTokens = array_keys($this->getTokenMetadata()); $requiredFields = array_intersect($this->getActiveTokens($e), $allTokens); if (empty($requiredFields)) { return []; @@ -569,4 +627,31 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { } } + /** + * Get any overrides for token metadata. + * + * This is most obviously used for setting the audience, which + * will affect widget-presence. + * + * @return \string[][] + */ + protected function getTokenMetadataOverrides(): array { + return []; + } + + /** + * To handle variable tokens, override this function and return the active tokens. + * + * @param \Civi\Token\Event\TokenValueEvent $e + * + * @return mixed + */ + public function getActiveTokens(TokenValueEvent $e) { + $messageTokens = $e->getTokenProcessor()->getMessageTokens(); + if (!isset($messageTokens[$this->entity])) { + return FALSE; + } + return array_intersect($messageTokens[$this->entity], array_keys($this->getTokenMetadata())); + } + } diff --git a/CRM/Event/ParticipantTokens.php b/CRM/Event/ParticipantTokens.php index f33af5d2b5..01d57750ab 100644 --- a/CRM/Event/ParticipantTokens.php +++ b/CRM/Event/ParticipantTokens.php @@ -111,7 +111,7 @@ class CRM_Event_ParticipantTokens extends CRM_Core_EntityTokens { * * @return string[] */ - public function getSkippedFields(): array { + protected function getSkippedFields(): array { $fields = parent::getSkippedFields(); // Never add these 2 fields - may not be a stable part of the schema. // This field is on it's way out of core. diff --git a/CRM/Event/Tokens.php b/CRM/Event/Tokens.php index 87ad1de375..91d5a8797b 100644 --- a/CRM/Event/Tokens.php +++ b/CRM/Event/Tokens.php @@ -112,8 +112,11 @@ class CRM_Event_Tokens extends CRM_Core_EntityTokens { */ protected function getEventTokenValues(int $eventID = NULL): array { $cacheKey = __CLASS__ . 'event_tokens' . $eventID . '_' . CRM_Core_I18n::getLocale(); + if ($this->checkPermissions) { + $cacheKey .= '__' . CRM_Core_Session::getLoggedInContactID(); + } if (!Civi::cache('metadata')->has($cacheKey)) { - $event = Event::get(FALSE)->addWhere('id', '=', $eventID) + $event = Event::get($this->checkPermissions)->addWhere('id', '=', $eventID) ->setSelect(array_merge([ 'loc_block_id.address_id.street_address', 'loc_block_id.address_id.city', @@ -140,7 +143,7 @@ class CRM_Event_Tokens extends CRM_Core_EntityTokens { $tokens['contact_phone']['text/html'] = $event['loc_block_id.phone_id.phone']; $tokens['contact_email']['text/html'] = $event['loc_block_id.email_id.email']; - foreach (array_keys($this->getAllTokens()) as $field) { + foreach (array_keys($this->getTokenMetadata()) as $field) { if (!isset($tokens[$field])) { if ($this->isCustomField($field)) { $this->prefetch[$eventID] = $event; diff --git a/CRM/Member/Tokens.php b/CRM/Member/Tokens.php index 3d369285d0..35ba54a355 100644 --- a/CRM/Member/Tokens.php +++ b/CRM/Member/Tokens.php @@ -50,6 +50,8 @@ class CRM_Member_Tokens extends CRM_Core_EntityTokens { 'end_date', 'status_id', 'membership_type_id', + 'source', + 'status_override_end_date', ]; } @@ -67,6 +69,30 @@ class CRM_Member_Tokens extends CRM_Core_EntityTokens { } } + /** + * Get any overrides for token metadata. + * + * This is most obviously used for setting the audience, which + * will affect widget-presence. + * + * Changing the audience is done in order to simplify the + * UI for more general users. + * + * @return \string[][] + */ + protected function getTokenMetadataOverrides(): array { + return [ + 'owner_membership_id' => ['audience' => 'sysadmin'], + 'max_related' => ['audience' => 'sysadmin'], + 'contribution_recur_id' => ['audience' => 'sysadmin'], + 'is_override' => ['audience' => 'sysadmin'], + 'is_test' => ['audience' => 'sysadmin'], + // Pay later is considered to be unreliable in the schema + // and will eventually be removed. + 'is_pay_later' => ['audience' => 'deprecated'], + ]; + } + /** * Get fields which need to be returned to render another token. * diff --git a/tests/phpunit/CRM/Activity/Form/Task/PDFLetterCommonTest.php b/tests/phpunit/CRM/Activity/Form/Task/PDFLetterCommonTest.php index 878aa70ba3..d70020cf1e 100644 --- a/tests/phpunit/CRM/Activity/Form/Task/PDFLetterCommonTest.php +++ b/tests/phpunit/CRM/Activity/Form/Task/PDFLetterCommonTest.php @@ -68,22 +68,16 @@ class CRM_Activity_Form_Task_PDFLetterCommonTest extends CiviUnitTestCase { protected function getActivityTokens(): array { return [ '{activity.id}' => 'Activity ID', - '{activity.subject}' => 'Activity Subject', - '{activity.details}' => 'Activity Details', - '{activity.activity_date_time}' => 'Activity Date-Time', - '{activity.created_date}' => 'Activity Created Date', - '{activity.modified_date}' => 'Activity Modified Date', - '{activity.activity_type_id}' => 'Activity Type ID', - '{activity.status_id}' => 'Activity Status ID', - '{activity.location}' => 'Activity Location', - '{activity.duration}' => 'Activity Duration', + '{activity.subject}' => 'Subject', + '{activity.details}' => 'Details', + '{activity.activity_date_time}' => 'Activity Date', + '{activity.created_date}' => 'Created Date', + '{activity.modified_date}' => 'Modified Date', + '{activity.location}' => 'Location', + '{activity.duration}' => 'Duration', '{activity.activity_type_id:label}' => 'Activity Type', - '{activity.activity_type_id:name}' => 'Machine name: Activity Type', '{activity.status_id:label}' => 'Activity Status', - '{activity.status_id:name}' => 'Machine name: Activity Status', '{activity.campaign_id:label}' => 'Campaign', - '{activity.campaign_id:name}' => 'Machine name: Campaign', - '{activity.campaign_id}' => 'Campaign ID', '{activity.case_id}' => 'Activity Case ID', ]; } diff --git a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php index 73df1631ce..fa2a50b8db 100644 --- a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php +++ b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php @@ -322,7 +322,7 @@ class CRM_Contribute_ActionMapping_ByTypeTest extends \Civi\ActionSchedule\Abstr $this->mut->checkMailLog($expected); $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [ - 'controller' => get_class(), + 'controller' => __CLASS__, 'smarty' => FALSE, 'schema' => ['contributionId'], 'contributionId' => $this->ids['Contribution']['alice'], @@ -375,17 +375,10 @@ class CRM_Contribute_ActionMapping_ByTypeTest extends \Civi\ActionSchedule\Abstr } $tokens = [ 'id', - 'payment_instrument_id', - 'payment_instrument_id:name', 'payment_instrument_id:label', - 'financial_type_id', - 'financial_type_id:name', 'financial_type_id:label', - 'contribution_status_id', - 'contribution_status_id:name', 'contribution_status_id:label', ]; - $processor = new CRM_Contribute_Tokens(); $legacyTokens = []; $realLegacyTokens = []; foreach (CRM_Core_SelectValues::contributionTokens() as $token => $label) { @@ -397,17 +390,48 @@ class CRM_Contribute_ActionMapping_ByTypeTest extends \Civi\ActionSchedule\Abstr $fields = (array) Contribution::getFields()->addSelect('name', 'title')->execute()->indexBy('name'); $allFields = []; foreach ($fields as $field) { - if (!in_array($field['name'], ['is_test', 'is_pay_later', 'is_template'], TRUE)) { + if (!array_key_exists($field['name'], $this->getUnadvertisedTokens())) { $allFields[$field['name']] = $field['title']; } } // contact ID is skipped. unset($allFields['contact_id']); $this->assertEquals($allFields, $realLegacyTokens); - $this->assertEquals($legacyTokens, $processor->tokenNames); + $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [ + 'controller' => __CLASS__, + 'smarty' => FALSE, + 'schema' => ['contributionId'], + ]); + $comparison = []; + foreach ($tokenProcessor->listTokens() as $token => $label) { + if (strpos($token, '{domain.') === 0) { + // domain token - ignore. + continue; + } + $comparison[substr($token, 14, -1)] = $label; + } + $this->assertEquals($legacyTokens, $comparison); foreach ($tokens as $token) { - $this->assertEquals(CRM_Core_SelectValues::contributionTokens()['{contribution.' . $token . '}'], $processor->tokenNames[$token]); + $this->assertEquals(CRM_Core_SelectValues::contributionTokens()['{contribution.' . $token . '}'], $comparison[$token]); } } + /** + * Get tokens not advertised in the widget. + * + * @return string[] + */ + public function getUnadvertisedTokens(): array { + return [ + 'financial_type_id' => 'Financial Type ID', + 'contribution_page_id' => 'Contribution Page ID', + 'payment_instrument_id' => 'Payment Method ID', + 'is_test' => 'Is test', + 'is_pay_later' => 'is pay later', + 'is_template' => 'is_template', + 'contribution_status_id' => 'Contribution Status ID', + 'campaign_id' => 'Campaign ID', + ]; + } + } diff --git a/tests/phpunit/CRM/Utils/TokenConsistencyTest.php b/tests/phpunit/CRM/Utils/TokenConsistencyTest.php index 33612e5778..679cd0b448 100644 --- a/tests/phpunit/CRM/Utils/TokenConsistencyTest.php +++ b/tests/phpunit/CRM/Utils/TokenConsistencyTest.php @@ -72,21 +72,12 @@ class CRM_Utils_TokenConsistencyTest extends CiviUnitTestCase { // And check our deprecated tokens still work. $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, '{case.case_type_id} {case.status_id}'); $this->assertEquals('Housing Support Ongoing', $tokenHtml); - - $additionalTokensFromProcessor = [ - '{case.case_type_id}' => 'Case Type ID', - '{case.status_id}' => 'Case Status', - '{case.case_type_id:name}' => 'Machine name: Case Type', - '{case.status_id:name}' => 'Machine name: Case Status', - ]; - $expectedTokens = array_merge($this->getCaseTokens(), $additionalTokensFromProcessor); - $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [ 'controller' => __CLASS__, 'smarty' => FALSE, 'schema' => ['caseId'], ]); - $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokenProcessor->listTokens()); + $this->assertEquals(array_merge($this->getCaseTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens()); $tokenProcessor->addRow([ 'caseId' => $this->getCaseID(), ]); @@ -205,7 +196,8 @@ case.custom_1 :' . ' 'smarty' => FALSE, 'schema' => ['contribution_recurId'], ]); - $this->assertEquals(array_merge($this->getContributionRecurTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens()); + $expectedTokens = array_merge($this->getContributionRecurTokens(), $this->getDomainTokens()); + $this->assertEquals(array_diff_key($expectedTokens, $this->getUnadvertisedTokens()), $tokenProcessor->listTokens()); $tokenString = $this->getTokenString(array_keys($this->getContributionRecurTokens())); $tokenProcessor->addMessage('html', $tokenString, 'text/plain'); @@ -225,6 +217,20 @@ case.custom_1 :' . ' '{membership.membership_type_id}' => 'Membership Type ID', '{membership.status_id:name}' => 'Machine name: Status', '{membership.membership_type_id:name}' => 'Machine name: Membership Type', + '{contribution_recur.frequency_unit}' => 'Frequency Unit', + '{contribution_recur.contribution_status_id}' => 'Status', + '{contribution_recur.payment_processor_id}' => 'Payment Processor ID', + '{contribution_recur.financial_type_id}' => 'Financial Type ID', + '{contribution_recur.payment_instrument_id}' => 'Payment Method', + '{contribution_recur.frequency_unit:name}' => 'Machine name: Frequency Unit', + '{contribution_recur.payment_instrument_id:name}' => 'Machine name: Payment Method', + '{contribution_recur.contribution_status_id:name}' => 'Machine name: Status', + '{contribution_recur.payment_processor_id:name}' => 'Machine name: Payment Processor', + '{contribution_recur.financial_type_id:name}' => 'Machine name: Financial Type', + '{participant.status_id:name}' => 'Machine name: Status', + '{participant.role_id:name}' => 'Machine name: Participant Role', + '{participant.status_id}' => 'Status ID', + '{participant.role_id}' => 'Participant Role ID', ]; } @@ -424,9 +430,7 @@ contribution_recur.payment_instrument_id:name :Check ]); $tokens = $tokenProcessor->listTokens(); // Add in custom tokens as token processor supports these. - $expectedTokens['{membership.custom_1}'] = 'Enter text here :: Group with field text'; - // Add in unadvertised tokens - for now they are included. - $expectedTokens = array_merge($expectedTokens, $this->getUnadvertisedTokens()); + $expectedTokens = array_merge($expectedTokens, $this->getTokensAdvertisedByTokenProcessorButNotLegacy()); $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokens); $tokenProcessor->addMessage('html', $tokenString, 'text/plain'); $tokenProcessor->addRow(['membershipId' => $this->getMembershipID()]); @@ -435,6 +439,19 @@ contribution_recur.payment_instrument_id:name :Check } + /** + * Get the advertised tokens the legacy function doesn't know about. + * + * @return string[] + */ + public function getTokensAdvertisedByTokenProcessorButNotLegacy(): array { + return [ + '{membership.custom_1}' => 'Enter text here :: Group with field text', + '{membership.source}' => 'Source', + '{membership.status_override_end_date}' => 'Status Override End Date', + ]; + } + /** * Get declared membership tokens. * @@ -547,7 +564,7 @@ December 21st, 2007 $this->setupParticipantScheduledReminder(); $tokens = CRM_Core_SelectValues::participantTokens(); - $this->assertEquals($this->getParticipantTokens(), $tokens); + $this->assertEquals(array_diff_key($this->getParticipantTokens(), $this->getUnadvertisedTokens()), $tokens); $mut = new CiviMailUtils($this); @@ -699,11 +716,7 @@ December 21st, 2007 $this->setupParticipantScheduledReminder(); $tokens = CRM_Core_SelectValues::eventTokens(); - $unadvertisedTokens = [ - '{event.event_type_id}' => 'Event Type', - '{event.event_type_id:name}' => 'Machine name: Event Type', - ]; - $this->assertEquals(array_merge($this->getEventTokens(), $unadvertisedTokens), $tokens); + $this->assertEquals(array_merge($this->getEventTokens()), $tokens); $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [ 'controller' => __CLASS__, 'smarty' => FALSE, -- 2.25.1