diff --git a/composer.json b/composer.json
index e05681a795492fcb5929bdb0cbc3f1d6127e010d..afa19257483b6436be7389465b1cec6f41bef078 100644
--- a/composer.json
+++ b/composer.json
@@ -129,6 +129,7 @@
         "drupal/focal_point": "1.0-beta6",
         "drupal/geolocation": "1.10",
         "drupal/google_analytics": "2.2",
+        "drupal/google_tag": "^1.1",
         "drupal/honeypot": "^1.28",
         "drupal/image_popup": "1.1",
         "drupal/inline_entity_form": "1.0-rc1",
@@ -188,7 +189,8 @@
         "gdsmith/jquery.easing": "1.4.1",
         "oomphinc/composer-installers-extender": "^1.1",
         "pantheon-systems/quicksilver-pushback": "~1",
-        "rvtraveller/qs-composer-installer": "^1.1"
+        "rvtraveller/qs-composer-installer": "^1.1",
+        "zaporylie/composer-drupal-optimizations": "^1.0"
     },
     "require-dev": {
     },
diff --git a/composer.lock b/composer.lock
index 3179968f35b57505320c44449a3937a8fd7a0d20..9237313a23f4525a142e0542675306b98a93177f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1,10 +1,10 @@
 {
     "_readme": [
         "This file locks the dependencies of your project to a known state",
-        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "f8e1f9c6a81412f0164d33bfbc631031",
+    "content-hash": "562988aa02aa13014da7f5532c594855",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -173,9 +173,7 @@
             "version": "4.8.0",
             "dist": {
                 "type": "zip",
-                "url": "https://download.ckeditor.com/indentblock/releases/indentblock_4.8.0.zip",
-                "reference": null,
-                "shasum": null
+                "url": "https://download.ckeditor.com/indentblock/releases/indentblock_4.8.0.zip"
             },
             "require": {
                 "composer/installers": "~1.0"
@@ -636,9 +634,7 @@
             "version": "4.2.0",
             "dist": {
                 "type": "zip",
-                "url": "https://github.com/desandro/masonry/archive/v4.2.0.zip",
-                "reference": null,
-                "shasum": null
+                "url": "https://github.com/desandro/masonry/archive/v4.2.0.zip"
             },
             "type": "drupal-library"
         },
@@ -4324,6 +4320,53 @@
                 "issues": "https://www.drupal.org/project/issues/google_analytics"
             }
         },
+        {
+            "name": "drupal/google_tag",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://git.drupalcode.org/project/google_tag.git",
+                "reference": "8.x-1.1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://ftp.drupal.org/files/projects/google_tag-8.x-1.1.zip",
+                "reference": "8.x-1.1",
+                "shasum": "69c434d465ccf7c180c39c3bfba7e1ae34aaaad7"
+            },
+            "require": {
+                "drupal/core": "~8.0"
+            },
+            "type": "drupal-module",
+            "extra": {
+                "branch-alias": {
+                    "dev-1.x": "1.x-dev"
+                },
+                "drupal": {
+                    "version": "8.x-1.1",
+                    "datestamp": "1534988884",
+                    "security-coverage": {
+                        "status": "covered",
+                        "message": "Covered by Drupal's security advisory policy"
+                    }
+                }
+            },
+            "notification-url": "https://packages.drupal.org/8/downloads",
+            "license": [
+                "GPL 2.0"
+            ],
+            "authors": [
+                {
+                    "name": "solotandem",
+                    "homepage": "https://www.drupal.org/user/240748"
+                }
+            ],
+            "description": "Allows your website analytics to be managed using Google Tag Manager.",
+            "homepage": "https://www.drupal.org/project/google_tag",
+            "support": {
+                "source": "https://git.drupalcode.org/project/google_tag"
+            }
+        },
         {
             "name": "drupal/honeypot",
             "version": "1.28.0",
@@ -5196,8 +5239,7 @@
             "homepage": "https://www.drupal.org/project/migrate_devel",
             "support": {
                 "source": "http://cgit.drupalcode.org/migrate_devel"
-            },
-            "time": "2017-06-25T23:46:13+00:00"
+            }
         },
         {
             "name": "drupal/migrate_plus",
@@ -11064,6 +11106,49 @@
             ],
             "time": "2015-02-11T11:06:42+00:00"
         },
