Container.php in GoogleTagManager 8
Namespace
Drupal\google_tag\EntityFile
src/Entity/Container.phpView source
<?php
namespace Drupal\google_tag\Entity;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Condition\ConditionPluginCollection;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Defines the container configuration entity.
*
* @ConfigEntityType(
* id = "google_tag_container",
* label = @Translation("Container"),
* label_singular = @Translation("container"),
* label_plural = @Translation("containers"),
* label_collection = @Translation("Containers"),
* handlers = {
* "storage" = "Drupal\Core\Config\Entity\ConfigEntityStorage",
* "list_builder" = "Drupal\google_tag\ContainerListBuilder",
* "form" = {
* "default" = "Drupal\google_tag\Form\ContainerForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm"
* },
* "access" = "Drupal\google_tag\ContainerAccessControlHandler"
* },
* admin_permission = "administer google tag manager",
* config_prefix = "container",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "status" = "status"
* },
* config_export = {
* "id",
* "label",
* "weight",
* "container_id",
* "data_layer",
* "include_classes",
* "whitelist_classes",
* "blacklist_classes",
* "include_environment",
* "environment_id",
* "environment_token",
* "path_toggle",
* "path_list",
* "role_toggle",
* "role_list",
* "status_toggle",
* "status_list",
* "conditions",
* },
* links = {
* "add-form" = "/admin/config/system/google-tag/add",
* "edit-form" = "/admin/config/system/google-tag/manage/{google_tag_container}",
* "delete-form" = "/admin/config/system/google-tag/manage/{google_tag_container}/delete",
* "enable" = "/admin/config/system/google-tag/manage/{google_tag_container}/enable",
* "disable" = "/admin/config/system/google-tag/manage/{google_tag_container}/disable",
* "collection" = "/admin/config/system/google-tag",
* }
* )
*
* @todo Add a clone operation.
* this may not be an option in above annotation
* "clone-form" = "/admin/structure/google_tag/manage/{google_tag_container}/clone",
*/
class Container extends ConfigEntityBase implements ConfigEntityInterface, EntityWithPluginCollectionInterface {
use StringTranslationTrait;
/**
* The machine name for the configuration entity.
*
* @var string
*/
protected $id;
/**
* The human-readable name of the configuration entity.
*
* @var string
*/
public $label;
/**
* The weight of the configuration entity.
*
* @var int
*/
public $weight = 0;
/**
* The Google Tag Manager container id.
*
* @var string
*/
public $container_id;
/**
* The name of the data layer.
*
* @var string
*/
public $data_layer;
/**
* Whether to add the listed classes to the data layer.
*
* @var bool
*/
public $include_classes;
/**
* The white-listed classes.
*
* @var string
*/
public $whitelist_classes;
/**
* The black-listed classes.
*
* @var string
*/
public $blacklist_classes;
/**
* Whether to include the environment items in the applicable snippets.
*
* @var bool
*/
public $include_environment;
/**
* The environment ID.
*
* @var string
*/
public $environment_id;
/**
* The environment token.
*
* @var string
*/
public $environment_token;
/**
* Whether to include or exclude the listed paths.
*
* @var string
*/
public $path_toggle;
/**
* The listed paths.
*
* @var string
*/
public $path_list;
/**
* Whether to include or exclude the listed roles.
*
* @var string
*/
public $role_toggle;
/**
* The listed roles.
*
* @var array
*/
public $role_list;
/**
* Whether to include or exclude the listed statuses.
*
* @var string
*/
public $status_toggle;
/**
* The listed statuses.
*
* @var string
*/
public $status_list;
/**
* The insertion conditions.
*
* Each item is the configuration array not the condition object.
*
* @var array
*/
protected $conditions = [];
/**
* The insertion condition collection.
*
* @var \Drupal\Core\Condition\ConditionPluginCollection
*/
protected $conditionCollection;
/**
* The condition plugin manager.
*
* @var \Drupal\Core\Executable\ExecutableManagerInterface
*/
protected $conditionPluginManager;
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
$values = array_diff_key($values, array_flip([
'uuid',
'langcode',
]));
if (empty($values)) {
// Initialize entity properties from default container settings.
$config = \Drupal::config('google_tag.settings');
foreach ($config
->get('_default_container') as $key => $value) {
$this->{$key} = $value;
}
}
}
/**
* Returns array of JavaScript snippets.
*
* @return array
* Associative array of snippets keyed by type: script, noscript and
* data_layer.
*/
public function snippets() {
$snippets = [
'script' => $this
->scriptSnippet(),
'noscript' => $this
->noscriptSnippet(),
'data_layer' => $this
->dataLayerSnippet(),
];
// Allow other modules to alter the snippets.
\Drupal::moduleHandler()
->alter('google_tag_snippets', $snippets, $this);
return $snippets;
}
/**
* Returns JavaScript script snippet.
*
* @return array
* The script snippet.
*/
protected function scriptSnippet() {
// Gather data.
$container_id = $this
->variableClean('container_id');
$data_layer = $this
->variableClean('data_layer');
$query = $this
->environmentQuery();
// Build script snippet.
$script = <<<EOS
(function(w,d,s,l,i){
w[l]=w[l]||[];
w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
var f=d.getElementsByTagName(s)[0];
var j=d.createElement(s);
var dl=l!='dataLayer'?'&l='+l:'';
j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+'{<span class="php-variable">$query</span>}';
j.async=true;
f.parentNode.insertBefore(j,f);
})(window,document,'script','{<span class="php-variable">$data_layer</span>}','{<span class="php-variable">$container_id</span>}');
EOS;
return $this
->compactSnippet($script);
}
/**
* Returns JavaScript noscript snippet.
*
* @return array
* The noscript snippet.
*/
protected function noscriptSnippet() {
// Gather data.
$container_id = $this
->variableClean('container_id');
$query = $this
->environmentQuery();
// Build noscript snippet.
$noscript = <<<EOS
<noscript aria-hidden="true"><iframe src="https://www.googletagmanager.com/ns.html?id={<span class="php-variable">$container_id</span>}{<span class="php-variable">$query</span>}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
EOS;
return $this
->compactSnippet($noscript, [
"\n",
]);
}
/**
* Returns JavaScript data layer snippet or adds items to data layer.
*
* @return string|null
* The data layer snippet or NULL.
*/
protected function dataLayerSnippet() {
// Gather data.
$data_layer = $this
->variableClean('data_layer');
$whitelist = $this
->get('whitelist_classes');
$blacklist = $this
->get('blacklist_classes');
$classes = [];
$names = [
'whitelist',
'blacklist',
];
foreach ($names as $name) {
if (empty(${$name})) {
continue;
}
// @see https://www.drupal.org/files/issues/add_options_to-2851405-7.patch
// this suggests to flip order of previous two statements; yet if user
// enters a new line in textarea, then this change does not eliminate the
// empty script item. Need to trim "\n" from ends of string.
${$name} = explode("\n", ${$name});
$classes["gtm.{$name}"] = ${$name};
}
if ($classes) {
// Build data layer snippet.
$classes = json_encode($classes);
$script = <<<EOS
(function(w,l){
w[l]=w[l]||[];
w[l].push({<span class="php-variable">$classes</span>});
})(window,'{<span class="php-variable">$data_layer</span>}');
EOS;
return $this
->compactSnippet($script);
}
}
/**
* Returns a query string with the environment parameters.
*
* @return string
* The query string.
*/
public function environmentQuery() {
if (!$this
->get('include_environment')) {
return '';
}
// Gather data.
$environment_id = $this
->variableClean('environment_id');
$environment_token = $this
->variableClean('environment_token');
// Build query string.
return ">m_auth={$environment_token}>m_preview={$environment_id}>m_cookies_win=x";
}
/**
* Returns a cleansed variable.
*
* @param string $variable
* The variable name.
*
* @return string
* The cleansed variable.
*/
public function variableClean($variable) {
return trim(json_encode($this
->get($variable)), '"');
}
/**
* Returns the compacted snippet.
*
* @param string $snippet
* The JavaScript snippet.
* @param array $search
* The array of strings to replace with blank.
*
* @return string
* The compacted snippet.
*/
protected function compactSnippet($snippet, array $search = [
"\n",
' ',
]) {
$compact = \Drupal::config('google_tag.settings')
->get('compact_snippet');
return $compact ? str_replace($search, '', $snippet) : $snippet;
}
/**
* Determines whether to insert the snippet on the response.
*
* @return bool
* TRUE if the conditions are met; FALSE otherwise.
*/
public function insertSnippet() {
static $satisfied = [];
if (!isset($satisfied[$this->id])) {
$id = $this
->get('container_id');
if (empty($id)) {
// No container ID.
return $satisfied[$this->id] = FALSE;
}
$this
->displayMessage('google_tag container ' . $this->id);
$satisfied[$this->id] = TRUE;
if (!$this
->statusCheck() || !$this
->pathCheck() || !$this
->roleCheck() || !$this
->access('view')) {
// Omit snippet if any condition is not met.
$satisfied[$this->id] = FALSE;
}
// Allow other modules to alter the insertion criteria.
\Drupal::moduleHandler()
->alter('google_tag_insert', $satisfied[$this->id], $this);
$this
->displayMessage('after alter @satisfied', [
'@satisfied' => $satisfied[$this->id],
]);
}
return $satisfied[$this->id];
}
/**
* Determines whether to insert the snippet based on status code settings.
*
* @return bool
* TRUE if the status conditions are met; FALSE otherwise.
*/
protected function statusCheck() {
$toggle = $this
->get('status_toggle');
$statuses = $this
->get('status_list');
if (empty($statuses)) {
$satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED;
}
else {
// Get the HTTP response status.
$request = \Drupal::request();
$status = '200';
if ($exception = $request->attributes
->get('exception')) {
$status = $exception
->getStatusCode();
}
$satisfied = strpos($statuses, (string) $status) !== FALSE;
$satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED ? !$satisfied : $satisfied;
}
$this
->displayMessage('status check @satisfied', [
'@satisfied' => $satisfied,
]);
return $satisfied;
}
/**
* Determines whether to insert the snippet based on the path settings.
*
* @return bool
* TRUE if the path conditions are met; FALSE otherwise.
*/
protected function pathCheck() {
$toggle = $this
->get('path_toggle');
$paths = mb_strtolower($this
->get('path_list'));
if (empty($paths)) {
$satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED;
}
else {
$request = \Drupal::request();
$current_path = \Drupal::service('path.current');
$alias_manager = \Drupal::service('path_alias.manager');
$path_matcher = \Drupal::service('path.matcher');
// @todo Are not some paths case sensitive???
// Compare the lowercase path alias (if any) and internal path.
$path = $current_path
->getPath($request);
$path_alias = mb_strtolower($alias_manager
->getAliasByPath($path));
$satisfied = $path_matcher
->matchPath($path_alias, $paths) || $path != $path_alias && $path_matcher
->matchPath($path, $paths);
$satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED ? !$satisfied : $satisfied;
}
$this
->displayMessage('path check @satisfied', [
'@satisfied' => $satisfied,
]);
return $satisfied;
}
/**
* Determines whether to insert the snippet based on the user role settings.
*
* @return bool
* TRUE if the role conditions are met; FALSE otherwise.
*/
protected function roleCheck() {
$toggle = $this
->get('role_toggle');
$roles = array_filter($this
->get('role_list'));
if (empty($roles)) {
$satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED;
}
else {
$satisfied = FALSE;
// Check user roles against listed roles.
$satisfied = (bool) array_intersect($roles, \Drupal::currentUser()
->getRoles());
$satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED ? !$satisfied : $satisfied;
}
$this
->displayMessage('role check @satisfied', [
'@satisfied' => $satisfied,
]);
return $satisfied;
}
/**
* Displays a message.
*
* @param string $message
* The message to display.
* @param array $args
* (optional) An associative array of replacements.
*/
public function displayMessage($message, array $args = []) {
if (\Drupal::config('google_tag.settings')
->get('debug_output')) {
\Drupal::service('messenger')
->addStatus($this
->t($message, $args), TRUE);
}
}
/**
* Returns the snippet directory path.
*
* @return string
* The snippet directory path.
*/
public function snippetDirectory() {
return \Drupal::config('google_tag.settings')
->get('uri') . "/google_tag/{$this->id()}";
}
/**
* Returns the snippet URI for a snippet type.
*
* @param string $type
* The snippet type.
*
* @return string
* The snippet URI.
*/
public function snippetURI($type) {
return $this
->snippetDirectory() . "/google_tag.{$type}.js";
}
/**
* Returns the snippet cache ID for a snippet type.
*
* @param string $type
* The snippet type.
*
* @return string
* The snippet cache ID.
*/
public function snippetCid($type) {
return "google_tag:{$type}:{$this->id()}";
}
/**
* Returns tag array for the snippet type.
*
* @param string $type
* The snippet type.
* @param int $weight
* The weight of the item.
*
* @return array
* The tag array.
*/
public function fileTag($type, $weight) {
$uri = $this
->snippetURI($type);
$url = file_url_transform_relative(file_create_url($uri));
$query_string = \Drupal::state()
->get('system.css_js_query_string') ?: '0';
$attachment = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#attributes' => [
'src' => $url . '?' . $query_string,
'defer' => TRUE,
],
'#weight' => $weight,
],
"google_tag_{$type}_tag__{$this->id()}",
];
return $attachment;
}
/**
* Returns tag array for the snippet type.
*
* @param string $type
* The snippet type.
* @param int $weight
* The weight of the item.
*
* @return array
* The tag array.
*/
public function inlineTag($type, $weight) {
$contents = $this
->getSnippetContents($type);
$attachment = [
$contents ? [
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => new FormattableMarkup($contents, []),
'#weight' => $weight,
] : [
'#type' => 'ignore_tag',
],
"google_tag_{$type}_tag__{$this->id()}",
];
return $attachment;
}
/**
* Returns tag array for the snippet type.
*
* @param string $type
* (optional) The snippet type.
* @param int $weight
* (optional) The weight of the item.
*
* @return array
* The tag array.
*/
public function noscriptTag($type = 'noscript', $weight = -10) {
// Note: depending on the theme, this may not place the snippet immediately
// after the body tag but should be close and it can be altered.
// @see https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Render!theme.api.php/group/theme_render/8.2.x
// The markup is passed through \Drupal\Component\Utility\Xss::filterAdmin()
// which strips known vectors while allowing a permissive list of HTML tags
// that are not XSS vectors. (e.g., <script> and <style> are not allowed.)
// As markup, core removes the 'style' attribute from the noscript snippet.
// With the inline template type, core does not alter the noscript snippet.
$contents = $this
->getSnippetContents($type);
$attachment = $contents ? [
"google_tag_{$type}_tag__{$this->id()}" => [
'#type' => 'inline_template',
'#template' => $contents,
'#weight' => $weight,
],
] : [];
return $attachment;
}
/**
* Returns the snippet contents for the snippet type.
*
* @param string $type
* The snippet type.
*
* @return string
* The snippet contents.
*/
public function getSnippetContents($type) {
$cache = \Drupal::service('cache.data')
->get($this
->snippetCid($type));
return $cache ? $cache->data : '';
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
return [
'conditions' => $this
->getInsertionConditions(),
];
}
/**
* Returns an array of configuration arrays keyed by insertion condition.
*
* @return array
* An array of condition configuration keyed by the condition ID.
*/
public function getInsertionConfiguration() {
return $this
->getInsertionConditions()
->getConfiguration();
}
/**
* Returns an insertion condition for this container.
*
* @param string $instance_id
* The condition plugin instance ID.
*
* @return \Drupal\Core\Condition\ConditionInterface
* A condition plugin.
*/
public function getInsertionCondition($instance_id) {
return $this
->getInsertionConditions()
->get($instance_id);
}
/**
* Sets the configuration for an insertion condition.
*
* @param string $instance_id
* The condition instance ID.
* @param array $configuration
* The condition configuration.
*
* @return $this
*
* @todo Does this need to set a persistent property?
*/
public function setInsertionCondition($instance_id, array $configuration) {
$conditions = $this
->getInsertionConditions();
if (!$conditions
->has($instance_id)) {
$configuration['id'] = $instance_id;
$conditions
->addInstanceId($instance_id, $configuration);
}
else {
$conditions
->setInstanceConfiguration($instance_id, $configuration);
}
return $this;
}
/**
* Returns the set of insertion conditions for this container.
*
* @return \Drupal\Core\Condition\ConditionPluginCollection
* A collection of configured condition plugins.
*/
public function getInsertionConditions() {
if (!isset($this->conditionCollection)) {
$this->conditionCollection = new ConditionPluginCollection($this
->conditionPluginManager(), $this
->get('conditions'));
}
return $this->conditionCollection;
}
/**
* Gets the condition plugin manager.
*
* @return \Drupal\Core\Executable\ExecutableManagerInterface
* The condition plugin manager.
*/
protected function conditionPluginManager() {
if (!isset($this->conditionPluginManager)) {
$this->conditionPluginManager = \Drupal::service('plugin.manager.condition');
}
return $this->conditionPluginManager;
}
}