+        {
+            "name": "zaporylie/composer-drupal-optimizations",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/zaporylie/composer-drupal-optimizations.git",
+                "reference": "173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/zaporylie/composer-drupal-optimizations/zipball/173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8",
+                "reference": "173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^1.1"
+            },
+            "require-dev": {
+                "composer/composer": "^1.6",
+                "phpunit/phpunit": "^6"
+            },
+            "type": "composer-plugin",
+            "extra": {
+                "class": "zaporylie\\ComposerDrupalOptimizations\\Plugin"
+            },
+            "autoload": {
+                "psr-4": {
+                    "zaporylie\\ComposerDrupalOptimizations\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "GPL-2.0-or-later"
+            ],
+            "authors": [
+                {
+                    "name": "Jakub Piasecki",
+                    "email": "jakub@piaseccy.pl"
+                }
+            ],
+            "description": "Composer plugin to improve composer performance for Drupal projects",
+            "time": "2019-02-20T10:00:17+00:00"
+        },
         {
             "name": "zendframework/zend-diactoros",
             "version": "1.8.6",
diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php
index dc02dfb114fb6af2eacf89407a529c37ab8e7eb8..fce8549f0781bafdc7da2301b84d048286757445 100644
--- a/vendor/composer/ClassLoader.php
+++ b/vendor/composer/ClassLoader.php
@@ -279,7 +279,7 @@ public function isClassMapAuthoritative()
      */
     public function setApcuPrefix($apcuPrefix)
     {
-        $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
     }
 
     /**
@@ -377,7 +377,7 @@ private function findFileWithExtension($class, $ext)
             $subPath = $class;
             while (false !== $lastPos = strrpos($subPath, '\\')) {
                 $subPath = substr($subPath, 0, $lastPos);
-                $search = $subPath.'\\';
+                $search = $subPath . '\\';
                 if (isset($this->prefixDirsPsr4[$search])) {
                     $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                     foreach ($this->prefixDirsPsr4[$search] as $dir) {
diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php
index e08058ef542d9b8123024962c0516331576b343f..ac031cc8af871c45bee3084d6f45dddef57a390f 100644
--- a/vendor/composer/autoload_classmap.php
+++ b/vendor/composer/autoload_classmap.php
@@ -1953,8 +1953,6 @@
     'Drupal\\Core\\Language\\LanguageManager' => $baseDir . '/web/core/lib/Drupal/Core/Language/LanguageManager.php',
     'Drupal\\Core\\Language\\LanguageManagerInterface' => $baseDir . '/web/core/lib/Drupal/Core/Language/LanguageManagerInterface.php',
     'Drupal\\Core\\Layout\\Annotation\\Layout' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Annotation/Layout.php',
-    'Drupal\\Core\\Layout\\Icon\\IconBuilderInterface' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php',
-    'Drupal\\Core\\Layout\\Icon\\SvgIconBuilder' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Icon/SvgIconBuilder.php',
     'Drupal\\Core\\Layout\\LayoutDefault' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutDefault.php',
     'Drupal\\Core\\Layout\\LayoutDefinition' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutDefinition.php',
     'Drupal\\Core\\Layout\\LayoutInterface' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutInterface.php',
@@ -5163,4 +5161,7 @@
     'cweagans\\Composer\\Patches' => $vendorDir . '/cweagans/composer-patches/src/Patches.php',
     'rvtraveller\\QuicksilverComposerInstaller\\QuicksilverComposerInstaller' => $vendorDir . '/rvtraveller/qs-composer-installer/src/QuicksilverComposerInstaller.php',
     'rvtraveller\\QuicksilverComposerInstaller\\QuicksilverComposerInstallerPlugin' => $vendorDir . '/rvtraveller/qs-composer-installer/src/QuicksilverComposerInstallerPlugin.php',
+    'zaporylie\\ComposerDrupalOptimizations\\Cache' => $vendorDir . '/zaporylie/composer-drupal-optimizations/src/Cache.php',
+    'zaporylie\\ComposerDrupalOptimizations\\Plugin' => $vendorDir . '/zaporylie/composer-drupal-optimizations/src/Plugin.php',
+    'zaporylie\\ComposerDrupalOptimizations\\TruncatedComposerRepository' => $vendorDir . '/zaporylie/composer-drupal-optimizations/src/TruncatedComposerRepository.php',
 );
diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php
index 1c2be53cd0566e897ec05aa6986cfe75077554a2..0b8bd6f2a318adb195045c01ecab20ad66d6bedf 100644
--- a/vendor/composer/autoload_psr4.php
+++ b/vendor/composer/autoload_psr4.php
@@ -6,6 +6,7 @@
 $baseDir = dirname($vendorDir);
 
 return array(
+    'zaporylie\\ComposerDrupalOptimizations\\' => array($vendorDir . '/zaporylie/composer-drupal-optimizations/src'),
     'rvtraveller\\QuicksilverComposerInstaller\\' => array($vendorDir . '/rvtraveller/qs-composer-installer/src'),
     'cweagans\\Composer\\' => array($vendorDir . '/cweagans/composer-patches/src'),
     'Zend\\Stdlib\\' => array($vendorDir . '/zendframework/zend-stdlib/src'),
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
index d4a723155e76d04c9fd434028fa75ec32a7b7dec..2dea499e2779d70c0228e6d8b513e0f547808ec2 100644
--- a/vendor/composer/autoload_static.php
+++ b/vendor/composer/autoload_static.php
@@ -32,6 +32,10 @@ class ComposerStaticInit5c689ffcd54b9e495ed983fdce09b530
     );
 
     public static $prefixLengthsPsr4 = array (
+        'z' => 
+        array (
+            'zaporylie\\ComposerDrupalOptimizations\\' => 38,
+        ),
         'r' => 
         array (
             'rvtraveller\\QuicksilverComposerInstaller\\' => 41,
@@ -162,6 +166,10 @@ class ComposerStaticInit5c689ffcd54b9e495ed983fdce09b530
     );
 
     public static $prefixDirsPsr4 = array (
+        'zaporylie\\ComposerDrupalOptimizations\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/zaporylie/composer-drupal-optimizations/src',
+        ),
         'rvtraveller\\QuicksilverComposerInstaller\\' => 
         array (
             0 => __DIR__ . '/..' . '/rvtraveller/qs-composer-installer/src',
@@ -2497,8 +2505,6 @@ class ComposerStaticInit5c689ffcd54b9e495ed983fdce09b530
         'Drupal\\Core\\Language\\LanguageManager' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Language/LanguageManager.php',
         'Drupal\\Core\\Language\\LanguageManagerInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Language/LanguageManagerInterface.php',
         'Drupal\\Core\\Layout\\Annotation\\Layout' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Annotation/Layout.php',
-        'Drupal\\Core\\Layout\\Icon\\IconBuilderInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php',
-        'Drupal\\Core\\Layout\\Icon\\SvgIconBuilder' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Icon/SvgIconBuilder.php',
         'Drupal\\Core\\Layout\\LayoutDefault' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutDefault.php',
         'Drupal\\Core\\Layout\\LayoutDefinition' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutDefinition.php',
         'Drupal\\Core\\Layout\\LayoutInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutInterface.php',
@@ -5707,6 +5713,9 @@ class ComposerStaticInit5c689ffcd54b9e495ed983fdce09b530
         'cweagans\\Composer\\Patches' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patches.php',
         'rvtraveller\\QuicksilverComposerInstaller\\QuicksilverComposerInstaller' => __DIR__ . '/..' . '/rvtraveller/qs-composer-installer/src/QuicksilverComposerInstaller.php',
         'rvtraveller\\QuicksilverComposerInstaller\\QuicksilverComposerInstallerPlugin' => __DIR__ . '/..' . '/rvtraveller/qs-composer-installer/src/QuicksilverComposerInstallerPlugin.php',
+        'zaporylie\\ComposerDrupalOptimizations\\Cache' => __DIR__ . '/..' . '/zaporylie/composer-drupal-optimizations/src/Cache.php',
+        'zaporylie\\ComposerDrupalOptimizations\\Plugin' => __DIR__ . '/..' . '/zaporylie/composer-drupal-optimizations/src/Plugin.php',
+        'zaporylie\\ComposerDrupalOptimizations\\TruncatedComposerRepository' => __DIR__ . '/..' . '/zaporylie/composer-drupal-optimizations/src/TruncatedComposerRepository.php',
     );
 
     public static function getInitializer(ClassLoader $loader)
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 030b9e5f1b29e2c540b913c5dfdbc35bd968b782..575d68851d53135cf0fdf6940870141bda4824e5 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -175,9 +175,7 @@
         "version_normalized": "4.8.0.0",
         "dist": {
             "type": "zip",
-            "url": "https://download.ckeditor.com/indentblock/releases/indentblock_4.8.0.zip",
-            "reference": null,
-            "shasum": null
+            "url": "https://download.ckeditor.com/indentblock/releases/indentblock_4.8.0.zip"
         },
         "require": {
             "composer/installers": "~1.0"
@@ -656,9 +654,7 @@
         "version_normalized": "4.2.0.0",
         "dist": {
             "type": "zip",
-            "url": "https://github.com/desandro/masonry/archive/v4.2.0.zip",
-            "reference": null,
-            "shasum": null
+            "url": "https://github.com/desandro/masonry/archive/v4.2.0.zip"
         },
         "type": "drupal-library",
         "installation-source": "dist"
@@ -4459,6 +4455,55 @@
             "issues": "https://www.drupal.org/project/issues/google_analytics"
         }
     },
+    {
+        "name": "drupal/google_tag",
+        "version": "1.1.0",
+        "version_normalized": "1.1.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://git.drupalcode.org/project/google_tag.git",
+            "reference": "8.x-1.1"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://ftp.drupal.org/files/projects/google_tag-8.x-1.1.zip",
+            "reference": "8.x-1.1",
+            "shasum": "69c434d465ccf7c180c39c3bfba7e1ae34aaaad7"
+        },
+        "require": {
+            "drupal/core": "~8.0"
+        },
+        "type": "drupal-module",
+        "extra": {
+            "branch-alias": {
+                "dev-1.x": "1.x-dev"
+            },
+            "drupal": {
+                "version": "8.x-1.1",
+                "datestamp": "1534988884",
+                "security-coverage": {
+                    "status": "covered",
+                    "message": "Covered by Drupal's security advisory policy"
+                }
+            }
+        },
+        "installation-source": "dist",
+        "notification-url": "https://packages.drupal.org/8/downloads",
+        "license": [
+            "GPL 2.0"
+        ],
+        "authors": [
+            {
+                "name": "solotandem",
+                "homepage": "https://www.drupal.org/user/240748"
+            }
+        ],
+        "description": "Allows your website analytics to be managed using Google Tag Manager.",
+        "homepage": "https://www.drupal.org/project/google_tag",
+        "support": {
+            "source": "https://git.drupalcode.org/project/google_tag"
+        }
+    },
     {
         "name": "drupal/honeypot",
         "version": "1.28.0",
@@ -11433,6 +11478,51 @@
             "apr1"
         ]
     },
+    {
+        "name": "zaporylie/composer-drupal-optimizations",
+        "version": "1.1.0",
+        "version_normalized": "1.1.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/zaporylie/composer-drupal-optimizations.git",
+            "reference": "173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/zaporylie/composer-drupal-optimizations/zipball/173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8",
+            "reference": "173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8",
+            "shasum": ""
+        },
+        "require": {
+            "composer-plugin-api": "^1.1"
+        },
+        "require-dev": {
+            "composer/composer": "^1.6",
+            "phpunit/phpunit": "^6"
+        },
+        "time": "2019-02-20T10:00:17+00:00",
+        "type": "composer-plugin",
+        "extra": {
+            "class": "zaporylie\\ComposerDrupalOptimizations\\Plugin"
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "zaporylie\\ComposerDrupalOptimizations\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "GPL-2.0-or-later"
+        ],
+        "authors": [
+            {
+                "name": "Jakub Piasecki",
+                "email": "jakub@piaseccy.pl"
+            }
+        ],
+        "description": "Composer plugin to improve composer performance for Drupal projects"
+    },
     {
         "name": "zendframework/zend-diactoros",
         "version": "1.8.6",
diff --git a/vendor/zaporylie/composer-drupal-optimizations/.gitignore b/vendor/zaporylie/composer-drupal-optimizations/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..6de133df8699783b224eeec70a2d8ef924399b01
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/.gitignore
@@ -0,0 +1,4 @@
+/vendor/
+composer.lock
+.phpunit.result.cache
+/test
diff --git a/vendor/zaporylie/composer-drupal-optimizations/.travis.yml b/vendor/zaporylie/composer-drupal-optimizations/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..addd8891069a3647d046d07943c11a0cb9e3cc8b
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/.travis.yml
@@ -0,0 +1,47 @@
+language: php
+dist: trusty
+sudo: false
+
+php:
+  - 5.6
+  - 7.0
+  - 7.1
+  - 7.2
+  - 7.3
+
+env:
+  matrix:
+    - RELEASE=stable COMPOSER_CHANNEL=stable
+    - RELEASE=dev COMPOSER_CHANNEL=stable
+    - RELEASE=stable COMPOSER_CHANNEL=snapshot
+
+matrix:
+  exclude:
+    - php: 5.6
+      env: RELEASE=dev COMPOSER_CHANNEL=stable
+    - php: 5.6
+      env: RELEASE=stable COMPOSER_CHANNEL=snapshot
+
+before_install:
+  - if [[ $TRAVIS_PHP_VERSION = 5.6 ]]; then export COMPOSER_MEMORY_LIMIT=-1; fi;
+  - composer --verbose self-update --$COMPOSER_CHANNEL
+  - composer --version
+
+install:
+  - composer --verbose validate
+
+script:
+  # Run automated tests.
+  - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then composer install && ./vendor/bin/phpunit; fi
+  - cd .. && composer create-project drupal-composer/drupal-project:8.x-dev drupal-project --stability dev --no-interaction
+  - ls -lah . && cd drupal-project
+  # zaporylie/composer-drupal-optimizations has been added to drupal-composer/drupal-project and now must be removed
+  # in order to measure the performance change.
+  - composer remove zaporylie/composer-drupal-optimizations
+  - if [[ $RELEASE = dev ]]; then composer --verbose require --no-update --dev webflo/drupal-core-require-dev:8.6.x-dev; fi;
+  - if [[ $RELEASE = dev ]]; then composer --verbose require --no-update drupal/core:8.6.x-dev; fi;
+  - if [[ $RELEASE = dev ]]; then composer --verbose update; fi;
+  # Check the performance.
+  - composer update nothing --profile
+  - composer config repositories.local path "../composer-drupal-optimizations" && composer require zaporylie/composer-drupal-optimizations:@dev
+  - composer update nothing --profile
diff --git a/vendor/zaporylie/composer-drupal-optimizations/README.md b/vendor/zaporylie/composer-drupal-optimizations/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..182be1a5d9c8d81660a903ea093f977d9bf0deb8
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/README.md
@@ -0,0 +1,68 @@
+Optimize Composer for Drupal projects
+====
+[![Build Status](https://travis-ci.org/zaporylie/composer-drupal-optimizations.svg?branch=master)](https://travis-ci.org/zaporylie/composer-drupal-optimizations)
+![Packagist](https://img.shields.io/packagist/v/zaporylie/composer-drupal-optimizations.svg)
+
+
+# About
+
+This composer-plugin contains set of improvements that makes running heavy duty composer commands (i.e. `composer update` or `composer require`) much faster.
+
+# Installation
+
+```bash
+composer require zaporylie/composer-drupal-optimizations:^1.0
+```
+
+No configuration required 🎊
+
+# Optimizations
+
+- Reduce memory usage and CPU usage by removing legacy symfony tags
+
+# Benchmark
+
+Following numbers are for clean https://github.com/drupal-composer/drupal-project/ without and with this plugin.
+
+Before:
+
+```
+Memory usage: 323.19MB (peak: 1121.09MB), time: 13.68s
+```
+
+After:
+
+```
+Memory usage: 238.66MB (peak: 297.17MB), time: 4.84s
+```
+
+> php 7.2, macOS High Sierra, i7, 16GB RAM
+
+# Configuration
+
+If no configuration is provided this package will provide sensible defaults based on the content of project's composer.json
+file. Default configuration should cover 99% of the cases. However, in case you want to manually specify the tags
+that should be filtered out you are welcome to use the `extra` section:
+
+```json
+{
+  "extra": {
+    "composer-drupal-optimizations": {
+      "require": {
+        "symfony/symfony": ">3.4"
+      }
+    }
+  }
+}
+```
+
+***Recommendation note:***
+Use defaults (skip config above) if possible - this package will be maintained throughout the Drupal's lifecycle in order
+to optimize legacy constraints in parallel with Drupal's requirements.
+
+All you have to do is to make sure your drupal core constraint is set to `drupal/core: ^8.5` or above.
+
+# Credits
+
+- Symfony community - idea and development; Special thanks to @nicolas-grekas
+- Jakub Piasecki - port and maintenance
diff --git a/vendor/zaporylie/composer-drupal-optimizations/composer.json b/vendor/zaporylie/composer-drupal-optimizations/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..415f844a3f9b2e030734c14b74e787373cc2eea8
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/composer.json
@@ -0,0 +1,27 @@
+{
+    "name": "zaporylie/composer-drupal-optimizations",
+    "description": "Composer plugin to improve composer performance for Drupal projects",
+    "type": "composer-plugin",
+    "license": "GPL-2.0-or-later",
+    "authors": [
+        {
+            "name": "Jakub Piasecki",
+            "email": "jakub@piaseccy.pl"
+        }
+    ],
+    "require": {
+        "composer-plugin-api": "^1.1"
+    },
+    "require-dev": {
+        "composer/composer": "^1.6",
+        "phpunit/phpunit": "^6"
+    },
+    "autoload": {
+        "psr-4": {
+            "zaporylie\\ComposerDrupalOptimizations\\": "src/"
+        }
+    },
+    "extra": {
+        "class": "zaporylie\\ComposerDrupalOptimizations\\Plugin"
+    }
+}
diff --git a/vendor/zaporylie/composer-drupal-optimizations/phpunit.xml.dist b/vendor/zaporylie/composer-drupal-optimizations/phpunit.xml.dist
new file mode 100644
index 0000000000000000000000000000000000000000..179ca29b14e1c05dd96fb283427d7452ec5d92e7
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/phpunit.xml.dist
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit backupGlobals="true"
+         backupStaticAttributes="false"
+         colors="true"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         processIsolation="false"
+         stopOnFailure="false"
+>
+    <testsuites>
+        <testsuite name="Test Suite">
+            <directory>./tests</directory>
+        </testsuite>
+    </testsuites>
+
+    <php>
+        <ini name="error_reporting" value="-1" />
+    </php>
+
+    <filter>
+        <whitelist>
+            <directory>./src/</directory>
+        </whitelist>
+    </filter>
+</phpunit>
diff --git a/vendor/zaporylie/composer-drupal-optimizations/src/Cache.php b/vendor/zaporylie/composer-drupal-optimizations/src/Cache.php
new file mode 100644
index 0000000000000000000000000000000000000000..815b43286ea7d5574facbe1b074183b1df080d5c
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/src/Cache.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace zaporylie\ComposerDrupalOptimizations;
+
+use Composer\Cache as BaseCache;
+use Composer\Semver\Constraint\Constraint;
+use Composer\Semver\VersionParser;
+
+/**
+ * Class Cache
+ * @package zaporylie\ComposerDrupalOptimizations
+ */
+class Cache extends BaseCache
+{
+
+    /**
+     * @var array
+     */
+    protected $packages = [];
+
+    /**
+     * @var \Composer\Semver\VersionParser
+     */
+    protected $versionParser;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function read($file)
+    {
+        $content = $this->readFile($file);
+        if (!\is_array($data = json_decode($content, true))) {
+            return $content;
+        }
+        foreach (array_keys($this->packages) as $key) {
+            list($provider, ) = explode('/', $key, 2);
+            if (0 === strpos($file, "provider-$provider\$")) {
+                $data = $this->removeLegacyTags($data);
+                break;
+            }
+        }
+        return json_encode($data);
+    }
+
+    protected function readFile($file)
+    {
+        return parent::read($file);
+    }
+
+    /**
+     * Removes legacy tags from $data.
+     *
+     * @param array $data
+     * @return array
+     */
+    public function removeLegacyTags(array $data)
+    {
+        // Skip if the list of packages is empty.
+        if (!$this->packages || empty($data['packages'])) {
+            return $data;
+        }
+
+        // Skip if none of the packages was found.
+        if (!array_diff_key($data['packages'], $this->packages)) {
+            return $data;
+        }
+
+        foreach ($this->packages as $packageName => $packageVersionConstraint) {
+            if (!isset($data['packages'][$packageName])) {
+                continue;
+            }
+            $packages = [];
+            $specificPackage = $data['packages'][$packageName];
+            foreach ($specificPackage as $version => $composerJson) {
+                if ('dev-master' === $version) {
+                    $normalizedVersion = $this->versionParser->normalize($composerJson['extra']['branch-alias']['dev-master']);
+                } else {
+                    $normalizedVersion = $composerJson['version_normalized'];
+                }
+                $packageConstraint = $this->versionParser->parseConstraints($packageVersionConstraint);
+                $versionConstraint = new Constraint('==', $normalizedVersion);
+                if ($packageConstraint->matches($versionConstraint)) {
+                    $packages += isset($composerJson['replace']) ? $composerJson['replace'] : [];
+                } else {
+                    unset($specificPackage[$version]);
+                }
+            }
+
+            // Ignore requirements: their intersection with versions of the package gives empty result.
+            if (!$specificPackage) {
+                continue;
+            }
+            $data['packages'][$packageName] = $specificPackage;
+
+            unset($specificPackage['dev-master']);
+            foreach ($data['packages'] as $name => $versions) {
+                if (!isset($packages[$name]) || null === $devMasterAlias = (isset($versions['dev-master']['extra']['branch-alias']['dev-master']) ? $versions['dev-master']['extra']['branch-alias']['dev-master'] : null)) {
+                    continue;
+                }
+                $devMaster = $versions['dev-master'];
+                $versions = array_intersect_key($versions, $specificPackage);
+                $packageConstraint = $this->versionParser->parseConstraints($packageVersionConstraint);
+                $versionConstraint = new Constraint('==', $this->versionParser->normalize($devMasterAlias));
+                if ($packageConstraint->matches($versionConstraint)) {
+                    $versions['dev-master'] = $devMaster;
+                }
+                if ($versions) {
+                    $data['packages'][$name] = $versions;
+                }
+            }
+        }
+        return $data;
+
+    }
+
+    /**
+     * @param array $packages
+     *
+     * @return $this
+     */
+    public function setRequiredVersionConstraints(array $packages) {
+        $this->versionParser = new VersionParser();
+        $this->packages = $packages;
+        return $this;
+    }
+
+}
diff --git a/vendor/zaporylie/composer-drupal-optimizations/src/Plugin.php b/vendor/zaporylie/composer-drupal-optimizations/src/Plugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..7069aac49cfdbfbec41dc42b4c3b7af619bfb297
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/src/Plugin.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace zaporylie\ComposerDrupalOptimizations;
+
+use Composer\Composer;
+use Composer\Factory;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+use Composer\Repository\RepositoryFactory;
+use Composer\Repository\RepositoryManager;
+use Composer\Semver\Constraint\Constraint;
+use Composer\Semver\Constraint\ConstraintInterface;
+
+class Plugin implements PluginInterface
+{
+
+    public function activate(Composer $composer, IOInterface $io)
+    {
+        // Set default version constraints based on the composer requirements.
+        $extra = $composer->getPackage()->getExtra();
+        $packages = $composer->getPackage()->getRequires();
+        if (!isset($extra['composer-drupal-optimizations']['require']) && isset($packages['drupal/core'])) {
+            $coreConstraint = $packages['drupal/core']->getConstraint();
+            $extra['composer-drupal-optimizations']['require'] = static::getDefaultRequire($coreConstraint);
+            if (!empty($extra['composer-drupal-optimizations']['require']) && $io->isVerbose()) {
+                $io->write('Required tags were not explicitly set so the zaporylie/composer-drupal-optimizations set default based on project\'s composer.json content.');
+            }
+        }
+        if (!empty($extra['composer-drupal-optimizations']['require']) && $io->isVerbose()) {
+            foreach ($extra['composer-drupal-optimizations']['require'] as $package => $version) {
+                $io->write(sprintf('extra.commerce-drupal-optimizations.require.%s: \'%s\'', $package, $version));
+            }
+        }
+
+        $rfs = Factory::createRemoteFilesystem($io, $composer->getConfig());
+        $manager = RepositoryFactory::manager($io, $composer->getConfig(), $composer->getEventDispatcher(), $rfs);
+        $setRepositories = \Closure::bind(function (RepositoryManager $manager) use ($extra) {
+            $manager->repositoryClasses = $this->repositoryClasses;
+            $manager->setRepositoryClass('composer', TruncatedComposerRepository::class);
+            $manager->repositories = $this->repositories;
+            $i = 0;
+            foreach (RepositoryFactory::defaultRepos(null, $this->config, $manager) as $repo) {
+                $manager->repositories[$i++] = $repo;
+                if ($repo instanceof TruncatedComposerRepository && !empty($extra['composer-drupal-optimizations']['require'])) {
+                  $repo->setRequiredVersionConstraints($extra['composer-drupal-optimizations']['require']);
+                }
+            }
+            $manager->setLocalRepository($this->getLocalRepository());
+        }, $composer->getRepositoryManager(), RepositoryManager::class);
+        $setRepositories($manager);
+        $composer->setRepositoryManager($manager);
+    }
+
+    /**
+     * Negotiates default require constraint and package for given drupal/core.
+     *
+     * @param \Composer\Semver\Constraint\ConstraintInterface
+     *
+     * @return array
+     */
+    static public function getDefaultRequire(ConstraintInterface $coreConstraint)
+    {
+        if ((new Constraint('>=', '8.5.0'))->matches($coreConstraint)
+          && !(new Constraint('<', '8.5.0'))->matches($coreConstraint)) {
+            return [
+              'symfony/symfony' => '>3.4',
+            ];
+        }
+        return [];
+    }
+}
diff --git a/vendor/zaporylie/composer-drupal-optimizations/src/TruncatedComposerRepository.php b/vendor/zaporylie/composer-drupal-optimizations/src/TruncatedComposerRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..23f8ecf08ba278eb906c226f062952da3e68a5fe
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/src/TruncatedComposerRepository.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace zaporylie\ComposerDrupalOptimizations;
+
+use Composer\Config;
+use Composer\EventDispatcher\EventDispatcher;
+use Composer\IO\IOInterface;
+use Composer\Repository\ComposerRepository as BaseComposerRepository;
+use Composer\Util\RemoteFilesystem;
+
+class TruncatedComposerRepository extends BaseComposerRepository
+{
+    public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
+    {
+        parent::__construct($repoConfig, $io, $config, $eventDispatcher, $rfs);
+        $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$');
+    }
+    protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false)
+    {
+        $data = parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime);
+        return \is_array($data) ? $this->cache->removeLegacyTags($data) : $data;
+    }
+    public function setRequiredVersionConstraints(array $packages) {
+        $this->cache->setRequiredVersionConstraints($packages);
+    }
+}
diff --git a/vendor/zaporylie/composer-drupal-optimizations/tests/CacheTest.php b/vendor/zaporylie/composer-drupal-optimizations/tests/CacheTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2eb471fa9b7ddacbdcc02226e798abcbdb179079
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/tests/CacheTest.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace zaporylie\ComposerDrupalOptimizations\Tests;
+
+use Composer\IO\NullIO;
+use PHPUnit\Framework\TestCase;
+use zaporylie\ComposerDrupalOptimizations\Cache;
+
+class CacheTest extends TestCase
+{
+
+    /**
+     * Tests if data is not malformed and only valid array from valid provider
+     * is processed.
+     *
+     * @param $provided
+     * @param $expected
+     *
+     * @dataProvider provideReadTest
+     * @covers \zaporylie\ComposerDrupalOptimizations\Cache::read
+     */
+    public function testRead($provided, $expected)
+    {
+        $cache = new class(new NullIO(), 'test') extends Cache {
+            protected $packages = [
+              'vendor/package' => 'version',
+            ];
+            protected function readFile($file)
+            {
+                // Remove provider - used only for tests.
+                if (0 === strpos($file, 'provider-vendor$')) {
+                    $file = substr($file, 16);
+                }
+                return $file;
+            }
+            public function removeLegacyTags(array $data)
+            {
+                $data['status'] = 'ok';
+                return $data;
+            }
+        };
+        static::assertEquals($expected, $cache->read($provided));
+    }
+
+    /**
+     * Test data.
+     */
+    function provideReadTest()
+    {
+        yield 'normal' => ['{"a":"b"}', '{"a":"b"}'];
+        yield 'falsy' => ['{"a":"b"', '{"a":"b"'];
+        yield 'empty' => ['', ''];
+        yield 'matching-incorrect-provider' => ['{"provider":"vendor"}', '{"provider":"vendor"}'];
+        yield 'matching' => ['provider-vendor${"provider":"vendor"}', '{"provider":"vendor","status":"ok"}'];
+    }
+
+    /**
+     * @dataProvider provideRemoveLegacyTags
+     * @covers \zaporylie\ComposerDrupalOptimizations\Cache::removeLegacyTags
+     */
+    public function testRemoveLegacyTags(array $expected, array $packages, array $versionConstraints)
+    {
+        /** @var Cache $cache */
+        $cache = (new \ReflectionClass(Cache::class))->newInstanceWithoutConstructor();
+        $cache->setRequiredVersionConstraints($versionConstraints);
+        $this->assertSame(['packages' => $expected], $cache->removeLegacyTags(['packages' => $packages]));
+    }
+
+    /**
+     * Test data.
+     */
+    public function provideRemoveLegacyTags()
+    {
+        yield 'no-symfony/symfony' => [[123], [123], ['symfony/symfony' => '~1']];
+        $branchAlias = function ($versionAlias) {
+            return [
+              'extra' => [
+                'branch-alias' => [
+                  'dev-master' => $versionAlias.'-dev',
+                ],
+              ],
+            ];
+        };
+        $packages = [
+          'foo/unrelated' => [
+            '1.0.0' => [],
+          ],
+          'symfony/symfony' => [
+            '3.3.0' => [
+              'version_normalized' => '3.3.0.0',
+              'replace' => ['symfony/foo' => 'self.version'],
+            ],
+            '3.4.0' => [
+              'version_normalized' => '3.4.0.0',
+              'replace' => ['symfony/foo' => 'self.version'],
+            ],
+            'dev-master' => $branchAlias('3.5') + [
+                'replace' => ['symfony/foo' => 'self.version'],
+              ],
+          ],
+          'symfony/foo' => [
+            '3.3.0' => ['version_normalized' => '3.3.0.0'],
+            '3.4.0' => ['version_normalized' => '3.4.0.0'],
+            'dev-master' => $branchAlias('3.5'),
+          ],
+        ];
+        yield 'empty-intersection-ignores' => [$packages, $packages, ['symfony/symfony' => '~2.0']];
+        yield 'empty-intersection-ignores' => [$packages, $packages, ['symfony/symfony' => '~4.0']];
+        $expected = $packages;
+        unset($expected['symfony/symfony']['3.3.0']);
+        unset($expected['symfony/foo']['3.3.0']);
+        yield 'non-empty-intersection-filters' => [$expected, $packages, ['symfony/symfony' => '~3.4']];
+        unset($expected['symfony/symfony']['3.4.0']);
+        unset($expected['symfony/foo']['3.4.0']);
+        yield 'master-only' => [$expected, $packages, ['symfony/symfony' => '~3.5']];
+        $packages = [
+          'symfony/symfony' => [
+            '2.8.0' => [
+              'version_normalized' => '2.8.0.0',
+              'replace' => [
+                'symfony/legacy' => 'self.version',
+                'symfony/foo' => 'self.version',
+              ],
+            ],
+          ],
+          'symfony/legacy' => [
+            '2.8.0' => ['version_normalized' => '2.8.0.0'],
+            'dev-master' => $branchAlias('2.8'),
+          ],
+        ];
+        yield 'legacy-are-not-filtered' => [$packages, $packages, ['symfony/symfony' => '~3.0']];
+        $packages = [
+          'symfony/symfony' => [
+            '2.8.0' => [
+              'version_normalized' => '2.8.0.0',
+              'replace' => [
+                'symfony/foo' => 'self.version',
+                'symfony/new' => 'self.version',
+              ],
+            ],
+            'dev-master' => $branchAlias('3.0') + [
+                'replace' => [
+                  'symfony/foo' => 'self.version',
+                  'symfony/new' => 'self.version',
+                ],
+              ],
+          ],
+          'symfony/foo' => [
+            '2.8.0' => ['version_normalized' => '2.8.0.0'],
+            'dev-master' => $branchAlias('3.0'),
+          ],
+          'symfony/new' => [
+            'dev-master' => $branchAlias('3.0'),
+          ],
+        ];
+        $expected = $packages;
+        unset($expected['symfony/symfony']['dev-master']);
+        unset($expected['symfony/foo']['dev-master']);
+        yield 'master-is-filtered-only-when-in-range' => [$expected, $packages, ['symfony/symfony' => '~2.8']];
+    }
+}
diff --git a/vendor/zaporylie/composer-drupal-optimizations/tests/DefaultRequireTest.php b/vendor/zaporylie/composer-drupal-optimizations/tests/DefaultRequireTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e3368c48ec42cd069a1689fbb246eaf0fcb61ca6
--- /dev/null
+++ b/vendor/zaporylie/composer-drupal-optimizations/tests/DefaultRequireTest.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace zaporylie\ComposerDrupalOptimizations\Tests;
+
+use Composer\Semver\VersionParser;
+use PHPUnit\Framework\TestCase;
+use zaporylie\ComposerDrupalOptimizations\Plugin;
+
+class DefaultRequireTest extends TestCase
+{
+
+    /**
+     * Tests default require provider.
+     *
+     * @param $provided
+     * @param $expected
+     * @dataProvider provideTestData
+     */
+    public function testDefaultRequire($provided, $expected)
+    {
+        $versionParser = new VersionParser();
+        self::assertEquals($expected, Plugin::getDefaultRequire($versionParser->parseConstraints($provided)));
+    }
+
+    /**
+     * Test data.
+     */
+    function provideTestData()
+    {
+        yield 'exact-below' => ['8.2.0', []];
+        yield 'exact-above' => ['8.6.0', ['symfony/symfony' => '>3.4']];
+        yield 'exact-min' => ['8.5.0', ['symfony/symfony' => '>3.4']];
+        yield 'range-below' => ['~8.4.0', []];
+        yield 'range-overlapping' => ['>8.4.0 <8.6.0', []];
+        yield 'range-below-above' => ['~8.2.0|~8.6.0', []];
+        yield 'range-above' => ['~8.6.0', ['symfony/symfony' => '>3.4']];
+        yield 'range-min' => ['^8.5', ['symfony/symfony' => '>3.4']];
+    }
+
+}
diff --git a/web/modules/google_tag/LICENSE.txt b/web/modules/google_tag/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1
--- /dev/null
+++ b/web/modules/google_tag/LICENSE.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/web/modules/google_tag/README.txt b/web/modules/google_tag/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bd5c359f6f782a47187286d86719b88b02493ce7
--- /dev/null
+++ b/web/modules/google_tag/README.txt
@@ -0,0 +1,91 @@
+
+CONTENTS OF THIS FILE
+---------------------
+
+ * Introduction
+ * Requirements
+ * Installation
+ * Configuration
+ * Troubleshooting
+ * Maintainers
+
+
+INTRODUCTION
+------------
+
+This Google Tag Manager project allows non-technical stakeholders to manage the
+analytics for their website by triggering the insertion of tags and tracking
+systems onto their page(s) via Google's Tag Manager (GTM) hosted application.
+
+ * For a full description, visit the project page:
+   https://www.drupal.org/project/google_tag
+
+ * To submit bug reports and feature suggestions, or to track changes:
+   https://www.drupal.org/project/issues/google_tag
+
+
+REQUIREMENTS
+------------
+
+Sign up for GTM and obtain a 'container ID' for your website. Enter the
+'container ID' on the settings form for this module (see Configuration).
+
+ * https://www.google.com/analytics/tag-manager/
+
+
+INSTALLATION
+------------
+
+Place the project files in an appropriate modules directory and enable the
+module as you would any other contributed module. For further information see:
+
+ * https://www.drupal.org/node/895232
+
+
+CONFIGURATION
+-------------
+
+Users in roles with the 'Administer Google Tag Manager' permission will be able
+to manage the settings for this module. Configure permissions as usual at:
+
+ * Administration » People » Permissions
+ * admin/people/permissions
+
+From the module settings page, configure the conditions on which the tags are
+inserted on a page response. Conditions exist for: page paths, user roles, and
+response statuses. See:
+
+ * Administration » Configuration » System » Google Tag Manager
+ * admin/config/system/google_tag
+
+For development purposes, create a GTM environment for your website and enter
+the 'environment ID' on the 'Advanced' tab of the settings form.
+
+ * https://tagmanager.google.com/#/admin
+
+
+TROUBLESHOOTING
+---------------
+
+If the JavaScript snippets are not present in the HTML output, try the following
+steps to debug the situation:
+
+ * Confirm the snippet files exist at public://google_tag/ (on most sites this
+   equates to sites/default/files/google_tag/).
+
+   If missing, then visit the module settings page and submit the form to
+   recreate the snippet files. The need to do this may arise if the project is
+   deployed from one environment to another (e.g. development to production) but
+   the snippet files are not deployed.
+
+ * Enable debug output on the 'Advanced' tab of the settings page to display the
+   result of each snippet insertion condition in the message area. Modify the
+   insertion conditions as needed.
+
+
+MAINTAINERS
+-----------
+
+Current maintainer:
+
+ * Jim Berry (https://www.drupal.org/u/solotandem)
diff --git a/web/modules/google_tag/composer.json b/web/modules/google_tag/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..66fb62a149d0c3db0a9612cd2bbfa031ef60df52
--- /dev/null
+++ b/web/modules/google_tag/composer.json
@@ -0,0 +1,6 @@
+{
+    "name": "drupal/google_tag",
+    "description": "Allows your website analytics to be managed using Google Tag Manager.",
+    "type": "drupal-module",
+    "license": "GPL 2.0"
+}
diff --git a/web/modules/google_tag/config/install/google_tag.settings.yml b/web/modules/google_tag/config/install/google_tag.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0cc57d2c0e51624f5b020b49c0d6749e18ddce8e
--- /dev/null
+++ b/web/modules/google_tag/config/install/google_tag.settings.yml
@@ -0,0 +1,19 @@
+container_id: ''
+path_toggle: 'exclude listed'
+path_list: "/admin*\n/batch*\n/node/add*\n/node/*/edit\n/node/*/delete\n/user/*/edit*\n/user/*/cancel*"
+role_toggle: 'exclude listed'
+role_list:
+  -
+status_toggle: 'exclude listed'
+status_list: "403\n404"
+compact_snippet: true
+include_file: true
+rebuild_snippets: true
+debug_output: false
+data_layer: 'dataLayer'
+include_classes: false
+whitelist_classes: "google\nnonGooglePixels\nnonGoogleScripts\nnonGoogleIframes"
+blacklist_classes: "customScripts\ncustomPixels"
+include_environment: false
+environment_id: ''
+environment_token: ''
diff --git a/web/modules/google_tag/config/schema/google_tag.schema.yml b/web/modules/google_tag/config/schema/google_tag.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6ad51032eab8da10e9bbbde99ca26b30dd8916d4
--- /dev/null
+++ b/web/modules/google_tag/config/schema/google_tag.schema.yml
@@ -0,0 +1,61 @@
+google_tag.settings:
+  type: config_object
+  mapping:
+    container_id:
+      type: string
+      label: 'Container ID'
+    path_toggle:
+      type: string
+      label: 'Add snippet on specific paths'
+    path_list:
+      type: string
+      label: 'Listed paths'
+    role_toggle:
+      type: string
+      label: 'Add snippet for specific roles'
+    role_list:
+      type: sequence
+      label: 'Selected roles'
+      sequence:
+        type: string
+        label: 'Role'
+    status_toggle:
+      type: string
+      label: 'Add snippet for specific statuses'
+    status_list:
+      type: string
+      label: 'Listed statuses'
+    compact_snippet:
+      type: boolean
+      label: 'Compact the JavaScript snippet'
+    include_file:
+      type: boolean
+      label: 'Include the snippet as a file'
+    rebuild_snippets:
+      type: boolean
+      label: 'Recreate snippets on cache rebuild'
+    debug_output:
+      type: boolean
+      label: 'Show debug output'
+    data_layer:
+      type: string
+      label: 'Data layer'
+    include_classes:
+      type: boolean
+      label: 'Add classes to the data layer'
+    whitelist_classes:
+      type: string
+      label: 'White-listed classes'
+    blacklist_classes:
+      type: string
+      label: 'Black-listed classes'
+    include_environment:
+      type: boolean
+      label: 'Include an environment'
+    environment_id:
+      type: string
+      label: 'Environment ID'
+    environment_token:
+      type: string
+      label: 'Environment token'
+
diff --git a/web/modules/google_tag/google_tag.api.php b/web/modules/google_tag/google_tag.api.php
new file mode 100644
index 0000000000000000000000000000000000000000..9f8f16d47ab02ef99a60b8383f395936e6c4eef1
--- /dev/null
+++ b/web/modules/google_tag/google_tag.api.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Documents hooks provided by this module.
+ *
+ * @author Jim Berry ("solotandem", http://drupal.org/user/240748)
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Alter the state of snippet insertion on the current page response.
+ *
+ * This hook allows other modules to alter the state of snippet insertion based
+ * on custom conditions that cannot be defined by the status, path, and role
+ * conditions provided by this module.
+ *
+ * @param bool $satisfied
+ *   The snippet insertion state.
+ */
+function hook_google_tag_insert_alter(&$satisfied) {
+  // Do something to the state.
+  $state = !$state;
+}
+
+/**
+ * Alter the snippets to be inserted on a page response.
+ *
+ * This hook allows other modules to alter the snippets to be inserted based on
+ * custom settings not defined by this module.
+ *
+ * @param array $snippets
+ *   Associative array of snippets keyed by type: script, noscript and
+ *   data_layer.
+ */
+function hook_google_tag_snippets_alter(&$snippets) {
+  // Do something to the script snippet.
+  $snippets['script'] = str_replace('insertBefore', 'insertAfter', $snippets['script']);
+}
diff --git a/web/modules/google_tag/google_tag.info.yml b/web/modules/google_tag/google_tag.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fd8033df49be973d32fb749f12e17d2e60eadd08
--- /dev/null
+++ b/web/modules/google_tag/google_tag.info.yml
@@ -0,0 +1,12 @@
+name: 'Google Tag Manager'
+type: module
+description: 'Allows your website analytics to be managed using Google Tag Manager.'
+package: 'Statistics'
+# core: 8.x
+configure: google_tag.settings_form
+
+# Information added by Drupal.org packaging script on 2018-08-23
+version: '8.x-1.1'
+core: '8.x'
+project: 'google_tag'
+datestamp: 1534988886
diff --git a/web/modules/google_tag/google_tag.install b/web/modules/google_tag/google_tag.install
new file mode 100644
index 0000000000000000000000000000000000000000..8c9ff1a532526eee96d8ac86b650972c22149ff1
--- /dev/null
+++ b/web/modules/google_tag/google_tag.install
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * @file
+ * Provides install, update, and uninstall functions.
+ *
+ * @author Jim Berry ("solotandem", http://drupal.org/user/240748)
+ */
+
+/**
+ * Implements hook_requirements().
+ */
+function google_tag_requirements($phase) {
+  $requirements = array();
+  if ($phase == 'runtime') {
+    $config = \Drupal::config('google_tag.settings');
+    if (!preg_match('/^GTM-\w{4,}$/', $config->get('container_id'))) {
+      // Google Tag Manager container ID has not been set.
+      $requirements['google_tag'] = array(
+        'title' => t('Google Tag Manager'),
+        'description' => t('Configure this integration module on its <a href=":url">settings page</a>.', array(':url' => \Drupal::url('google_tag.settings_form'))),
+        'severity' => REQUIREMENT_WARNING,
+        'value' => t('Not configured'),
+      );
+    }
+  }
+  if ($phase == 'runtime' || $phase == 'update' || $phase == 'install') {
+    // Adapted from system_requirements().
+    $directory = 'public://google_tag';
+    $phase == 'install' ? module_load_include('module', 'google_tag') : '';
+    if (!is_dir($directory) || !_google_tag_is_writable($directory) || !_google_tag_is_executable($directory)) {
+      __file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    }
+    $is_executable = _google_tag_is_executable($directory);
+    $is_writable = _google_tag_is_writable($directory);
+    $is_directory = is_dir($directory);
+    if (!$is_executable || !$is_writable || !$is_directory) {
+      // The snippet directory does not exist or is not writable or searchable.
+
+      // If applicable, get the directory path of stream wrapper.
+      $wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($directory);
+      if (method_exists($wrapper, 'getDirectoryPath') && ($path = $wrapper->getDirectoryPath())) {
+        // getDirectoryPath() is not defined in StreamWrapperInterface; it
+        // exists in LocalStream and the local storage replacement classes in
+        // google_appengine; s3fs returns an empty string.
+        $path .= '/google_tag';
+      }
+      elseif (!($path = $wrapper->getExternalUrl())) {
+        $path = $directory;
+      }
+
+      if (!$is_directory) {
+        $error = t('The directory %directory does not exist.', array('%directory' => $path));
+        $description = t('An automated attempt to create the directory failed, possibly due to a permissions problem. Create the directory and make it writable.');
+        $value = t('Does not exist');
+      }
+      elseif (!$is_writable) {
+        $error = t('The directory %directory is not writable.', array('%directory' => $path));
+        $description = t('An automated attempt to make the directory writable failed, possibly due to a permissions problem. Make the directory writable.');
+        $value = t('Not writable');
+      }
+      else {
+        $error = t('The directory %directory is not searchable.', array('%directory' => $path));
+        $description = t('An automated attempt to make the directory searchable failed, possibly due to a permissions problem. Make the directory searchable.');
+        $value = t('Not searchable');
+      }
+      $extra = '';
+      if ($phase == 'install') {
+        $extra = t('For more information, see INSTALL.txt or the <a href=":handbook_url">online handbook</a>.', array(':handbook_url' => 'https://www.drupal.org/server-permissions'));
+        $value = '';
+      }
+      $description = array(
+        '#type' => 'inline_template',
+        '#template' => '{{ error }} {{ description }} {{ extra }}',
+        '#context' => array(
+          'error' => $error,
+          'description' => $description,
+          'extra' => $extra,
+        ),
+      );
+      $requirements['google_tag_snippet_directory'] = array(
+        'title' => t('Google Tag Manager snippet directory'),
+        'description' => $description,
+        'severity' => REQUIREMENT_ERROR,
+        'value' => $value,
+      );
+    }
+  }
+  return $requirements;
+}
+
+/**
+ * Implements hook_install().
+ */
+function google_tag_install() {
+  global $google_tag_display_message;
+  $google_tag_display_message = TRUE;
+  _google_tag_assets_create();
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function google_tag_uninstall() {
+  @file_unmanaged_delete_recursive('public://google_tag');
+  \Drupal::service('asset.js.collection_optimizer')->deleteAll();
+  _drupal_flush_css_js();
+}
diff --git a/web/modules/google_tag/google_tag.libraries.yml b/web/modules/google_tag/google_tag.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..52e9422d0019a06ebc4613c7db9e7687d1c6ebf4
--- /dev/null
+++ b/web/modules/google_tag/google_tag.libraries.yml
@@ -0,0 +1,7 @@
+drupal.settings_form:
+  js:
+    js/google_tag.admin.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupal.form
diff --git a/web/modules/google_tag/google_tag.links.menu.yml b/web/modules/google_tag/google_tag.links.menu.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c394f1b841a004b5cc752ade9f9a229f4b0183c6
--- /dev/null
+++ b/web/modules/google_tag/google_tag.links.menu.yml
@@ -0,0 +1,5 @@
+google_tag.settings_form:
+  title: 'Google Tag Manager'
+  description: 'Configure the website integration with GTM and the resultant capturing of website analytics.'
+  route_name: google_tag.settings_form
+  parent: system.admin_config_system
diff --git a/web/modules/google_tag/google_tag.module b/web/modules/google_tag/google_tag.module
new file mode 100644
index 0000000000000000000000000000000000000000..9175c09ae8bc909ef5005cb90ae86b18be957752
--- /dev/null
+++ b/web/modules/google_tag/google_tag.module
@@ -0,0 +1,391 @@
+<?php
+
+/**
+ * @file
+ * Provides primary Drupal hook implementations.
+ *
+ * Adds a JavaScript snippet to selected page responses to trigger analytics and
+ * other tracking items configured using the Google Tag Manager.
+ *
+ * @author Jim Berry ("solotandem", http://drupal.org/user/240748)
+ */
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Default for matching all items except listed.
+ */
+const GOOGLE_TAG_EXCLUDE_LISTED = 'exclude listed';
+
+/**
+ * Default for matching only listed items.
+ */
+const GOOGLE_TAG_INCLUDE_LISTED = 'include listed';
+
+/**
+ * Implements hook_help().
+ */
+function google_tag_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.google_tag':
+    case 'google_tag.settings_form':
+      return t('<a href=":url">Google Tag Manager</a> is a free service (registration required) to manage the insertion of tags for capturing website analytics.', [':url' => 'https://tagmanager.google.com/']);
+  }
+}
+
+/**
+ * Implements hook_rebuild().
+ */
+function google_tag_rebuild() {
+  $rebuild_snippets = \Drupal::config('google_tag.settings')->get('rebuild_snippets');
+  if ($rebuild_snippets) {
+    _google_tag_assets_create();
+  }
+}
+
+/**
+ * Saves snippet files and data layer classes based on current settings.
+ */
+function _google_tag_assets_create() {
+  $config_factory = \Drupal::configFactory();
+  $form = new Drupal\google_tag\Form\GoogleTagSettingsForm($config_factory);
+  $form->createAssets();
+}
+
+/**
+ * Implements hook_page_attachments().
+ */
+function google_tag_page_attachments(&$attachments) {
+  if (!google_tag_insert_snippet()) {
+    return;
+  }
+
+  $config = \Drupal::config('google_tag.settings');
+  $include_script_as_file = $config->get('include_file');
+  $include_classes = $config->get('include_classes');
+  $types = $include_classes ? ['data_layer', 'script'] : ['script'];
+
+  // Add data_layer and script snippets to head (no longer by default).
+  $weight = 9;
+  if ($include_script_as_file) {
+    foreach ($types as $type) {
+      // @todo Will it matter if file is empty?
+      // @todo Check config for the whitelist and blacklist classes before adding.
+      $attachments['#attached']['html_head'][] = _google_tag_file_tag($type, $weight++);
+    }
+  }
+  else {
+    foreach ($types as $type) {
+      // @see drupal_get_js() in 7.x core.
+      // For inline JavaScript to validate as XHTML, all JavaScript containing
+      // XHTML needs to be wrapped in CDATA.
+      $attachments['#attached']['html_head'][] = _google_tag_inline_tag($type, $weight++);
+    }
+  }
+}
+
+/**
+ * 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.
+ */
+function _google_tag_file_tag($type, $weight) {
+  $uri = "public://google_tag/google_tag.$type.js";
+  $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],
+      '#weight' => $weight,
+    ],
+    "google_tag_{$type}_tag",
+  ];
+  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.
+ */
+function _google_tag_inline_tag($type, $weight) {
+  $uri = "public://google_tag/google_tag.$type.js";
+  $url = \Drupal::service('file_system')->realpath($uri);
+  $contents = @file_get_contents($url);
+  $attachment = $contents ? [
+    [
+      '#type' => 'html_tag',
+      '#tag' => 'script',
+      '#value' => $contents,
+      '#weight' => $weight,
+    ],
+    "google_tag_{$type}_tag",
+  ] : [];
+  return $attachment;
+}
+
+/**
+ * Implements hook_page_top().
+ */
+function google_tag_page_top(&$page) {
+  if (!google_tag_insert_snippet()) {
+    return;
+  }
+
+  // Add noscript snippet to page_top region.
+  $uri = 'public://google_tag/google_tag.noscript.js';
+  $url = \Drupal::service('file_system')->realpath($uri);
+  $contents = @file_get_contents($url);
+
+  // 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.)
+  // @todo Core is removing the 'style' attribute from the noscript snippet.
+  if ($contents) {
+    $page['google_tag_noscript_tag'] = [
+      '#markup' => $contents,
+      '#allowed_tags' => ['noscript', 'iframe'],
+      '#weight' => -10,
+    ];
+  }
+}
+
+/**
+ * Determines whether to insert the snippet on the response.
+ *
+ * @return bool
+ *   TRUE if the conditions are met; FALSE otherwise.
+ */
+function google_tag_insert_snippet() {
+  static $satisfied;
+
+  if (!isset($satisfied)) {
+    $config = \Drupal::config('google_tag.settings');
+    $debug = $config->get('debug_output');
+    $id = $config->get('container_id');
+
+    if (empty($id)) {
+      // No container ID.
+      return FALSE;
+    }
+
+    $satisfied = TRUE;
+    if (!_google_tag_status_check() || !_google_tag_path_check() || !_google_tag_role_check()) {
+      // Omit snippet if any condition is not met.
+      $satisfied = FALSE;
+    }
+
+    // Allow other modules to alter the insertion criteria.
+    \Drupal::moduleHandler()->alter('google_tag_insert', $satisfied);
+    $debug ? drupal_set_message(t('after alter @satisfied', ['@satisfied' => $satisfied])) : '';
+  }
+  return $satisfied;
+}
+
+/**
+ * Determines whether to insert the snippet based on status code settings.
+ *
+ * @return bool
+ *   TRUE if the status conditions are met; FALSE otherwise.
+ */
+function _google_tag_status_check() {
+  static $satisfied;
+
+  if (!isset($satisfied)) {
+    $config = \Drupal::config('google_tag.settings');
+    $debug = $config->get('debug_output');
+    $toggle = $config->get('status_toggle');
+    $statuses = $config->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;
+    }
+    $debug ? drupal_set_message(t('google_tag')) : '';
+    $debug ? drupal_set_message(t('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.
+ */
+function _google_tag_path_check() {
+  static $satisfied;
+
+  if (!isset($satisfied)) {
+    $config = \Drupal::config('google_tag.settings');
+    $debug = $config->get('debug_output');
+    $toggle = $config->get('path_toggle');
+    $paths = Unicode::strtolower($config->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 = Unicode::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;
+    }
+    $debug ? drupal_set_message(t('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.
+ */
+function _google_tag_role_check() {
+  static $satisfied;
+
+  if (!isset($satisfied)) {
+    $config = \Drupal::config('google_tag.settings');
+    $debug = $config->get('debug_output');
+    $toggle = $config->get('role_toggle');
+    $roles = $config->get('role_list');
+    $roles = array_filter($roles);
+
+    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;
+    }
+    $debug ? drupal_set_message(t('role check @satisfied', ['@satisfied' => $satisfied])) : '';
+  }
+  return $satisfied;
+}
+
+/**
+ * Checks that the directory exists and is writable.
+ *
+ * @todo Remove this function if core is updated to check the executable bit.
+ *
+ * @see file_prepare_directory()
+ */
+function __file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
+  if (!file_stream_wrapper_valid_scheme(\Drupal::service('file_system')->uriScheme($directory))) {
+    // Only trim if we're not dealing with a stream.
+    $directory = rtrim($directory, '/\\');
+  }
+
+  // Check if directory exists.
+  if (!is_dir($directory)) {
+    // Let mkdir() recursively create directories and use the default directory
+    // permissions.
+    if ($options & FILE_CREATE_DIRECTORY) {
+      return @drupal_mkdir($directory, NULL, TRUE);
+    }
+    return FALSE;
+  }
+  // The directory exists, so check to see if it is writable.
+  $writable = _google_tag_is_writable($directory) && _google_tag_is_executable($directory);
+  if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
+    return drupal_chmod($directory);
+  }
+
+  return $writable;
+}
+
+/**
+ * Determines whether a directory is writable.
+ *
+ * Remove this if PHP is_writable() is changed to respect ACLS on a 'local'
+ * stream wrapper other than the local file wrapper provided by PHP.
+ *
+ * @param string $uri
+ *   A directory path or stream wrapper URI.
+ *
+ * @return bool
+ *   Whether the directory is writable.
+ */
+function _google_tag_is_writable($uri) {
+  // Use the local path, if applicable, since PHP only checks ACLs on its local
+  // file wrapper.
+  $realpath = \Drupal::service('file_system')->realpath($uri);
+  return is_writable($realpath ? $realpath : $uri);
+}
+
+/**
+ * Determines whether a directory is searchable.
+ *
+ * Remove this if PHP is_executable() is changed to not return FALSE simply
+ * because the URI points to a directory (not a file) in a stream wrapper other
+ * than the local file wrapper provided by PHP.
+ *
+ * @param string $uri
+ *   A directory path or stream wrapper URI.
+ *
+ * @return bool
+ *   Whether the directory is searchable.
+ */
+function _google_tag_is_executable($uri) {
+  if ($realpath = \Drupal::service('file_system')->realpath($uri)) {
+    // The URI is a local stream wrapper or a local path.
+    // Use the local path since PHP only checks ACLs on its local file wrapper.
+    return is_executable($realpath);
+  }
+  if ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri)) {
+    // The URI is a remote stream wrapper.
+    if (!($stat = $wrapper->url_stat($uri, 0))) {
+      return FALSE;
+    }
+
+    // Determine the appropriate permissions bit mask as an octal.
+    // The stat array is likely to have uid=gid=0 so that the mask is octal 01.
+    // This is true for Amazon S3 and Google Cloud Storage.
+    $mask = 1;
+    if ($stat['uid'] == posix_getuid()) {
+      $mask = $mask << 6;
+    }
+    elseif ($stat['gid'] == posix_getgid()) {
+      $mask = $mask << 3;
+    }
+    return ($stat['mode'] & $mask) != 0;
+  }
+  return FALSE;
+}
diff --git a/web/modules/google_tag/google_tag.permissions.yml b/web/modules/google_tag/google_tag.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f24e8b92f2c1038d948eb5f3d24d025ad720b88f
--- /dev/null
+++ b/web/modules/google_tag/google_tag.permissions.yml
@@ -0,0 +1,3 @@
+administer google tag manager:
+  title: 'Administer Google Tag Manager'
+  description: 'Configure the website integration with Google Tag Manager'
diff --git a/web/modules/google_tag/google_tag.routing.yml b/web/modules/google_tag/google_tag.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9d2fe9888930e108b8d327d0e1c054cdc06f28ea
--- /dev/null
+++ b/web/modules/google_tag/google_tag.routing.yml
@@ -0,0 +1,7 @@
+google_tag.settings_form:
+  path: '/admin/config/system/google_tag'
+  defaults:
+    _title: 'Google Tag Manager'
+    _form: '\Drupal\google_tag\Form\GoogleTagSettingsForm'
+  requirements:
+    _permission: 'administer google tag manager'
diff --git a/web/modules/google_tag/includes/snippet.inc b/web/modules/google_tag/includes/snippet.inc
new file mode 100644
index 0000000000000000000000000000000000000000..1e9bc6191f143daef7ef0d6bcbaeedce0a522f2a
--- /dev/null
+++ b/web/modules/google_tag/includes/snippet.inc
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @file
+ * Contains the JavaScript snippet insertion code.
+ *
+ * @author Jim Berry ("solotandem", http://drupal.org/user/240748)
+ */
+
+/**
+ * Returns JavaScript snippets.
+ *
+ * @return array
+ *   Associative array of snippets keyed by type: script, noscript and
+ *   data_layer.
+ */
+function google_tag_snippets() {
+  $snippets = [
+    'script' => _google_tag_script_snippet(),
+    'noscript' => _google_tag_noscript_snippet(),
+    'data_layer' => _google_tag_data_layer_snippet(),
+  ];
+  // Allow other modules to alter the snippets.
+  \Drupal::moduleHandler()->alter('google_tag_snippets', $snippets);
+  return $snippets;
+}
+
+/**
+ * Returns JavaScript script snippet.
+ *
+ * @return array
+ *   The script snippet.
+ */
+function _google_tag_script_snippet() {
+  // Gather data.
+  $config = \Drupal::config('google_tag.settings');
+  $container_id = _google_tag_variable_clean('container_id');
+  $data_layer = _google_tag_variable_clean('data_layer');
+  $query = _google_tag_environment_query();
+  $compact = $config->get('compact_snippet');
+
+  // 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+'$query';
+  j.async=true;
+  f.parentNode.insertBefore(j,f);
+
+})(window,document,'script','$data_layer','$container_id');
+EOS;
+  if ($compact) {
+    $script = str_replace(["\n", '  '], '', $script);
+  }
+/*
+  $script = <<<EOS
+<!-- Google Tag Manager -->
+$script
+<!-- End Google Tag Manager -->
+EOS;
+*/
+  return $script;
+}
+
+/**
+ * Returns JavaScript noscript snippet.
+ *
+ * @return array
+ *   The noscript snippet.
+ */
+function _google_tag_noscript_snippet() {
+  // Gather data.
+  $config = \Drupal::config('google_tag.settings');
+  $container_id = _google_tag_variable_clean('container_id');
+  $query = _google_tag_environment_query();
+  $compact = $config->get('compact_snippet');
+
+  // Build noscript snippet.
+  // @todo Core removes the 'style' attribute from the snippet; so omit it.
+  // style="display:none;visibility:hidden"
+  $noscript = <<<EOS
+<noscript aria-hidden="true"><iframe src="https://www.googletagmanager.com/ns.html?id=$container_id$query"
+ height="0" width="0" title="Google Tag Manager"></iframe></noscript>
+EOS;
+  if ($compact) {
+    $noscript = str_replace("\n", '', $noscript);
+  }
+/*
+  $noscript = <<<EOS
+<!-- Google Tag Manager -->
+$noscript
+<!-- End Google Tag Manager -->
+EOS;
+*/
+  return $noscript;
+}
+
+/**
+ * Returns JavaScript data layer snippet or adds items to data layer.
+ *
+ * @return string|null
+ *   The data layer snippet or NULL.
+ */
+function _google_tag_data_layer_snippet() {
+  // Gather data.
+  $config = \Drupal::config('google_tag.settings');
+  $data_layer = _google_tag_variable_clean('data_layer');
+  $whitelist = $config->get('whitelist_classes');
+  $blacklist = $config->get('blacklist_classes');
+
+  $classes = [];
+  $names = ['whitelist', 'blacklist'];
+  foreach ($names as $name) {
+    $$name = explode("\n", $$name);
+    if (empty($$name)) {
+      continue;
+    }
+    $classes["gtm.$name"] = $$name;
+  }
+
+  if ($classes) {
+    // Build data layer snippet.
+    $script = "var $data_layer = [" . json_encode($classes) . '];';
+    return $script;
+  }
+}
+
+/**
+ * Returns a query string with the environment parameters.
+ *
+ * @return string
+ *   The query string.
+ */
+function _google_tag_environment_query() {
+  $config = \Drupal::config('google_tag.settings');
+  if (!$config->get('include_environment')) {
+    return '';
+  }
+
+  // Gather data.
+  $environment_id = _google_tag_variable_clean('environment_id');
+  $environment_token = _google_tag_variable_clean('environment_token');
+
+  // Build query string.
+  return "&gtm_auth=$environment_token&gtm_preview=$environment_id&gtm_cookies_win=x";
+}
+
+/**
+ * Returns a cleansed variable.
+ *
+ * @param string $variable
+ *   The variable name.
+ *
+ * @return string
+ *   The cleansed variable.
+ */
+function _google_tag_variable_clean($variable) {
+  $config = \Drupal::config('google_tag.settings');
+  return trim(json_encode($config->get($variable)), '"');
+}
diff --git a/web/modules/google_tag/js/google_tag.admin.js b/web/modules/google_tag/js/google_tag.admin.js
new file mode 100644
index 0000000000000000000000000000000000000000..da9a14402d4e130b006eb4e535276495bb9953d7
--- /dev/null
+++ b/web/modules/google_tag/js/google_tag.admin.js
@@ -0,0 +1,84 @@
+/**
+ * @file
+ * Behaviors and utility functions for administrative pages.
+ *
+ * @author Jim Berry ("solotandem", http://drupal.org/user/240748)
+ */
+
+(function ($) {
+
+  "use strict";
+
+  /**
+  * Provides summary information for the vertical tabs.
+  */
+  Drupal.behaviors.gtmInsertionSettings = {
+    attach: function (context) {
+
+      $('details#edit-path', context).drupalSetSummary(function (context) {
+        var $radio = $('input[name="path_toggle"]:checked', context);
+        if ($radio.val() == 'exclude listed') {
+          if (!$('textarea[name="path_list"]', context).val()) {
+            return Drupal.t('All paths');
+          }
+          else {
+            return Drupal.t('All paths except listed paths');
+          }
+        }
+        else {
+          if (!$('textarea[name="path_list"]', context).val()) {
+            return Drupal.t('No paths');
+          }
+          else {
+            return Drupal.t('Only listed paths');
+          }
+        }
+      });
+
+      $('details#edit-role', context).drupalSetSummary(function (context) {
+        var vals = [];
+        $('input[type="checkbox"]:checked', context).each(function () {
+          vals.push($.trim($(this).next('label').text()));
+        });
+        var $radio = $('input[name="role_toggle"]:checked', context);
+        if ($radio.val() == 'exclude listed') {
+          if (!vals.length) {
+            return Drupal.t('All roles');
+          }
+          else {
+            return Drupal.t('All roles except selected roles');
+          }
+        }
+        else {
+          if (!vals.length) {
+            return Drupal.t('No roles');
+          }
+          else {
+            return Drupal.t('Only selected roles');
+          }
+        }
+      });
+
+      $('details#edit-status', context).drupalSetSummary(function (context) {
+        var $radio = $('input[name="status_toggle"]:checked', context);
+        if ($radio.val() == 'exclude listed') {
+          if (!$('textarea[name="status_list"]', context).val()) {
+            return Drupal.t('All statuses');
+          }
+          else {
+            return Drupal.t('All statuses except listed statuses');
+          }
+        }
+        else {
+          if (!$('textarea[name="status_list"]', context).val()) {
+            return Drupal.t('No statuses');
+          }
+          else {
+            return Drupal.t('Only listed statuses');
+          }
+        }
+      });
+    }
+  };
+
+})(jQuery);
diff --git a/web/modules/google_tag/src/Form/GoogleTagSettingsForm.php b/web/modules/google_tag/src/Form/GoogleTagSettingsForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..1056b8ba0437fe64c7cd2ace002dd07ce7d283f3
--- /dev/null
+++ b/web/modules/google_tag/src/Form/GoogleTagSettingsForm.php
@@ -0,0 +1,432 @@
+<?php
+
+/**
+ * @author Jim Berry ("solotandem", http://drupal.org/user/240748)
+ */
+
+namespace Drupal\google_tag\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Class GoogleTagSettingsForm
+ * @package Drupal\google_tag\Form
+ */
+class GoogleTagSettingsForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'google_tag_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['google_tag.settings'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $config = $this->config('google_tag.settings');
+
+    // Build form elements.
+    $form['settings'] = [
+      '#type' => 'vertical_tabs',
+      '#attributes' => ['class' => ['google-tag']],
+      '#attached' => [
+        'library' => ['google_tag/drupal.settings_form'],
+      ],
+    ];
+
+    // General tab.
+    $form['general'] = [
+      '#type' => 'details',
+      '#title' => $this->t('General'),
+      '#group' => 'settings',
+    ];
+
+    $form['general']['container_id'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Container ID'),
+      '#description' => $this->t('The ID assigned by Google Tag Manager (GTM) for this website container. To get a container ID, <a href="https://tagmanager.google.com/">sign up for GTM</a> and create a container for your website.'),
+      '#default_value' => $config->get('container_id'),
+      '#attributes' => ['placeholder' => ['GTM-xxxxxx']],
+      '#size' => 12,
+      '#maxlength' => 15,
+      '#required' => TRUE,
+    ];
+
+    // Page paths tab.
+    $description = $this->t('On this and the next two tabs, specify the conditions on which the GTM JavaScript snippet will either be included in or excluded from the page response, thereby enabling or disabling tracking and other analytics. All conditions must be satisfied for the snippet to be included. The snippet will be excluded if any condition is not met.<br /><br />On this tab, specify the path condition.');
+
+    // @todo Use singular for element names.
+    $form['path'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Page paths'),
+      '#group' => 'settings',
+      '#description' => $description,
+    ];
+
+    $form['path']['path_toggle'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Add snippet on specific paths'),
+      '#options' => [
+        GOOGLE_TAG_EXCLUDE_LISTED => $this->t('All paths except the listed paths'),
+        GOOGLE_TAG_INCLUDE_LISTED => $this->t('Only the listed paths'),
+      ],
+      '#default_value' => $config->get('path_toggle'),
+    ];
+
+    $args = [
+      '%blog' => '/blog',
+      '%blog-wildcard' => '/blog/*',
+      '%front' => '<front>',
+    ];
+
+    $form['path']['path_list'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Listed paths'),
+      '#description' => $this->t('Enter one relative path per line using the "*" character as a wildcard. Example paths are: "%blog" for the blog page, "%blog-wildcard" for each individual blog, and "%front" for the front page.', $args),
+      '#default_value' => $config->get('path_list'),
+      '#rows' => 10,
+    ];
+
+    // User roles tab.
+    $form['role'] = [
+      '#type' => 'details',
+      '#title' => $this->t('User roles'),
+      '#description' => $this->t('On this tab, specify the user role condition.'),
+      '#group' => 'settings',
+    ];
+
+    $form['role']['role_toggle'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Add snippet for specific roles'),
+      '#options' => [
+        GOOGLE_TAG_EXCLUDE_LISTED => $this->t('All roles except the selected roles'),
+        GOOGLE_TAG_INCLUDE_LISTED => $this->t('Only the selected roles'),
+      ],
+      '#default_value' => $config->get('role_toggle'),
+    ];
+
+    $user_roles = array_map(function($role) {
+      return $role->label();
+    }, user_roles());
+
+    $form['role']['role_list'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Selected roles'),
+      '#default_value' => $config->get('role_list'),
+      '#options' => $user_roles,
+    ];
+
+    // Response statuses tab.
+    $description = $this->t('Enter one response status per line. For more information, refer to the <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes">list of HTTP status codes</a>.');
+
+    $form['status'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Response statuses'),
+      '#group' => 'settings',
+      '#description' => $this->t('On this tab, specify the page response status condition.'),
+    ];
+
+    $form['status']['status_toggle'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Add snippet for specific statuses'),
+      '#options' => [
+        GOOGLE_TAG_EXCLUDE_LISTED => $this->t('All statuses except the listed statuses'),
+        GOOGLE_TAG_INCLUDE_LISTED => $this->t('Only the listed statuses'),
+      ],
+      '#default_value' => $config->get('status_toggle'),
+    ];
+
+    $form['status']['status_list'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Listed statuses'),
+      '#description' => $description,
+      '#default_value' => $config->get('status_list'),
+      '#rows' => 5,
+    ];
+
+    // Advanced tab.
+    $form['advanced'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Advanced'),
+      '#group' => 'settings',
+    ];
+
+    $form['advanced']['compact_snippet'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Compact the JavaScript snippet'),
+      '#description' => $this->t('If checked, then the JavaScript snippet will be compacted to remove unnecessary whitespace. This is <strong>recommended on production sites</strong>. Leave unchecked to output a snippet that can be examined using a JavaScript debugger in the browser.'),
+      '#default_value' => $config->get('compact_snippet'),
+    ];
+
+    $form['advanced']['include_file'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Include the snippet as a file'),
+      '#description' => $this->t('If checked, then each JavaScript snippet will be included as a file. This is <strong>recommended</strong>. Leave unchecked to inline each snippet into the page. This only applies to data layer and script snippets.'),
+      '#default_value' => $config->get('include_file'),
+    ];
+
+    $form['advanced']['rebuild_snippets'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Recreate snippets on cache rebuild'),
+      '#description' => $this->t('If checked, then the JavaScript snippet files will be created during a cache rebuild. This is <strong>recommended on production sites</strong>.'),
+      '#default_value' => $config->get('rebuild_snippets'),
+    ];
+
+    $form['advanced']['debug_output'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Show debug output'),
+      '#description' => $this->t('If checked, then the result of each snippet insertion condition will be shown in the message area. Enable <strong>only for development</strong> purposes.'),
+      '#default_value' => $config->get('debug_output'),
+    ];
+
+    $form['advanced']['data_layer'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Data layer'),
+      '#description' => $this->t('The name of the data layer. Default value is "dataLayer". In most cases, use the default.'),
+      '#default_value' => $config->get('data_layer'),
+      '#attributes' => ['placeholder' => ['dataLayer']],
+      '#required' => TRUE,
+    ];
+
+    $form['advanced']['include_classes'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Add classes to the data layer'),
+      '#description' => $this->t('If checked, then the listed classes will be added to the data layer.'),
+      '#default_value' => $config->get('include_classes'),
+    ];
+
+    $description = $this->t('The types of tags, triggers, and variables <strong>allowed</strong> on a page. Enter one class per line. For more information, refer to the <a href="https://developers.google.com/tag-manager/devguide#security">developer documentation</a>.');
+
+    $form['advanced']['whitelist_classes'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('White-listed classes'),
+      '#description' => $description,
+      '#default_value' => $config->get('whitelist_classes'),
+      '#rows' => 5,
+      '#states' => $this->statesArray('include_classes'),
+    ];
+
+    $form['advanced']['blacklist_classes'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Black-listed classes'),
+      '#description' => $this->t('The types of tags, triggers, and variables <strong>forbidden</strong> on a page. Enter one class per line.'),
+      '#default_value' => $config->get('blacklist_classes'),
+      '#rows' => 5,
+      '#states' => $this->statesArray('include_classes'),
+    ];
+
+    $form['advanced']['include_environment'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Include an environment'),
+      '#description' => $this->t('If checked, then the applicable snippets will include the environment items below. Enable <strong>only for development</strong> purposes.'),
+      '#default_value' => $config->get('include_environment'),
+    ];
+
+    $description = $this->t('The environment ID to use with this website container. To get an environment ID, <a href="https://tagmanager.google.com/#/admin">select Environments</a>, create an environment, then click the "Get Snippet" action. The environment ID and token will be in the snippet.');
+
+    $form['advanced']['environment_id'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Environment ID'),
+      '#description' => $description,
+      '#default_value' => $config->get('environment_id'),
+      '#attributes' => ['placeholder' => ['env-x']],
+      '#size' => 10,
+      '#maxlength' => 7,
+      '#states' => $this->statesArray('include_environment'),
+    ];
+
+    $form['advanced']['environment_token'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Environment token'),
+      '#description' => $this->t('The authentication token for this environment.'),
+      '#default_value' => $config->get('environment_token'),
+      '#attributes' => ['placeholder' => ['xxxxxxxxxxxxxxxxxxxxxx']],
+      '#size' => 20,
+      '#maxlength' => 25,
+      '#states' => $this->statesArray('include_environment'),
+    ];
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * Returns states array for a form element.
+   *
+   * @param string $variable
+   *   The name of the form element.
+   *
+   * @return array
+   *   The states array.
+   */
+  public function statesArray($variable) {
+    return [
+      'required' => [
+        ':input[name="' . $variable . '"]' => ['checked' => TRUE],
+      ],
+      'invisible' => [
+        ':input[name="' . $variable . '"]' => ['checked' => FALSE],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    // Trim the text values.
+    $container_id = trim($form_state->getValue('container_id'));
+    $environment_id = trim($form_state->getValue('environment_id'));
+    $form_state->setValue('data_layer', trim($form_state->getValue('data_layer')));
+    $form_state->setValue('path_list', $this->cleanText($form_state->getValue('path_list')));
+    $form_state->setValue('status_list', $this->cleanText($form_state->getValue('status_list')));
+    $form_state->setValue('whitelist_classes', $this->cleanText($form_state->getValue('whitelist_classes')));
+    $form_state->setValue('blacklist_classes', $this->cleanText($form_state->getValue('blacklist_classes')));
+
+    // Replace all types of dashes (n-dash, m-dash, minus) with a normal dash.
+    $container_id = str_replace(['–', '—', '−'], '-', $container_id);
+    $environment_id = str_replace(['–', '—', '−'], '-', $environment_id);
+    $form_state->setValue('container_id', $container_id);
+    $form_state->setValue('environment_id', $environment_id);
+
+    if (!preg_match('/^GTM-\w{4,}$/', $container_id)) {
+      // @todo Is there a more specific regular expression that applies?
+      // @todo Is there a way to "test the connection" to determine a valid ID for
+      // a container? It may be valid but not the correct one for the website.
+      $form_state->setError($form['general']['container_id'], $this->t('A valid container ID is case sensitive and formatted like GTM-xxxxxx.'));
+    }
+    if ($form_state->getValue('include_environment') && !preg_match('/^env-\d{1,}$/', $environment_id)) {
+      $form_state->setError($form['advanced']['environment_id'], $this->t('A valid environment ID is case sensitive and formatted like env-x.'));
+    }
+
+    parent::validateForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->config('google_tag.settings')
+      ->set('container_id', $form_state->getValue('container_id'))
+      ->set('path_toggle', $form_state->getValue('path_toggle'))
+      ->set('path_list', $form_state->getValue('path_list'))
+      ->set('role_toggle', $form_state->getValue('role_toggle'))
+      ->set('role_list', $form_state->getValue('role_list'))
+      ->set('status_toggle', $form_state->getValue('status_toggle'))
+      ->set('status_list', $form_state->getValue('status_list'))
+      ->set('compact_snippet', $form_state->getValue('compact_snippet'))
+      ->set('include_file', $form_state->getValue('include_file'))
+      ->set('rebuild_snippets', $form_state->getValue('rebuild_snippets'))
+      ->set('debug_output', $form_state->getValue('debug_output'))
+      ->set('data_layer', $form_state->getValue('data_layer'))
+      ->set('include_classes', $form_state->getValue('include_classes'))
+      ->set('whitelist_classes', $form_state->getValue('whitelist_classes'))
+      ->set('blacklist_classes', $form_state->getValue('blacklist_classes'))
+      ->set('include_environment', $form_state->getValue('include_environment'))
+      ->set('environment_id', $form_state->getValue('environment_id'))
+      ->set('environment_token', $form_state->getValue('environment_token'))
+      ->save();
+
+    parent::submitForm($form, $form_state);
+
+    global $google_tag_display_message;
+    $google_tag_display_message = TRUE;
+    $this->createAssets();
+  }
+
+  /**
+   * Prepares directory for and saves snippet files based on current settings.
+   *
+   * @return bool
+   *   Whether the files were saved.
+   */
+  public function createAssets() {
+    $result = TRUE;
+    $directory = 'public://google_tag';
+    if (!is_dir($directory) || !_google_tag_is_writable($directory) || !_google_tag_is_executable($directory)) {
+      $result = __file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    }
+    if ($result) {
+      $result = $this->saveSnippets();
+    }
+    else {
+      $args = ['%directory' => $directory];
+      $message = 'The directory %directory could not be prepared for use, possibly due to file system permissions. The directory either does not exist, or is not writable or searchable.';
+      $this->displayMessage($message, $args, 'error');
+      \Drupal::logger('google_tag')->error($message, $args);
+    }
+    return $result;
+  }
+
+  /**
+   * Saves JS snippet files based on current settings.
+   *
+   * @return bool
+   *   Whether the files were saved.
+   */
+  public function saveSnippets() {
+    // Save the altered snippets after hook_google_tag_snippets_alter().
+    module_load_include('inc', 'google_tag', 'includes/snippet');
+    $result = TRUE;
+    $snippets = google_tag_snippets();
+    foreach ($snippets as $type => $snippet) {
+      $path = file_unmanaged_save_data($snippet, "public://google_tag/google_tag.$type.js", FILE_EXISTS_REPLACE);
+      $result = !$path ? FALSE : $result;
+    }
+    $args = ['@count' => count($snippets)];
+    if (!$result) {
+      $message = 'An error occurred saving @count snippet files. Contact the site administrator if this persists.';
+      $this->displayMessage($message, $args, 'error');
+      \Drupal::logger('google_tag')->error($message, $args);
+    }
+    else {
+      $message = 'Created @count snippet files based on configuration.';
+      $this->displayMessage($message, $args);
+      \Drupal::service('asset.js.collection_optimizer')->deleteAll();
+      _drupal_flush_css_js();
+    }
+    return $result;
+  }
+
+  /**
+   * Cleans a string representing a list of items.
+   *
+   * @param string $text
+   *   The string to clean.
+   * @param string $format
+   *   The final format of $text, either 'string' or 'array'.
+   *
+   * @return string
+   *   The clean text.
+   */
+  public function cleanText($text, $format = 'string') {
+    $text = explode("\n", $text);
+    $text = array_map('trim', $text);
+    $text = array_filter($text, 'trim');
+    if ($format == 'string') {
+      $text = implode("\n", $text);
+    }
+    return $text;
+  }
+
+  /**
+   * Displays a message to admin users.
+   *
+   * @see arguments to t() and drupal_set_message()
+   */
+  public function displayMessage($message, $args = [], $type = 'status') {
+    global $google_tag_display_message;
+    if ($google_tag_display_message) {
+      drupal_set_message($this->t($message, $args), $type);
+    }
+  }
+}