diff --git a/PATCHES/amazon_ses-3417090-cron-queue-14.patch b/PATCHES/amazon_ses-3417090-cron-queue-14.patch new file mode 100644 index 000000000..627b81779 --- /dev/null +++ b/PATCHES/amazon_ses-3417090-cron-queue-14.patch @@ -0,0 +1,18 @@ +diff --git a/src/Plugin/QueueWorker/AmazonSesMailQueue.php b/src/Plugin/QueueWorker/AmazonSesMailQueue.php +index dc410504566451d20aec939de6953e6d1a6e9be9..fdc7af32ac6634c51283fb38f0227530a1772735 100644 +--- a/src/Plugin/QueueWorker/AmazonSesMailQueue.php ++++ b/src/Plugin/QueueWorker/AmazonSesMailQueue.php +@@ -29,7 +29,12 @@ class AmazonSesMailQueue extends QueueWorkerBase implements ContainerFactoryPlug + $plugin_definition + ); + +- $instance->setHandler($container->get('amazon_ses.handler')); ++ // Only set the handler if queueing is enabled to avoid an error when ++ // trying to run without config. ++ $enabled = \Drupal::config('amazon_ses.settings')->get('queue'); ++ if ($enabled) { ++ $instance->setHandler($container->get('amazon_ses.handler')); ++ } + + return $instance; + } diff --git a/PATCHES/core--drupal--3418098-php-mailer.patch b/PATCHES/core--drupal--3418098-php-mailer.patch new file mode 100644 index 000000000..68484412f --- /dev/null +++ b/PATCHES/core--drupal--3418098-php-mailer.patch @@ -0,0 +1,13 @@ +diff --git a/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php b/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php +index 09a56006a6..1767dc4875 100644 +--- a/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php ++++ b/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php +@@ -113,7 +113,7 @@ public function mail(array $message) { + $mail_body = preg_replace('@\r?\n@', $line_endings, $message['body']); + $mail_headers = $headers->toString(); + +- if (!$this->request->server->has('WINDIR') && !str_contains($this->request->server->get('SERVER_SOFTWARE'), 'Win32')) { ++ if (!$this->request->server->has('WINDIR') && !str_contains($this->request->server->get('SERVER_SOFTWARE', ''), 'Win32')) { + // On most non-Windows systems, the "-f" option to the sendmail command + // is used to set the Return-Path. There is no space between -f and + // the value of the return path. diff --git a/composer.json b/composer.json index 5cf4736dd..fecf66590 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,8 @@ "cweagans/composer-patches": "^1.7", "drupal/admin_denied": "^2.0", "drupal/allowed_formats": "^3", + "drupal/amazon_ses": "^3", + "drupal/aws": "dev-2.0.x", "drupal/classy": "^1.0", "drupal/components": "^3.0@beta", "drupal/config_filter": "^2.2", diff --git a/composer.lock b/composer.lock index 530352bae..141a7455c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5afd17b8f5fbd8e8a5fc388ff5b478a5", + "content-hash": "984e75ef3a54068236165e345f1197b5", "packages": [ { "name": "asm89/stack-cors", @@ -64,16 +64,16 @@ }, { "name": "aws/aws-crt-php", - "version": "v1.2.4", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2" + "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/eb0c6e4e142224a10b08f49ebf87f32611d162b2", - "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/0ea1f04ec5aa9f049f97e012d1ed63b76834a31b", + "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b", "shasum": "" }, "require": { @@ -112,22 +112,22 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.4" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.5" }, - "time": "2023-11-08T00:42:13+00:00" + "time": "2024-04-19T21:30:56+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.304.2", + "version": "3.305.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "2435079c3e1a08148d955de15ec090018114f35a" + "reference": "c553a07fab74348517e72a0ccc02a612cbf4688b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2435079c3e1a08148d955de15ec090018114f35a", - "reference": "2435079c3e1a08148d955de15ec090018114f35a", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c553a07fab74348517e72a0ccc02a612cbf4688b", + "reference": "c553a07fab74348517e72a0ccc02a612cbf4688b", "shasum": "" }, "require": { @@ -207,9 +207,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.304.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.305.2" }, - "time": "2024-04-10T18:05:32+00:00" + "time": "2024-04-24T18:07:47+00:00" }, { "name": "behat/mink", @@ -417,16 +417,16 @@ }, { "name": "chi-teck/drupal-code-generator", - "version": "3.4.0", + "version": "3.5.0", "source": { "type": "git", "url": "https://github.com/Chi-teck/drupal-code-generator.git", - "reference": "b8136b945dc3446894e6cd2b2f055e147140faed" + "reference": "74c2dc687e124bfc4001e73e9346b33067e2ec2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Chi-teck/drupal-code-generator/zipball/b8136b945dc3446894e6cd2b2f055e147140faed", - "reference": "b8136b945dc3446894e6cd2b2f055e147140faed", + "url": "https://api.github.com/repos/Chi-teck/drupal-code-generator/zipball/74c2dc687e124bfc4001e73e9346b33067e2ec2b", + "reference": "74c2dc687e124bfc4001e73e9346b33067e2ec2b", "shasum": "" }, "require": { @@ -444,7 +444,7 @@ "squizlabs/php_codesniffer": "<3.6" }, "require-dev": { - "chi-teck/drupal-coder-extension": "^2.0.0-beta2", + "chi-teck/drupal-coder-extension": "^2.0.0-beta3", "drupal/coder": "8.3.23", "drupal/core": "10.3.x-dev", "ext-simplexml": "*", @@ -471,9 +471,9 @@ "description": "Drupal code generator", "support": { "issues": "https://github.com/Chi-teck/drupal-code-generator/issues", - "source": "https://github.com/Chi-teck/drupal-code-generator/tree/3.4.0" + "source": "https://github.com/Chi-teck/drupal-code-generator/tree/3.5.0" }, - "time": "2024-03-10T13:35:00+00:00" + "time": "2024-04-11T11:23:44+00:00" }, { "name": "colinodell/psr-testlogger", @@ -705,16 +705,16 @@ }, { "name": "composer/composer", - "version": "2.7.2", + "version": "2.7.4", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "b826edb791571ab1eaf281eb1bd6e181a1192adc" + "reference": "a625e50598e12171d3f60b1149eb530690c43474" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/b826edb791571ab1eaf281eb1bd6e181a1192adc", - "reference": "b826edb791571ab1eaf281eb1bd6e181a1192adc", + "url": "https://api.github.com/repos/composer/composer/zipball/a625e50598e12171d3f60b1149eb530690c43474", + "reference": "a625e50598e12171d3f60b1149eb530690c43474", "shasum": "" }, "require": { @@ -799,7 +799,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.2" + "source": "https://github.com/composer/composer/tree/2.7.4" }, "funding": [ { @@ -815,7 +815,7 @@ "type": "tidelift" } ], - "time": "2024-03-11T16:12:18+00:00" + "time": "2024-04-22T19:17:03+00:00" }, { "name": "composer/installers", @@ -2425,6 +2425,122 @@ "issues": "https://www.drupal.org/project/issues/allowed_formats" } }, + { + "name": "drupal/amazon_ses", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/amazon_ses.git", + "reference": "3.0.1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/amazon_ses-3.0.1.zip", + "reference": "3.0.1", + "shasum": "2dbb10219056e21f0bb60d37df5b459caea635e2" + }, + "require": { + "aws/aws-sdk-php": "^3.54", + "drupal/aws": "^2.0", + "drupal/core": "^9.1 || ^10" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "3.0.1", + "datestamp": "1671308264", + "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": "Ben Davis (davisben)", + "homepage": "https://www.drupal.org/u/davisben", + "role": "Maintainer" + }, + { + "name": "Ryan Palmer", + "homepage": "https://www.drupal.org/user/44161" + }, + { + "name": "tkuldeep17", + "homepage": "https://www.drupal.org/user/2498278" + } + ], + "description": "Allows site email to be sent using Amazon SES.", + "homepage": "http://drupal.org/project/amazon_ses", + "support": { + "source": "https://git.drupalcode.org/project/amazon_ses", + "issues": "http://drupal.org/project/issues/amazon_ses" + } + }, + { + "name": "drupal/aws", + "version": "dev-2.0.x", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/aws.git", + "reference": "5c8371feefd3ea641cca486925e3db9e628b07ba" + }, + "require": { + "aws/aws-sdk-php": "^3.54", + "drupal/core": "^9.2 || ^10" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-2.0.x": "2.0.x-dev" + }, + "drupal": { + "version": "2.0.3+4-dev", + "datestamp": "1690752016", + "security-coverage": { + "status": "not-covered", + "message": "Dev releases are not covered by Drupal security advisories." + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Ben Davis (davisben)", + "homepage": "https://www.drupal.org/u/davisben", + "role": "Maintainer" + }, + { + "name": "dragonwize", + "homepage": "https://www.drupal.org/user/137882" + }, + { + "name": "mpriscella", + "homepage": "https://www.drupal.org/user/2354820" + }, + { + "name": "recidive", + "homepage": "https://www.drupal.org/user/12564" + }, + { + "name": "yas", + "homepage": "https://www.drupal.org/user/17536" + } + ], + "description": "Provides a unified AWS profile management system.", + "homepage": "http://drupal.org/project/aws", + "support": { + "source": "https://git.drupalcode.org/project/aws", + "issues": "https://drupal.org/project/issues/aws" + } + }, { "name": "drupal/classy", "version": "1.0.2", @@ -2481,16 +2597,16 @@ }, { "name": "drupal/coder", - "version": "8.3.23", + "version": "8.3.24", "source": { "type": "git", "url": "https://github.com/pfrenssen/coder.git", - "reference": "1a1613d83c08dac5be593f2775c9eccae1b41805" + "reference": "1a59890f972db5da091354f0191dec1037f7c582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pfrenssen/coder/zipball/1a1613d83c08dac5be593f2775c9eccae1b41805", - "reference": "1a1613d83c08dac5be593f2775c9eccae1b41805", + "url": "https://api.github.com/repos/pfrenssen/coder/zipball/1a59890f972db5da091354f0191dec1037f7c582", + "reference": "1a59890f972db5da091354f0191dec1037f7c582", "shasum": "" }, "require": { @@ -2499,7 +2615,7 @@ "php": ">=7.2", "sirbrillig/phpcs-variable-analysis": "^2.11.7", "slevomat/coding-standard": "^8.11", - "squizlabs/php_codesniffer": "^3.7.1", + "squizlabs/php_codesniffer": "^3.9.1", "symfony/yaml": ">=3.4.0" }, "require-dev": { @@ -2528,7 +2644,7 @@ "issues": "https://www.drupal.org/project/issues/coder", "source": "https://www.drupal.org/project/coder" }, - "time": "2024-01-27T18:13:12+00:00" + "time": "2024-04-21T06:13:24+00:00" }, { "name": "drupal/components", @@ -3838,7 +3954,7 @@ "shasum": "fc8ea60619b6b4682bade340e13fb4565d3a7e0c" }, "require": { - "drupal/core": "^10" + "drupal/core": "^9.1 || ^10" }, "type": "drupal-module", "extra": { @@ -4037,7 +4153,7 @@ "shasum": "c25246747dac4372c7d5a5a5fd0f276d9e468eff" }, "require": { - "drupal/core": "^10", + "drupal/core": "^9.3 || ^10", "drupal/mailsystem": "^4" }, "require-dev": { @@ -4740,7 +4856,7 @@ "shasum": "77906ae731878b68a181f82b073617b798e5f110" }, "require": { - "drupal/core": "^10", + "drupal/core": "^9.3 || ^10", "enshrined/svg-sanitize": ">=0.15 <1.0" }, "type": "drupal-module", @@ -4888,7 +5004,7 @@ "shasum": "9ea9eee91cf75f21fcc939704baa6a7ec10d7748" }, "require": { - "drupal/core": "^10" + "drupal/core": "^8.9 || ^9 || ^10" }, "type": "drupal-module", "extra": { @@ -4927,17 +5043,17 @@ }, { "name": "drupal/token", - "version": "1.13.0", + "version": "1.14.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/token.git", - "reference": "8.x-1.13" + "reference": "8.x-1.14" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/token-8.x-1.13.zip", - "reference": "8.x-1.13", - "shasum": "f2a074b51726de3727c1d900237d6d471806a4d2" + "url": "https://ftp.drupal.org/files/projects/token-8.x-1.14.zip", + "reference": "8.x-1.14", + "shasum": "df3cae709fcc1a99ac1111ce67a0d6af56d287d7" }, "require": { "drupal/core": "^9.2 || ^10" @@ -4945,8 +5061,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.13", - "datestamp": "1697885927", + "version": "8.x-1.14", + "datestamp": "1713009399", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -5923,24 +6039,24 @@ }, { "name": "grasmash/yaml-cli", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/grasmash/yaml-cli.git", - "reference": "00f3fd775f6abbfacd44432f1999c3c3b02791f0" + "reference": "a5af7c16a0b98fca7d06e85ba517d526e1ba21de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/grasmash/yaml-cli/zipball/00f3fd775f6abbfacd44432f1999c3c3b02791f0", - "reference": "00f3fd775f6abbfacd44432f1999c3c3b02791f0", + "url": "https://api.github.com/repos/grasmash/yaml-cli/zipball/a5af7c16a0b98fca7d06e85ba517d526e1ba21de", + "reference": "a5af7c16a0b98fca7d06e85ba517d526e1ba21de", "shasum": "" }, "require": { "dflydev/dot-access-data": "^3", "php": ">=8.0", - "symfony/console": "^6", - "symfony/filesystem": "^6", - "symfony/yaml": "^6" + "symfony/console": "^6 || ^7", + "symfony/filesystem": "^6 || ^7", + "symfony/yaml": "^6 || ^7" }, "require-dev": { "php-coveralls/php-coveralls": "^2", @@ -5973,9 +6089,9 @@ "description": "A command line tool for reading and manipulating yaml files.", "support": { "issues": "https://github.com/grasmash/yaml-cli/issues", - "source": "https://github.com/grasmash/yaml-cli/tree/3.1.0" + "source": "https://github.com/grasmash/yaml-cli/tree/3.2.0" }, - "time": "2022-05-09T20:22:34+00:00" + "time": "2024-04-17T16:23:04+00:00" }, { "name": "grpc/grpc", @@ -8013,20 +8129,20 @@ }, { "name": "open-telemetry/sem-conv", - "version": "1.24.0", + "version": "1.25.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "d03e6501d21c04cd1b1e66e4cbcc7c2dd2e2cfa3" + "reference": "23f457ba390847647a17068e0095d9ffe9a4824c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/d03e6501d21c04cd1b1e66e4cbcc7c2dd2e2cfa3", - "reference": "d03e6501d21c04cd1b1e66e4cbcc7c2dd2e2cfa3", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/23f457ba390847647a17068e0095d9ffe9a4824c", + "reference": "23f457ba390847647a17068e0095d9ffe9a4824c", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": "^8.1" }, "type": "library", "extra": { @@ -8066,7 +8182,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-01-23T21:47:17+00:00" + "time": "2024-04-09T23:31:35+00:00" }, { "name": "openai-php/client", @@ -9245,28 +9361,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", + "version": "5.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + "reference": "298d2febfe79d03fe714eb871d5538da55205b1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/298d2febfe79d03fe714eb871d5538da55205b1a", + "reference": "298d2febfe79d03fe714eb871d5538da55205b1a", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.1", "ext-filter": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" + "mockery/mockery": "~1.3.5", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^5.13" }, "type": "library", "extra": { @@ -9290,15 +9413,15 @@ }, { "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.0" }, - "time": "2021-10-19T17:43:47+00:00" + "time": "2024-04-09T21:13:58+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -9624,16 +9747,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.66", + "version": "1.10.67", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "94779c987e4ebd620025d9e5fdd23323903950bd" + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/94779c987e4ebd620025d9e5fdd23323903950bd", - "reference": "94779c987e4ebd620025d9e5fdd23323903950bd", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", "shasum": "" }, "require": { @@ -9676,13 +9799,9 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2024-03-28T16:17:31+00:00" + "time": "2024-04-16T07:22:02+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -12064,16 +12183,16 @@ }, { "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.17", + "version": "v2.11.18", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049" + "reference": "ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/3b71162a6bf0cde2bff1752e40a1788d8273d049", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0", + "reference": "ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0", "shasum": "" }, "require": { @@ -12118,7 +12237,7 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2023-08-05T23:46:11+00:00" + "time": "2024-04-13T16:42:46+00:00" }, { "name": "slevomat/coding-standard", @@ -12187,16 +12306,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.1", + "version": "3.9.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909" + "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/267a4405fff1d9c847134db3a3c92f1ab7f77909", - "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/aac1f6f347a5c5ac6bc98ad395007df00990f480", + "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480", "shasum": "" }, "require": { @@ -12263,7 +12382,7 @@ "type": "open_collective" } ], - "time": "2024-03-31T21:03:09+00:00" + "time": "2024-04-23T20:25:34+00:00" }, { "name": "symfony/browser-kit", @@ -15880,16 +15999,16 @@ }, { "name": "unocha/ocha_monitoring", - "version": "1.0.14", + "version": "1.0.15", "source": { "type": "git", "url": "https://github.com/UN-OCHA/ocha_monitoring.git", - "reference": "c105d4199bc86049f2b0589e8cf0b08aff95dd63" + "reference": "6a59f1f612d26c07ec5d84f3b29b94240a43d2d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/UN-OCHA/ocha_monitoring/zipball/c105d4199bc86049f2b0589e8cf0b08aff95dd63", - "reference": "c105d4199bc86049f2b0589e8cf0b08aff95dd63", + "url": "https://api.github.com/repos/UN-OCHA/ocha_monitoring/zipball/6a59f1f612d26c07ec5d84f3b29b94240a43d2d6", + "reference": "6a59f1f612d26c07ec5d84f3b29b94240a43d2d6", "shasum": "" }, "require": { @@ -15904,9 +16023,9 @@ "description": "UNOCHA Monitoring", "support": { "issues": "https://github.com/UN-OCHA/ocha_monitoring/issues", - "source": "https://github.com/UN-OCHA/ocha_monitoring/tree/1.0.14" + "source": "https://github.com/UN-OCHA/ocha_monitoring/tree/1.0.15" }, - "time": "2024-02-09T10:23:16+00:00" + "time": "2024-04-22T09:26:45+00:00" }, { "name": "webflo/drupal-finder", @@ -16014,16 +16133,16 @@ "packages-dev": [ { "name": "doctrine/common", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced" + "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced", + "url": "https://api.github.com/repos/doctrine/common/zipball/0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", + "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", "shasum": "" }, "require": { @@ -16085,7 +16204,7 @@ ], "support": { "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.4.3" + "source": "https://github.com/doctrine/common/tree/3.4.4" }, "funding": [ { @@ -16101,7 +16220,7 @@ "type": "tidelift" } ], - "time": "2022-10-09T11:47:59+00:00" + "time": "2024-04-16T13:35:33+00:00" }, { "name": "doctrine/event-manager", @@ -16799,6 +16918,7 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { + "drupal/aws": 20, "drupal/components": 10, "drupal/config_split": 5, "drupal/imageapi_optimize_binaries": 10, diff --git a/composer.patches.json b/composer.patches.json index 3e258fd2f..ad32ed1aa 100644 --- a/composer.patches.json +++ b/composer.patches.json @@ -1,9 +1,13 @@ { "patches": { + "drupal/amazon_ses": { + "Cron fails on amazon_ses when not configured": "PATCHES/amazon_ses-3417090-cron-queue-14.patch" + }, "drupal/core" : { "https://www.drupal.org/project/drupal/issues/3047110": "PATCHES/core--drupal--3047110-taxonomy-term-moderation-class.patch", "https://www.drupal.org/project/drupal/issues/3008292": "PATCHES/core--drupal--3008292-image-upload-validators.patch", - "https://www.drupal.org/project/drupal/issues/3143617": "PATCHES/core--drupal--3143617-pager-parameter.patch" + "https://www.drupal.org/project/drupal/issues/3143617": "PATCHES/core--drupal--3143617-pager-parameter.patch", + "https://www.drupal.org/project/drupal/issues/3418098": "PATCHES/core--drupal--3418098-php-mailer.patch" }, "drupal/guidelines": { "Drupal 10 compatibility": "PATCHES/guidelines-drupal-10-compatibility.patch" diff --git a/config/amazon_ses.settings.yml b/config/amazon_ses.settings.yml new file mode 100644 index 000000000..71871fca8 --- /dev/null +++ b/config/amazon_ses.settings.yml @@ -0,0 +1,3 @@ +from_address: ops@reliefweb.int +throttle: true +queue: false diff --git a/config/aws.profile.amazon_ses.yml b/config/aws.profile.amazon_ses.yml new file mode 100644 index 000000000..0b0a2668a --- /dev/null +++ b/config/aws.profile.amazon_ses.yml @@ -0,0 +1,12 @@ +uuid: 5e4ba0be-7f26-41ac-aa7d-b55feb6afce9 +langcode: en +status: true +dependencies: { } +id: amazon_ses +name: 'Amazon SES' +default: 1 +aws_role_arn: '' +aws_access_key_id: '' +aws_secret_access_key: '' +region: us-east-1 +encryption_profile: '' diff --git a/config/aws.settings.yml b/config/aws.settings.yml new file mode 100644 index 000000000..2a115ac36 --- /dev/null +++ b/config/aws.settings.yml @@ -0,0 +1 @@ +services: { } diff --git a/config/core.extension.yml b/config/core.extension.yml index af50f6ee4..e08eeb4f9 100644 --- a/config/core.extension.yml +++ b/config/core.extension.yml @@ -3,6 +3,8 @@ _core: module: admin_denied: 0 allowed_formats: 0 + amazon_ses: 0 + aws: 0 block: 0 book: 0 breakpoint: 0 diff --git a/config/reliefweb_subscriptions.settings.yml b/config/reliefweb_subscriptions.settings.yml new file mode 100644 index 000000000..7cc622886 --- /dev/null +++ b/config/reliefweb_subscriptions.settings.yml @@ -0,0 +1,2 @@ +encryption_method: 'aes-128-ctr' +encryption_key: diff --git a/docker/etc/nginx/apps/drupal/drupal.conf b/docker/etc/nginx/apps/drupal/drupal.conf index c139d2d17..f601604d9 100644 --- a/docker/etc/nginx/apps/drupal/drupal.conf +++ b/docker/etc/nginx/apps/drupal/drupal.conf @@ -56,41 +56,16 @@ location / { } ## Trying to access private files directly returns a 404. - location ^~ /sites/.*/private/ { + location ~ "^/sites/.*/private/" { internal; } - ## Location for public files. Avoid hitting Drupal on thye *production* env - ## if a file exists, but do pass it on otherwise, so stage_file_proxy can - ## fetch a file from production if needed. - location ^~ /sites/.*/files/ { - access_log off; - expires 30d; - ## No need to bleed constant updates. Send the all shebang in one - ## fell swoop. - tcp_nodelay off; - - ## Set the OS file cache. - open_file_cache max=3000 inactive=120s; - open_file_cache_valid 45s; - open_file_cache_min_uses 2; - open_file_cache_errors off; - - ## Location for aggregated css and js files under D 10.1. See: - ## https://www.drupal.org/node/2888767#nginx-php-fpm - location ^~ /sites/.*/files/(css|js)/ { - # Hit the original file *or* allow Drupal to aggregate. - try_files $uri @drupal; - } - - ## Serve the file directly and fall back to drupal in case stage_file_proxy is needed. - try_files $uri @drupal-stage-file-proxy; - } - ## Location for public derivative images to avoid hitting Drupal for invalid ## image derivative paths or if the source image doesn't exist. - location ^~ /sites/.*/files/styles/ { - + ## + ## Note: this needs to be before the public files regex because nginx stops + ## at the first found matching regex location. + location ~ "^/sites/.*/files/styles/" { ## Valid public derivative image paths. ## We store the source image path without the extra `.webp` extension ## present in the derivative so that we can check if the source image @@ -120,9 +95,36 @@ location / { return 404; } + ## Location for public files. Avoid hitting Drupal on the *production* env + ## if a file exists, but do pass it on otherwise, so stage_file_proxy can + ## fetch a file from production if needed. + location ~ "^/sites/.*/files/" { + access_log off; + expires 30d; + ## No need to bleed constant updates. Send the all shebang in one + ## fell swoop. + tcp_nodelay off; + + ## Set the OS file cache. + open_file_cache max=3000 inactive=120s; + open_file_cache_valid 45s; + open_file_cache_min_uses 2; + open_file_cache_errors off; + + ## Location for aggregated css and js files under D 10.1. See: + ## https://www.drupal.org/node/2888767#nginx-php-fpm + location ~ "^/sites/.*/files/(css|js)/" { + # Hit the original file *or* allow Drupal to aggregate. + try_files $uri @drupal; + } + + ## Serve the file directly and fall back to drupal in case + ## stage_file_proxy is needed. + try_files $uri @drupal-stage-file-proxy; + } + ## All static files will be served directly. location ~* ^.+\.(?:cur|htc|ico|html|otf|ttf|eot|svg)$ { - access_log off; expires 30d; ## No need to bleed constant updates. Send the all shebang in one diff --git a/docker/etc/nginx/custom/04_unsubscribe.conf b/docker/etc/nginx/custom/04_unsubscribe.conf new file mode 100644 index 000000000..24ab845d7 --- /dev/null +++ b/docker/etc/nginx/custom/04_unsubscribe.conf @@ -0,0 +1,32 @@ +## Handle unsubscribe links. +location ^~ /notifications/unsubscribe/ { + + ## One click unsubscribe links. + location ~ "^(?/notifications/unsubscribe/[^/]+)$" { + error_page 418 = @one-click-unsubscribe-no-cookies; + + ## Pass the request to Drupal but with the cookie header removed. + if ($request_method = POST) { + return 418; + } + + ## Pass the request to Drupal. + try_files /dev/null @drupal; + } + + ## Pass the request to Drupal. + try_files /dev/null @drupal; +} + +## Similar to @drupal but with the cookie headers removed. +location @one-click-unsubscribe-no-cookies { + ## Remove the Cookie header from the request. + more_clear_input_headers "Cookie"; + + ## Include the FastCGI config. + include apps/drupal/fastcgi_drupal.conf; + fastcgi_pass phpcgi; + + ## Remove the Set-Cookie header from the response. + fastcgi_hide_header 'Set-Cookie'; +} diff --git a/html/modules/custom/reliefweb_api/README.md b/html/modules/custom/reliefweb_api/README.md index 4add34719..c7e1aaa28 100644 --- a/html/modules/custom/reliefweb_api/README.md +++ b/html/modules/custom/reliefweb_api/README.md @@ -21,7 +21,7 @@ Creating, updating or deleting a taxonomy term clears all the cached queries bec This modules also provides a set of [drush commands](src/Commands/ReliefWebApiCommands.php) to allow (re-)indexing content. -Ex `drush rapi-i --limit 100 reports` will re-index the 100 most recent reports. +Ex `drush rapi-i --limit 100 report` will re-index the 100 most recent reports. ## Settings diff --git a/html/modules/custom/reliefweb_api/src/Commands/ReliefWebApiCommands.php b/html/modules/custom/reliefweb_api/src/Commands/ReliefWebApiCommands.php index 3992e26af..d198c0f56 100644 --- a/html/modules/custom/reliefweb_api/src/Commands/ReliefWebApiCommands.php +++ b/html/modules/custom/reliefweb_api/src/Commands/ReliefWebApiCommands.php @@ -60,7 +60,7 @@ public function __construct( EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, - StateInterface $state + StateInterface $state, ) { $this->apiConfig = $config_factory->get('reliefweb_api.settings'); $this->entityFieldManager = $entity_field_manager; @@ -136,26 +136,29 @@ public function __construct( * * @aliases rw-api:index, rw-api:i, rwapi-i, rapi-i */ - public function index($bundle = '', array $options = [ - 'elasticsearch' => '', - 'base-index-name' => '', - 'website' => '', - 'limit' => 0, - 'offset' => 0, - 'filter' => '', - 'chunk-size' => 500, - 'tag' => '', - 'id' => 0, - 'remove' => FALSE, - 'replace' => FALSE, - 'alias' => FALSE, - 'alias-only' => FALSE, - 'log' => 'echo', - 'count-only' => FALSE, - 'memory-limit' => '512M', - 'replicas' => NULL, - 'shards' => NULL, - ]) { + public function index( + $bundle = '', + array $options = [ + 'elasticsearch' => '', + 'base-index-name' => '', + 'website' => '', + 'limit' => 0, + 'offset' => 0, + 'filter' => '', + 'chunk-size' => 500, + 'tag' => '', + 'id' => 0, + 'remove' => FALSE, + 'replace' => FALSE, + 'alias' => FALSE, + 'alias-only' => FALSE, + 'log' => 'echo', + 'count-only' => FALSE, + 'memory-limit' => '512M', + 'replicas' => NULL, + 'shards' => NULL, + ], + ) { // Index all the references at once when the special 'references' bundle // is passed to the command. if ($bundle === 'references') { @@ -274,10 +277,15 @@ public function index($bundle = '', array $options = [ * * @aliases rw-api:replace, rw-api:r, rwapi-r */ - public function replace($bundle, $newtag, $oldtag = NULL, array $options = [ - 'elasticsearch' => '', - 'base-index-name' => '', - ]) { + public function replace( + $bundle, + $newtag, + $oldtag = NULL, + array $options = [ + 'elasticsearch' => '', + 'base-index-name' => '', + ], + ) { if (!isset($oldtag)) { $oldtag = $this->state->get('reliefweb_api_index_tag_' . $bundle, ''); } diff --git a/html/modules/custom/reliefweb_entities/src/EntityFormAlterServiceBase.php b/html/modules/custom/reliefweb_entities/src/EntityFormAlterServiceBase.php index 4a699204b..f006e7ffd 100644 --- a/html/modules/custom/reliefweb_entities/src/EntityFormAlterServiceBase.php +++ b/html/modules/custom/reliefweb_entities/src/EntityFormAlterServiceBase.php @@ -91,7 +91,7 @@ public function __construct( EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, StateInterface $state, - TranslationInterface $string_translation + TranslationInterface $string_translation, ) { $this->database = $database; $this->currentUser = $current_user; diff --git a/html/modules/custom/reliefweb_entities/src/Plugin/EntityReferenceSelection/AnyTermSelection.php b/html/modules/custom/reliefweb_entities/src/Plugin/EntityReferenceSelection/AnyTermSelection.php index 0039887bc..5f868b40d 100644 --- a/html/modules/custom/reliefweb_entities/src/Plugin/EntityReferenceSelection/AnyTermSelection.php +++ b/html/modules/custom/reliefweb_entities/src/Plugin/EntityReferenceSelection/AnyTermSelection.php @@ -96,7 +96,7 @@ public function __construct( EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, EntityRepositoryInterface $entity_repository, Connection $database, - LanguageManagerInterface $language_manager + LanguageManagerInterface $language_manager, ) { parent::__construct( $configuration, diff --git a/html/modules/custom/reliefweb_files/src/Controller/FileDownloadController.php b/html/modules/custom/reliefweb_files/src/Controller/FileDownloadController.php index cef62345d..67d194f42 100644 --- a/html/modules/custom/reliefweb_files/src/Controller/FileDownloadController.php +++ b/html/modules/custom/reliefweb_files/src/Controller/FileDownloadController.php @@ -81,7 +81,7 @@ public function __construct( DocstoreClient $docstore_client, EntityTypeManagerInterface $entity_type_manager, LoggerChannelFactoryInterface $logger_factory, - StreamWrapperManagerInterface $stream_wrapper_manager + StreamWrapperManagerInterface $stream_wrapper_manager, ) { parent::__construct($stream_wrapper_manager); diff --git a/html/modules/custom/reliefweb_files/src/Plugin/Field/FieldWidget/ReliefWebFile.php b/html/modules/custom/reliefweb_files/src/Plugin/Field/FieldWidget/ReliefWebFile.php index 64b724b69..d869911dc 100644 --- a/html/modules/custom/reliefweb_files/src/Plugin/Field/FieldWidget/ReliefWebFile.php +++ b/html/modules/custom/reliefweb_files/src/Plugin/Field/FieldWidget/ReliefWebFile.php @@ -91,7 +91,7 @@ public function __construct( LoggerChannelFactoryInterface $logger_factory, RendererInterface $renderer, RequestStack $request_stack, - FileValidatorInterface $file_validator + FileValidatorInterface $file_validator, ) { parent::__construct( $plugin_id, @@ -114,7 +114,7 @@ public static function create( ContainerInterface $container, array $configuration, $plugin_id, - $plugin_definition + $plugin_definition, ) { return new static( $plugin_id, diff --git a/html/modules/custom/reliefweb_files/src/Services/DocstoreClient.php b/html/modules/custom/reliefweb_files/src/Services/DocstoreClient.php index 91f51d044..9f7b8e8ed 100644 --- a/html/modules/custom/reliefweb_files/src/Services/DocstoreClient.php +++ b/html/modules/custom/reliefweb_files/src/Services/DocstoreClient.php @@ -49,7 +49,7 @@ class DocstoreClient { public function __construct( ConfigFactoryInterface $config_factory, ClientInterface $http_client, - LoggerChannelFactoryInterface $logger_factory + LoggerChannelFactoryInterface $logger_factory, ) { $this->config = $config_factory->get('reliefweb_files.settings'); $this->httpClient = $http_client; diff --git a/html/modules/custom/reliefweb_guidelines/src/Controller/GuidelineSinglePageController.php b/html/modules/custom/reliefweb_guidelines/src/Controller/GuidelineSinglePageController.php index 81d957b3a..af9c87a64 100644 --- a/html/modules/custom/reliefweb_guidelines/src/Controller/GuidelineSinglePageController.php +++ b/html/modules/custom/reliefweb_guidelines/src/Controller/GuidelineSinglePageController.php @@ -49,7 +49,7 @@ class GuidelineSinglePageController extends ControllerBase { public function __construct( CacheBackendInterface $cache_backend, AccountProxyInterface $current_user, - EntityTypeManagerInterface $entity_type_manager + EntityTypeManagerInterface $entity_type_manager, ) { $this->cache = $cache_backend; $this->currentUser = $current_user; diff --git a/html/modules/custom/reliefweb_guidelines/src/Form/GuidelineForm.php b/html/modules/custom/reliefweb_guidelines/src/Form/GuidelineForm.php index 8cf90c29b..f3ce01c0a 100644 --- a/html/modules/custom/reliefweb_guidelines/src/Form/GuidelineForm.php +++ b/html/modules/custom/reliefweb_guidelines/src/Form/GuidelineForm.php @@ -42,7 +42,7 @@ public function __construct( EntityTypeBundleInfoInterface $entity_type_bundle_info, TimeInterface $time, PrivateTempStoreFactory $temp_store_factory, - AccountProxyInterface $current_user + AccountProxyInterface $current_user, ) { parent::__construct($entity_repository, $entity_type_bundle_info, $time); $this->tempStoreFactory = $temp_store_factory; diff --git a/html/modules/custom/reliefweb_guidelines/src/Form/GuidelineListSortForm.php b/html/modules/custom/reliefweb_guidelines/src/Form/GuidelineListSortForm.php index 65847c21e..6f10539b0 100644 --- a/html/modules/custom/reliefweb_guidelines/src/Form/GuidelineListSortForm.php +++ b/html/modules/custom/reliefweb_guidelines/src/Form/GuidelineListSortForm.php @@ -25,7 +25,7 @@ class GuidelineListSortForm extends FormBase { * {@inheritdoc} */ public function __construct( - EntityTypeManagerInterface $entity_type_manager + EntityTypeManagerInterface $entity_type_manager, ) { $this->entityTypeManager = $entity_type_manager; } diff --git a/html/modules/custom/reliefweb_guidelines/src/Plugin/Field/FieldFormatter/GuidelineFieldTargetDefaultFormatter.php b/html/modules/custom/reliefweb_guidelines/src/Plugin/Field/FieldFormatter/GuidelineFieldTargetDefaultFormatter.php index a735b0571..c7cbc3fb1 100644 --- a/html/modules/custom/reliefweb_guidelines/src/Plugin/Field/FieldFormatter/GuidelineFieldTargetDefaultFormatter.php +++ b/html/modules/custom/reliefweb_guidelines/src/Plugin/Field/FieldFormatter/GuidelineFieldTargetDefaultFormatter.php @@ -80,7 +80,7 @@ public function __construct( array $third_party_settings, EntityFieldManagerInterface $entity_field_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, - EntityTypeManagerInterface $entity_type_manager + EntityTypeManagerInterface $entity_type_manager, ) { parent::__construct( $plugin_id, diff --git a/html/modules/custom/reliefweb_homepage/src/Controller/Homepage.php b/html/modules/custom/reliefweb_homepage/src/Controller/Homepage.php index ac0c9f2ba..2afd4a86f 100644 --- a/html/modules/custom/reliefweb_homepage/src/Controller/Homepage.php +++ b/html/modules/custom/reliefweb_homepage/src/Controller/Homepage.php @@ -74,7 +74,7 @@ public function __construct( EntityTypeManagerInterface $entity_type_manager, ReliefWebApiClient $reliefweb_api_client, RendererInterface $renderer, - StateInterface $state + StateInterface $state, ) { $this->currentUser = $current_user; $this->entityTypeManager = $entity_type_manager; diff --git a/html/modules/custom/reliefweb_import/src/Command/ReliefwebImportCommand.php b/html/modules/custom/reliefweb_import/src/Command/ReliefwebImportCommand.php index 567c7ee48..4ed156d9b 100644 --- a/html/modules/custom/reliefweb_import/src/Command/ReliefwebImportCommand.php +++ b/html/modules/custom/reliefweb_import/src/Command/ReliefwebImportCommand.php @@ -100,7 +100,7 @@ public function __construct( $account_switcher, ClientInterface $http_client, LoggerChannelFactoryInterface $logger_factory, - StateInterface $state + StateInterface $state, ) { $this->database = $database; $this->entityTypeManager = $entity_type_manager; diff --git a/html/modules/custom/reliefweb_job_tagger/reliefweb_job_tagger.module b/html/modules/custom/reliefweb_job_tagger/reliefweb_job_tagger.module index 1795da5e0..a62229f3e 100644 --- a/html/modules/custom/reliefweb_job_tagger/reliefweb_job_tagger.module +++ b/html/modules/custom/reliefweb_job_tagger/reliefweb_job_tagger.module @@ -220,6 +220,8 @@ function reliefweb_job_tagger_node_presave(EntityInterface $node) { // Skip queued and processed nodes. if ($node->hasField('reliefweb_job_tagger_status')) { + reliefweb_job_tagger_log_manual_changes_to_tagging($node); + if ($node->reliefweb_job_tagger_status->value == 'queued' || $node->reliefweb_job_tagger_status->value == 'processed') { return; } @@ -259,6 +261,51 @@ function reliefweb_job_tagger_node_presave(EntityInterface $node) { } } +/** + * Check for manual changes to the AI tagging and log them. + * + * @param \Drupal\Entity\EntityInterface $entity + * Changed entity. + */ +function reliefweb_job_tagger_log_manual_changes_to_tagging(EntityInterface $entity) { + if (!isset($entity->original) || !$entity->original->hasField('reliefweb_job_tagger_status')) { + return; + } + $original = $entity->original; + + // Skip if the original was not processed by the AI. + if ($original->reliefweb_job_tagger_status->value !== 'processed') { + return; + } + + $editor = UserHelper::userHasRoles(['editor']); + $logger = \Drupal::logger('reliefweb_job_tagger'); + + $fields = [ + 'field_theme', + 'field_career_categories', + ]; + + foreach ($fields as $field) { + if ($entity->hasField($field) && !$entity->get($field)->equals($original->get($field))) { + $old = []; + $new = []; + foreach ($original->get($field) as $item) { + $old[] = $item->entity?->label() ?? $item->target_id; + } + foreach ($entity->get($field) as $item) { + $new[] = $item->entity?->label() ?? $item->target_id; + } + $logger->info(strtr('Manual change to the @field tagging by @user: @old --> @new', [ + '@field' => $field, + '@user' => $editor ? 'editor' : 'non editor', + '@old' => implode(', ', $old), + '@new' => implode(', ', $new), + ])); + } + } +} + /** * Implements hook_entity_after_save(). * diff --git a/html/modules/custom/reliefweb_moderation/src/Commands/ReliefWebModerationCommands.php b/html/modules/custom/reliefweb_moderation/src/Commands/ReliefWebModerationCommands.php index 43814b265..f54177bad 100644 --- a/html/modules/custom/reliefweb_moderation/src/Commands/ReliefWebModerationCommands.php +++ b/html/modules/custom/reliefweb_moderation/src/Commands/ReliefWebModerationCommands.php @@ -39,7 +39,7 @@ class ReliefWebModerationCommands extends DrushCommands { public function __construct( Connection $database, EntityTypeManagerInterface $entity_type_manager, - StateInterface $state + StateInterface $state, ) { $this->database = $database; $this->entityTypeManager = $entity_type_manager; @@ -72,9 +72,11 @@ public function __construct( * * @validate-module-enabled reliefweb_moderation */ - public function updateInactiveSources(array $options = [ - 'dry-run' => FALSE, - ]) { + public function updateInactiveSources( + array $options = [ + 'dry-run' => FALSE, + ], + ) { if (!empty($this->state->get('system.maintenance_mode', 0))) { $this->logger()->warning(dt('Maintenance mode, aborting.')); return TRUE; diff --git a/html/modules/custom/reliefweb_moderation/src/ModerationServiceBase.php b/html/modules/custom/reliefweb_moderation/src/ModerationServiceBase.php index e5bd7ed81..f056f74b4 100644 --- a/html/modules/custom/reliefweb_moderation/src/ModerationServiceBase.php +++ b/html/modules/custom/reliefweb_moderation/src/ModerationServiceBase.php @@ -140,7 +140,7 @@ public function __construct( PagerManagerInterface $pager_manager, PagerParametersInterface $pager_parameters, RequestStack $request_stack, - TranslationInterface $string_translation + TranslationInterface $string_translation, ) { $this->currentUser = $current_user; $this->database = $database; diff --git a/html/modules/custom/reliefweb_reporting/reliefweb_reporting.module b/html/modules/custom/reliefweb_reporting/reliefweb_reporting.module new file mode 100644 index 000000000..fdc142631 --- /dev/null +++ b/html/modules/custom/reliefweb_reporting/reliefweb_reporting.module @@ -0,0 +1,38 @@ + $value) { + $message['headers'][$key] = $value; + } + + // Ensure the Reply-To header is set and not duplicated due to different case. + $reply_to = $message['from']; + foreach (['reply-to', 'Reply-to', 'Reply-To'] as $key) { + if (isset($message['headers'][$key])) { + $reply_to = $message['headers'][$key]; + unset($message['headers'][$key]); + } + } + $message['headers']['Reply-To'] = $reply_to; + + // Set the attachments. + if (isset($params['attachments'])) { + $message['params']['attachments'] = $params['attachments']; + } + + $message['subject'] = $params['subject']; + $message['body'] = $params['body']; +} diff --git a/html/modules/custom/reliefweb_reporting/src/Commands/ReliefWebReportingCommands.php b/html/modules/custom/reliefweb_reporting/src/Commands/ReliefWebReportingCommands.php index 83fc89a82..c29427127 100644 --- a/html/modules/custom/reliefweb_reporting/src/Commands/ReliefWebReportingCommands.php +++ b/html/modules/custom/reliefweb_reporting/src/Commands/ReliefWebReportingCommands.php @@ -59,7 +59,7 @@ public function __construct( MailManagerInterface $mail_manager, LanguageDefault $language_default, LoggerChannelFactoryInterface $logger_factory, - StateInterface $state + StateInterface $state, ) { $this->configFactory = $config_factory; $this->database = $database; @@ -177,7 +177,8 @@ public function sendWeeklyJobStats($recipients) { $body .= ''; // Send the email. - if (mail($recipients, $subject, $body, $headers)) { + $message = $this->sendMail($from, $recipients, $subject, $body, $headers); + if (!empty($message['result'])) { $this->logger->info(dt('"@subject" sent to @recipients', [ '@subject' => $subject, '@recipients' => $recipients, @@ -243,7 +244,7 @@ public function sendWeeklyReportStats($recipients) { $headers = [ 'From' => $from, 'Reply-To' => $from, - 'Content-Type' => 'multipart/mixed; boundary=GvXjxJ+pjyke8COw', + 'Content-Type' => 'text/plain; charset=utf-8', 'Content-Disposition' => 'inline', 'MIME-Version' => '1.0', ]; @@ -276,6 +277,8 @@ public function sendWeeklyReportStats($recipients) { ORDER BY n.nid DESC ")->fetchAll(\PDO::FETCH_ASSOC); + $attachments = []; + if (empty($records)) { $body = 'No reports posted this week.'; } @@ -292,23 +295,172 @@ public function sendWeeklyReportStats($recipients) { $csv = trim(stream_get_contents($handle)); fclose($handle); - $body = implode("\n", [ - '--GvXjxJ+pjyke8COw', - 'Content-Type: text/html', - 'Content-Disposition: inline', - '', - "Attachment: $filename", - '', - '--GvXjxJ+pjyke8COw', - 'Content-Type: text/csv', - "Content-Disposition: attachment; filename=$filename", - '', - $csv, + $body = "Attachment: $filename"; + + $attachments[] = [ + 'filecontent' => $csv, + 'filename' => $filename, + 'filemime' => 'text/csv', + ]; + } + + // Send the email. + $message = $this->sendMail($from, $recipients, $subject, $body, $headers, $attachments); + if (!empty($message['result'])) { + $this->logger->info(dt('"@subject" sent to @recipients', [ + '@subject' => $subject, + '@recipients' => $recipients, + ])); + } + else { + $this->logger->error(dt('Unable to send "@subject" to @recipients', [ + '@subject' => $subject, + '@recipients' => $recipients, + ])); + } + + return TRUE; + } + + /** + * Send weekly statistics about the AI tagging. + * + * @param string $recipients + * Comma delimited list of recipients for the report email. + * + * @command reliefweb_reporting:send-weekly-ai-tagging-stats + * + * @usage reliefweb_reporting:send-weekly-ai-tagging-stats + * Send weekly statistics about the AI tagging. + * + * @validate-module-enabled reliefweb_reporting + */ + public function sendWeeklyAiTaggingStats($recipients) { + if (!empty($this->state->get('system.maintenance_mode', 0))) { + $this->logger()->warning(dt('Maintenance mode, aborting.')); + return TRUE; + } + + if (empty($recipients)) { + $this->logger()->error(dt('Missing recipients.')); + return FALSE; + } + + $from = $this->configFactory->get('system.site')->get('mail') ?? ini_get('sendmail_from'); + if (empty($from)) { + $this->logger()->error(dt('Missing from address.')); + return FALSE; + } + + // Format the from to include ReliefWeb if not already. + if (strpos($from, '<') === FALSE) { + $from = strtr('@sitename <@sitemail>', [ + '@sitename' => $this->configFactory->get('system.site')->get('name') ?? 'ReliefWeb', + '@sitemail' => $from, ]); } + $recipients = implode(', ', preg_split('/,\s*/', trim($recipients))); + + $last_sunday = strtotime('last sunday', time()); + $last_week_sunday = strtotime('last sunday', $last_sunday); + $formatted_last_sunday = gmdate('Y-m-d', $last_sunday); + $formatted_last_week_sunday = gmdate('Y-m-d', $last_week_sunday); + + $filename = "job-ai-tagging-stats-$formatted_last_week_sunday-$formatted_last_sunday.csv"; + $subject = "Weekly job AI tagging stats - $formatted_last_week_sunday to $formatted_last_sunday"; + $headers = [ + 'From' => $from, + 'Reply-To' => $from, + 'Content-Type' => 'text/plain; charset=utf-8', + 'Content-Disposition' => 'inline', + 'MIME-Version' => '1.0', + ]; + + $records = $this->database->query(" + SELECT + nr.nid AS nid, + nr.vid As vid, + IFNULL(GROUP_CONCAT(DISTINCT ur.roles_target_id ORDER BY ur.roles_target_id SEPARATOR ','), '') AS roles, + GROUP_CONCAT(DISTINCT nfcc.field_career_categories_target_id ORDER BY nfcc.field_career_categories_target_id SEPARATOR ',') AS career_categories, + GROUP_CONCAT(DISTINCT nft.field_theme_target_id ORDER BY nft.field_theme_target_id SEPARATOR ',') AS themes + FROM node_revision AS nr + INNER JOIN node_field_data AS n + ON n.nid = nr.nid + INNER JOIN node_revision__reliefweb_job_tagger_status AS njts + ON njts.entity_id = n.nid + AND njts.revision_id = nr.vid + AND njts.reliefweb_job_tagger_status_value = 'processed' + LEFT JOIN user__roles AS ur + ON ur.entity_id = nr.revision_uid + LEFT JOIN node_revision__field_career_categories AS nfcc + ON nfcc.entity_id = nr.nid + AND nfcc.revision_id = nr.vid + LEFT JOIN node_revision__field_theme AS nft + ON nft.entity_id = nr.nid + AND nft.revision_id = nr.vid + WHERE n.type = 'job' + AND n.created >= UNIX_TIMESTAMP(DATE_SUB(DATE(NOW()), INTERVAL DAYOFWEEK(NOW()) + 6 DAY)) + AND n.created < UNIX_TIMESTAMP(DATE_SUB(DATE(NOW()), INTERVAL DAYOFWEEK(NOW()) - 1 DAY)) + GROUP BY nr.vid + ORDER BY nr.vid + ")->fetchAll(\PDO::FETCH_ASSOC); + + $attachments = []; + + if (empty($records)) { + $body = 'No jobs tagged by AI this week.'; + } + else { + $jobs = []; + $changed_career_categories = []; + $changed_themes = []; + + foreach ($records as $record) { + $nid = $record['nid']; + + if (isset($jobs[$nid])) { + $previous = $jobs[$nid]; + + if (strpos($record['roles'], 'editor') !== FALSE) { + if ($record['career_categories'] !== $previous['career_categories']) { + $changed_career_categories[$nid] = TRUE; + } + if ($record['themes'] !== $previous['themes']) { + $changed_themes[$nid] = TRUE; + } + } + } + + $jobs[$nid] = $record; + } + + $data = [ + 'jobs_tagged_by_ai' => count($jobs), + 'career_categories_changed_by_editors' => count($changed_career_categories), + 'themes_changed_by_editors' => count($changed_themes), + ]; + + // Convert to CSV. + $handle = fopen('php://memory', 'r+'); + fputcsv($handle, array_keys($data), "\t"); + fputcsv($handle, array_values($data), "\t"); + rewind($handle); + $csv = trim(stream_get_contents($handle)); + fclose($handle); + + $body = "Attachment: $filename"; + + $attachments[] = [ + 'filecontent' => $csv, + 'filename' => $filename, + 'filemime' => 'text/csv', + ]; + } + // Send the email. - if (mail($recipients, $subject, $body, $headers)) { + $message = $this->sendMail($from, $recipients, $subject, $body, $headers, $attachments); + if (!empty($message['result'])) { $this->logger->info(dt('"@subject" sent to @recipients', [ '@subject' => $subject, '@recipients' => $recipients, @@ -324,4 +476,34 @@ public function sendWeeklyReportStats($recipients) { return TRUE; } + /** + * Send an email. + * + * @param string $from + * From address. + * @param string $recipients + * Recipient addresses. + * @param string $subject + * Email subjects. + * @param string $body + * Email body. + * @param array $headers + * Email headers. + * @param array $attachments + * Optional attachments. + * + * @return array + * The message array with a `result` property indicating success or failture + * at the PHP level. + */ + protected function sendMail(string $from, string $recipients, string $subject, string $body, array $headers, array $attachments = []) { + $language = $this->languageDefault->get()->getId(); + return $this->mailManager->mail('reliefweb_reporting', 'reporting', $recipients, $language, [ + 'headers' => $headers, + 'subject' => $subject, + 'body' => [$body], + 'attachments' => $attachments, + ], $from, TRUE); + } + } diff --git a/html/modules/custom/reliefweb_revisions/src/Services/EntityHistory.php b/html/modules/custom/reliefweb_revisions/src/Services/EntityHistory.php index 8a24d6309..44ef43965 100644 --- a/html/modules/custom/reliefweb_revisions/src/Services/EntityHistory.php +++ b/html/modules/custom/reliefweb_revisions/src/Services/EntityHistory.php @@ -125,7 +125,7 @@ public function __construct( EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, - TranslationInterface $string_translation + TranslationInterface $string_translation, ) { $this->cache = $cache_backend; $this->config = $config_factory->get('reliefweb_revisions.settings'); diff --git a/html/modules/custom/reliefweb_rivers/src/Controller/SearchConverter.php b/html/modules/custom/reliefweb_rivers/src/Controller/SearchConverter.php index 38322b2ad..9e30c7567 100644 --- a/html/modules/custom/reliefweb_rivers/src/Controller/SearchConverter.php +++ b/html/modules/custom/reliefweb_rivers/src/Controller/SearchConverter.php @@ -74,7 +74,7 @@ public function __construct( AccountProxyInterface $current_user, FormBuilderInterface $form_builder, RequestStack $request_stack, - ReliefWebApiClient $reliefweb_api_client + ReliefWebApiClient $reliefweb_api_client, ) { $this->configFactory = $config_factory; $this->currentUser = $current_user; diff --git a/html/modules/custom/reliefweb_rivers/src/Parameters.php b/html/modules/custom/reliefweb_rivers/src/Parameters.php index f9d1c528a..b66643305 100644 --- a/html/modules/custom/reliefweb_rivers/src/Parameters.php +++ b/html/modules/custom/reliefweb_rivers/src/Parameters.php @@ -456,7 +456,7 @@ class Parameters { */ public function __construct( array $query = NULL, - array $exclude = ['q', 'page'] + array $exclude = ['q', 'page'], ) { $this->parameters = static::getParameters($query, $exclude); diff --git a/html/modules/custom/reliefweb_rivers/src/RiverServiceBase.php b/html/modules/custom/reliefweb_rivers/src/RiverServiceBase.php index c873fdb7a..7276b0eaa 100644 --- a/html/modules/custom/reliefweb_rivers/src/RiverServiceBase.php +++ b/html/modules/custom/reliefweb_rivers/src/RiverServiceBase.php @@ -169,7 +169,7 @@ public function __construct( ReliefWebApiClient $api_client, RequestStack $request_stack, RendererInterface $renderer, - TranslationInterface $string_translation + TranslationInterface $string_translation, ) { $this->configFactory = $config_factory; $this->currentUser = $current_user; diff --git a/html/modules/custom/reliefweb_rivers/src/Services/TopicRiver.php b/html/modules/custom/reliefweb_rivers/src/Services/TopicRiver.php index f1a02660f..ce34659c6 100644 --- a/html/modules/custom/reliefweb_rivers/src/Services/TopicRiver.php +++ b/html/modules/custom/reliefweb_rivers/src/Services/TopicRiver.php @@ -120,7 +120,7 @@ public function __construct( RequestStack $request_stack, RendererInterface $renderer, TranslationInterface $string_translation, - EntityTypeManagerInterface $entity_type_manager + EntityTypeManagerInterface $entity_type_manager, ) { parent::__construct( $config_factory, diff --git a/html/modules/custom/reliefweb_subscriptions/config/install/reliefweb_subscriptions.settings.yml b/html/modules/custom/reliefweb_subscriptions/config/install/reliefweb_subscriptions.settings.yml new file mode 100644 index 000000000..e34afc6ad --- /dev/null +++ b/html/modules/custom/reliefweb_subscriptions/config/install/reliefweb_subscriptions.settings.yml @@ -0,0 +1,4 @@ +# Encryption method for the unsubscribe links. +encryption_method: 'aes-128-ctr' +# Encryption key for the unsubscribe links. +encryption_key: diff --git a/html/modules/custom/reliefweb_subscriptions/config/schema/reliefweb_subscriptions.schema.yml b/html/modules/custom/reliefweb_subscriptions/config/schema/reliefweb_subscriptions.schema.yml new file mode 100644 index 000000000..70fb74fcd --- /dev/null +++ b/html/modules/custom/reliefweb_subscriptions/config/schema/reliefweb_subscriptions.schema.yml @@ -0,0 +1,10 @@ +reliefweb_subscriptions.settings: + type: config_object + label: 'Reliefweb Subscriptions settings' + mapping: + encryption_method: + type: string + label: 'Encryption method for the unsubscribe links' + encryption_key: + type: string + label: 'Encryption key for the unsubscribe links' diff --git a/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.info.yml b/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.info.yml index 9e9bc67c4..8c3dd5d78 100644 --- a/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.info.yml +++ b/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.info.yml @@ -4,6 +4,7 @@ description: 'Allow users to subscribe to content changes.' package: reliefweb core_version_requirement: ^9 || ^10 dependencies: + - amazon_ses:amazon_ses - drupal:reliefweb_api - drupal:reliefweb_form - drupal:reliefweb_utility diff --git a/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.routing.yml b/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.routing.yml index 5ed5e96b5..562978c8f 100644 --- a/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.routing.yml +++ b/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.routing.yml @@ -25,6 +25,17 @@ reliefweb_subscriptions.unsubscribe: requirements: _access: 'TRUE' user: \d+ +reliefweb_subscriptions.unsubscribe_one_click: + path: '/notifications/unsubscribe/{opaque}' + defaults: + _controller: '\Drupal\reliefweb_subscriptions\Controller\UnsubscribeController::unsubscribeOneClick' + _title: 'Unsubscribe' + methods: [POST, GET] + options: + no_cache: 'TRUE' + requirements: + _access: 'TRUE' + user: \d+ reliefweb_subscriptions.unsubscription_form: path: '/user/{user}/notifications/unsubscribe/{timestamp}/{signature}' defaults: diff --git a/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.services.yml b/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.services.yml index a77867b44..0f66e6216 100644 --- a/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.services.yml +++ b/html/modules/custom/reliefweb_subscriptions/reliefweb_subscriptions.services.yml @@ -2,3 +2,15 @@ services: reliefweb_subscriptions.mailer: class: \Drupal\reliefweb_subscriptions\ReliefwebSubscriptionsMailer arguments: ['@config.factory', '@database', '@entity_field.manager', '@entity.repository', '@entity_type.manager', '@state', '@datetime.time', '@reliefweb_api.client', '@private_key', '@renderer', '@plugin.manager.mail', '@language.default', '@logger.factory', '@theme.initialization', '@theme.manager', '@theme_handler'] + + # Override the amazon_ses message builder so we can add extra headers and + # override the text version of the email. + amazon_ses.message_builder: + class: Drupal\reliefweb_subscriptions\AmazonSesMessageBuilder + arguments: ['@logger.channel.amazon_ses', '@config.factory', '@file_system', '@file.mime_type.guesser'] + + # We need to override the logger channel as well so Drupal stops complaining. + logger.channel.amazon_ses: + class: Drupal\Core\Logger\LoggerChannel + factory: logger.factory:get + arguments: ['amazon_ses'] diff --git a/html/modules/custom/reliefweb_subscriptions/src/AmazonSesMessageBuilder.php b/html/modules/custom/reliefweb_subscriptions/src/AmazonSesMessageBuilder.php new file mode 100644 index 000000000..092464af2 --- /dev/null +++ b/html/modules/custom/reliefweb_subscriptions/src/AmazonSesMessageBuilder.php @@ -0,0 +1,81 @@ + $value) { + switch (strtolower($key)) { + case 'from': + $message['from'] = $value; + unset($message['headers'][$key]); + $message['headers']['From'] = $value; + break; + + case 'reply-to': + $message['reply-to'] = $value; + unset($message['headers'][$key]); + $message['headers']['Reply-to'] = $value; + break; + + case 'cc': + unset($message['headers'][$key]); + $message['headers']['Cc'] = $value; + break; + + case 'bcc': + unset($message['headers'][$key]); + $message['headers']['Bcc'] = $value; + break; + } + } + } + + /** @var Symfony\Component\Mime\Email $email */ + $email = parent::buildMessage($message); + + // Add extra headers. + // @see \Drupal\reliefweb_subscriptions\ReliefWebSubscriptionsMailer::generateEmail(). + if (isset($message['params']['headers'])) { + /** @var Symfony\Component\Mime\Headers $headers */ + $headers = $email->getHeaders(); + foreach ($message['params']['headers'] as $header => $value) { + $headers->addHeader($header, $value); + } + } + + // Replace the plain text message if it exists. + // + // @see reliefweb_subscriptions_mail(). + // @see reliefweb_entities_mail() + // @see reliefweb_contact_mail_alter(). + if (isset($message['params']['plaintext'])) { + $email->text($message['params']['plaintext']); + } + + // We replace the email with ours that preserves the BCC header so that + // AWS SES can send emails to the BCC addresses when parsing the raw email + // data sent by the AmazonSesHandler service. + // + // @see \Drupal\amazon_ses\MessageBuilder\AmazonSesHandler::send(). + // @see \Symfony\Component\Mime\Message::getPreparedHeaders() + $data = $email->__serialize(); + $email_with_bcc = new EmailWithBcc(); + $email_with_bcc->__unserialize($data); + + return $email_with_bcc; + } + +} diff --git a/html/modules/custom/reliefweb_subscriptions/src/Command/ReliefwebSubscriptionsSendCommand.php b/html/modules/custom/reliefweb_subscriptions/src/Command/ReliefwebSubscriptionsSendCommand.php index a94186df5..572e0c255 100644 --- a/html/modules/custom/reliefweb_subscriptions/src/Command/ReliefwebSubscriptionsSendCommand.php +++ b/html/modules/custom/reliefweb_subscriptions/src/Command/ReliefwebSubscriptionsSendCommand.php @@ -122,11 +122,14 @@ public function send($limit = 50) { * * @validate-module-enabled reliefweb_subscriptions */ - public function queue($sid, array $options = [ - 'entity_type' => '', - 'entity_id' => 0, - 'last' => 0, - ]) { + public function queue( + $sid, + array $options = [ + 'entity_type' => '', + 'entity_id' => 0, + 'last' => 0, + ], + ) { $this->mailer->queue($sid, $options); } @@ -153,9 +156,12 @@ public function queue($sid, array $options = [ * * @validate-module-enabled reliefweb_subscriptions */ - public function unsubscribe($frequency = '1w', array $options = [ - 'dry-run' => FALSE, - ]) { + public function unsubscribe( + $frequency = '1w', + array $options = [ + 'dry-run' => FALSE, + ], + ) { $settings = Settings::get('ocha_elk_mail', []); if (empty($settings['url'])) { $this->logger()->error(dt('ELK url missing')); diff --git a/html/modules/custom/reliefweb_subscriptions/src/Controller/UnsubscribeController.php b/html/modules/custom/reliefweb_subscriptions/src/Controller/UnsubscribeController.php index 4a6b0c9ad..9c98f34db 100644 --- a/html/modules/custom/reliefweb_subscriptions/src/Controller/UnsubscribeController.php +++ b/html/modules/custom/reliefweb_subscriptions/src/Controller/UnsubscribeController.php @@ -4,11 +4,17 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Url; +use Drupal\reliefweb_subscriptions\ReliefwebSubscriptionsMailer; use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** * Unsubscribe controller. @@ -25,16 +31,46 @@ class UnsubscribeController extends ControllerBase { /** * Account. * - * @var \Symfony\Component\HttpFoundation\Request + * @var \Symfony\Component\HttpFoundation\RequestStack */ - protected $request; + protected $requestStack; + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The actual mailer. + * + * @var \Drupal\reliefweb_subscriptions\ReliefwebSubscriptionsMailer + */ + protected $mailer; /** * {@inheritdoc} */ - public function __construct(AccountInterface $account, RequestStack $request_stack) { + public function __construct( + AccountInterface $account, + RequestStack $request_stack, + Connection $database, + EntityTypeManagerInterface $entity_type_manager, + ReliefwebSubscriptionsMailer $mailer, + ) { $this->account = $account; - $this->request = $request_stack->getCurrentRequest(); + $this->requestStack = $request_stack; + $this->database = $database; + $this->entityTypeManager = $entity_type_manager; + $this->mailer = $mailer; } /** @@ -43,17 +79,52 @@ public function __construct(AccountInterface $account, RequestStack $request_sta public static function create(ContainerInterface $container) { return new static( $container->get('current_user'), - $container->get('request_stack') + $container->get('request_stack'), + $container->get('database'), + $container->get('entity_type.manager'), + $container->get('reliefweb_subscriptions.mailer') ); } + /** + * Get the current request. + * + * @return \Symfony\Component\HttpFoundation\Request + * Current request. + */ + public function getRequest(): Request { + return $this->requestStack->getCurrentRequest(); + } + + /** + * Get the current user. + * + * @return \Drupal\Core\Session\AccountInterface + * Current user. + */ + public function getCurrentUser(): AccountInterface { + return $this->account; + } + + /** + * Get the entity type manger. + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * Entity type manager. + */ + public function getEntityTypeManager(): EntityTypeManagerInterface { + return $this->entityTypeManager; + } + /** * Unsubscribe a user. */ public function unsubscribe(AccountInterface $user) { if ($this->account->isAnonymous()) { - $timestamp = $this->request->get('timestamp'); - $signature = $this->request->get('signature'); + $request = $this->getRequest(); + $timestamp = $request->get('timestamp'); + $signature = $request->get('signature'); + if (empty($timestamp) || empty($signature)) { throw new AccessDeniedHttpException(); } @@ -65,7 +136,7 @@ public function unsubscribe(AccountInterface $user) { ]); } - if ($this->account->id() === $user->id()) { + if ($user->id() === $this->getCurrentUser()->id()) { return $this->redirect('reliefweb_subscriptions.subscription_form', [ 'user' => $user->id(), ]); @@ -74,6 +145,69 @@ public function unsubscribe(AccountInterface $user) { throw new AccessDeniedHttpException(); } + /** + * Unsubscribe a user (one click). + * + * @param string $opaque + * Opaque data from the unsubscribe link. + * + * @return array + * Render array for the page. + * + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * Bad request if the link is invalid. + */ + public function unsubscribeOneClick(string $opaque): array { + $data = $this->mailer->decryptOneClickUnsubscribeLink($opaque); + if ($data === FALSE) { + throw new BadRequestHttpException(); + } + + [$uid, $sid] = $data; + + $subscriptions = reliefweb_subscriptions_subscriptions(); + if (!isset($subscriptions[$sid])) { + throw new BadRequestHttpException(); + } + + $user = $this->getEntityTypeManager()->getStorage('user')->load($uid); + if (is_null($user)) { + throw new BadRequestHttpException(); + } + + // Remove the subscription. + $this->database->delete('reliefweb_subscriptions_subscriptions') + ->condition('sid', $sid) + ->condition('uid', $user->id()) + ->execute(); + + $subscription_page = '/user/notifications'; + + if ($user->id() === $this->getCurrentUser()->id()) { + $message = $this->t('To add new subscriptions or manage your list, please go to your subscriptions page.', [ + ':subscription_page' => $subscription_page, + ]); + } + else { + $login_link = Url::fromRoute('user.login', [], [ + 'query' => ['destination' => $subscription_page], + ]); + + $message = $this->t('To add new subscriptions or manage your list, please log in.', [ + ':login_link' => $login_link->toString(), + ]); + } + + return [ + '#type' => 'inline_template', + '#template' => '

You have successfully unsubscribed.

You will no longer receive {{ subscription_label }} notifications.

{{ message }}

', + '#context' => [ + 'subscription_label' => $subscriptions[$sid]['name'], + 'message' => $message, + ], + ]; + } + /** * Check the access to the page. * diff --git a/html/modules/custom/reliefweb_subscriptions/src/EmailWithBcc.php b/html/modules/custom/reliefweb_subscriptions/src/EmailWithBcc.php new file mode 100644 index 000000000..9e20c864e --- /dev/null +++ b/html/modules/custom/reliefweb_subscriptions/src/EmailWithBcc.php @@ -0,0 +1,26 @@ +has('Bcc') && $this->getHeaders()->has('Bcc')) { + $headers->add($this->getHeaders()->get('Bcc')); + } + return $headers; + } + +} diff --git a/html/modules/custom/reliefweb_subscriptions/src/Form/UnsubscribeForm.php b/html/modules/custom/reliefweb_subscriptions/src/Form/UnsubscribeForm.php index 5b3d1b641..efbeb8699 100644 --- a/html/modules/custom/reliefweb_subscriptions/src/Form/UnsubscribeForm.php +++ b/html/modules/custom/reliefweb_subscriptions/src/Form/UnsubscribeForm.php @@ -107,7 +107,7 @@ public function buildForm(array $form, FormStateInterface $form_state, AccountIn ]; $login_link = Url::fromRoute('user.login', [], [ - 'query' => ['destination' => '/user/' . $user->id() . '/notifications'], + 'query' => ['destination' => '/user/notifications'], ]); $form['description'] = [ diff --git a/html/modules/custom/reliefweb_subscriptions/src/ReliefwebSubscriptionsMailer.php b/html/modules/custom/reliefweb_subscriptions/src/ReliefwebSubscriptionsMailer.php index 2bfe96f9e..bdff07bee 100644 --- a/html/modules/custom/reliefweb_subscriptions/src/ReliefwebSubscriptionsMailer.php +++ b/html/modules/custom/reliefweb_subscriptions/src/ReliefwebSubscriptionsMailer.php @@ -157,23 +157,23 @@ class ReliefwebSubscriptionsMailer { * {@inheritdoc} */ public function __construct( - ConfigFactoryInterface $config_factory, - Connection $database, - EntityFieldManagerInterface $entity_field_manager, - EntityRepositoryInterface $entity_repository, - EntityTypeManagerInterface $entity_type_manager, - StateInterface $state, - TimeInterface $time, - ReliefWebApiClient $reliefwebApiClient, - PrivateKey $privateKey, - RendererInterface $renderer, - MailManagerInterface $mailManager, - LanguageDefault $languageDefault, - LoggerChannelFactoryInterface $loggerFactory, - ThemeInitialization $themeInitialization, - ThemeManagerInterface $themeManager, - ThemeHandlerInterface $themeHandler - ) { + ConfigFactoryInterface $config_factory, + Connection $database, + EntityFieldManagerInterface $entity_field_manager, + EntityRepositoryInterface $entity_repository, + EntityTypeManagerInterface $entity_type_manager, + StateInterface $state, + TimeInterface $time, + ReliefWebApiClient $reliefwebApiClient, + PrivateKey $privateKey, + RendererInterface $renderer, + MailManagerInterface $mailManager, + LanguageDefault $languageDefault, + LoggerChannelFactoryInterface $loggerFactory, + ThemeInitialization $themeInitialization, + ThemeManagerInterface $themeManager, + ThemeHandlerInterface $themeHandler, + ) { $this->configFactory = $config_factory; $this->database = $database; $this->entityFieldManager = $entity_field_manager; @@ -279,10 +279,13 @@ public function send($notifications) { * @option last * Timestamp to use as the last time notifications were sent. */ - public function queue($sid, array $options = [ - 'entity_id' => 0, - 'last' => 0, - ]) { + public function queue( + $sid, + array $options = [ + 'entity_id' => 0, + 'last' => 0, + ], + ) { $this->logger->notice('Processing {sid}', [ 'sid' => $sid, ]); @@ -441,6 +444,7 @@ protected function generateEmail(array $subscription, array $data) { static $from; static $language; static $batch_size; + static $throttle; if (!isset($from)) { $from = $this->config('system.site')->get('mail') ?? ini_get('sendmail_from'); @@ -454,6 +458,10 @@ protected function generateEmail(array $subscription, array $data) { $language = $this->languageDefault->get()->getId(); // Number of emails to send by second. $batch_size = $this->state->get('reliefweb_subscriptions_mail_batch_size', 40); + + // Do not throttle if using the amazon_ses module since it does that by + // itself. + $throttle = $this->config('mailsystem.settings')?->get('defaults.sender') !== 'amazon_ses_mail'; } $sid = $subscription['id']; @@ -512,7 +520,7 @@ protected function generateEmail(array $subscription, array $data) { // Send current batch of emails in a simple loop. foreach ($batch as $subscriber) { // Generate the individual unsubscribe link. - $unsubscribe = $this->generateUnsubscribeLink($subscriber->uid, $sid); + $unsubscribe = $this->generateOneClickUnsubscribeLink($subscriber->uid, $sid); // Update the body with the unique ubsubscribe link. $mail_body = new FormattableMarkup($body, [ @@ -524,7 +532,8 @@ protected function generateEmail(array $subscription, array $data) { $this->mailManager->mail('reliefweb_subscriptions', 'notifications', $subscriber->mail, $language, [ 'headers' => [ 'List-Id' => $list_id, - 'List-Unsubscribe' => $unsubscribe, + 'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click', + 'List-Unsubscribe' => '<' . $unsubscribe . '>', 'X-RW-Category' => $category, ], 'subject' => $subject, @@ -537,8 +546,8 @@ protected function generateEmail(array $subscription, array $data) { // batch. Probably not strictly necessary, but if we *do* go fast this // can help clear a back-log of 38,000 mails just a bit faster. $timer_elapsed = microtime(TRUE) - $timer_start; - if ($timer_elapsed < 1) { - usleep((1 - $timer_elapsed) * 1e+6); + if ($throttle && $timer_elapsed < 1) { + usleep((int) ((1 - $timer_elapsed) * 1e+6)); } } @@ -1468,6 +1477,77 @@ protected function generateUnsubscribeLink($uid, $sid) { return $url->toString(); } + /** + * Generate a one click unsubscribe link for the given user and subscription. + * + * @param int $uid + * User id. + * @param string $sid + * Subscription id. + * + * @return string + * Unsubscribe link. + */ + protected function generateOneClickUnsubscribeLink($uid, $sid) { + $data = $uid . '-' . $sid; + $config = $this->config('reliefweb_subscriptions.settings'); + + $method = $config->get('encryption_method') ?? 'aes-128-ctr'; + $iv_length = openssl_cipher_iv_length($method); + $iv = openssl_random_pseudo_bytes($iv_length); + + $salt = Settings::getHashSalt(); + $encryption_key = $config->get('encryption_key'); + $signature_key = $this->privateKey->get(); + + $encrypted = openssl_encrypt($data, $method, $encryption_key . $salt, OPENSSL_RAW_DATA, $iv); + $signature = hash_hmac('sha256', $encrypted, $signature_key . $salt, TRUE); + + $opaque = strtr(base64_encode($iv . $signature . $encrypted), '+/', '-_'); + $uri = $this->getSchemeAndHttpHost() . '/notifications/unsubscribe/' . $opaque; + + $options = [ + 'absolute' => TRUE, + ]; + $options = $this->addLinkTrackingParameters($sid, $options); + $url = Url::fromUri($uri, $options); + return $url->toString(); + } + + /** + * Generate a one click unsubscribe link for the given user and subscription. + * + * @param string $opaque + * Opaque data from the unsubscribe link. + * + * @return array|false + * FALSE if the message could not be decrypted or an array with the user + * ID and the subscription ID. + */ + public function decryptOneClickUnsubscribeLink(string $opaque) { + $raw = base64_decode(strtr($opaque, '-_', '+/')); + $config = $this->config('reliefweb_subscriptions.settings'); + + $method = $config->get('encryption_method') ?? 'aes-128-ctr'; + $iv_length = openssl_cipher_iv_length($method); + + $salt = Settings::getHashSalt(); + $encryption_key = $config->get('encryption_key'); + $signature_key = $this->privateKey->get(); + + $iv = substr($raw, 0, $iv_length); + $signature = substr($raw, $iv_length, 32); + $encrypted = substr($raw, $iv_length + 32); + + $hash = hash_hmac('sha256', $encrypted, $signature_key . $salt, TRUE); + if (!hash_equals($signature, $hash)) { + return FALSE; + } + + $data = openssl_decrypt($encrypted, $method, $encryption_key . $salt, OPENSSL_RAW_DATA, $iv); + return $data ? explode('-', $data, 2) : FALSE; + } + /** * Get signature for the unsubscribe links. * @@ -1484,7 +1564,7 @@ protected function getSignature($path, $timestamp) { } /** - * Check unsubscribe links. + * Check unsubscribe link. * * @param string $uid * Unsubscribe path. @@ -1994,7 +2074,7 @@ public function generatePreview($sid) { 'absolute' => TRUE, ]))->toString(); // Dummy unsubscribe link. - $unsubscribe = $this->generateUnsubscribeLink(0, $sid); + $unsubscribe = $this->generateOneClickUnsubscribeLink(0, $sid); // Update the body with the unique ubsubscribe link. $body = new FormattableMarkup($body, [ diff --git a/html/modules/custom/reliefweb_user_posts/src/Services/UserPostsService.php b/html/modules/custom/reliefweb_user_posts/src/Services/UserPostsService.php index a591e57cc..32b11619b 100644 --- a/html/modules/custom/reliefweb_user_posts/src/Services/UserPostsService.php +++ b/html/modules/custom/reliefweb_user_posts/src/Services/UserPostsService.php @@ -64,7 +64,7 @@ public function __construct( PagerParametersInterface $pager_parameters, RequestStack $request_stack, TranslationInterface $string_translation, - RouteMatchInterface $route_match + RouteMatchInterface $route_match, ) { parent::__construct( $current_user, diff --git a/html/modules/custom/reliefweb_users/src/Controller/UserEmailConfirmationController.php b/html/modules/custom/reliefweb_users/src/Controller/UserEmailConfirmationController.php index 591ad6ddb..95cf42a16 100644 --- a/html/modules/custom/reliefweb_users/src/Controller/UserEmailConfirmationController.php +++ b/html/modules/custom/reliefweb_users/src/Controller/UserEmailConfirmationController.php @@ -41,7 +41,7 @@ class UserEmailConfirmationController extends ControllerBase { public function __construct( ConfigFactoryInterface $config_factory, RequestStack $request_stack, - TimeInterface $time + TimeInterface $time, ) { $this->configFactory = $config_factory; $this->requestStack = $request_stack; diff --git a/html/modules/custom/reliefweb_utility/src/Helpers/MarkdownHelper.php b/html/modules/custom/reliefweb_utility/src/Helpers/MarkdownHelper.php index ce2a2198b..8fe6e61d4 100644 --- a/html/modules/custom/reliefweb_utility/src/Helpers/MarkdownHelper.php +++ b/html/modules/custom/reliefweb_utility/src/Helpers/MarkdownHelper.php @@ -187,6 +187,9 @@ public static function convertInlinesOnly($text, array $internal_hosts = ['relie ], ]; + // Prevent link references `[foo]: /url` from being parsed. + $text = preg_replace('#^(\s*)\[([^]]+)\]:(\s+)#', '$1\[$2\]:$3', $text); + // Create an Environment with all the CommonMark parsers and renderers for // inline elements. $environment = new Environment($config); diff --git a/html/modules/custom/reliefweb_utility/tests/src/Unit/MarkdownHelperTest.php b/html/modules/custom/reliefweb_utility/tests/src/Unit/MarkdownHelperTest.php index 4fad7abc9..62a3c42bf 100644 --- a/html/modules/custom/reliefweb_utility/tests/src/Unit/MarkdownHelperTest.php +++ b/html/modules/custom/reliefweb_utility/tests/src/Unit/MarkdownHelperTest.php @@ -97,4 +97,17 @@ public function providerMarkdown() { ]; } + /** + * @covers \Drupal\reliefweb_utility\Helpers\MarkdownHelper::convertInlinesOnly + */ + public function testConvertInlinesOnly() { + $text = "test [link](https://test.test)\n\nthis **not** a paragraph\n\n* list item 1\n* list item 2\n"; + $expected = "test link\n\nthis not a paragraph\n\n* list item 1\n* list item 2\n"; + $this->assertEquals($expected, MarkdownHelper::convertInlinesOnly($text)); + + $text = "[test]: /test"; + $expected = "[test]: /test\n"; + $this->assertEquals($expected, MarkdownHelper::convertInlinesOnly($text)); + } + } diff --git a/html/modules/custom/reliefweb_xmlsitemap/src/Command/ReliefwebXmlsitemapCommand.php b/html/modules/custom/reliefweb_xmlsitemap/src/Command/ReliefwebXmlsitemapCommand.php index 476c9f901..d3af6f91b 100644 --- a/html/modules/custom/reliefweb_xmlsitemap/src/Command/ReliefwebXmlsitemapCommand.php +++ b/html/modules/custom/reliefweb_xmlsitemap/src/Command/ReliefwebXmlsitemapCommand.php @@ -96,7 +96,7 @@ public function __construct( FileSystemInterface $file_system, ClientInterface $http_client, LoggerChannelFactoryInterface $logger_factory, - StateInterface $state + StateInterface $state, ) { $this->database = $database; $this->extensionPathResolver = $extension_path_resolver; @@ -264,9 +264,11 @@ public function prepareDirectory() { * @option delete * Delete directory as well. */ - public function clearDirectory(array $options = [ - 'delete' => FALSE, - ]) { + public function clearDirectory( + array $options = [ + 'delete' => FALSE, + ], + ) { $directory = $this->getDirectory(); if (is_dir($directory)) { diff --git a/html/themes/custom/common_design_subtheme/common_design_subtheme.info.yml b/html/themes/custom/common_design_subtheme/common_design_subtheme.info.yml index 928f5b2cb..687d20ae5 100644 --- a/html/themes/custom/common_design_subtheme/common_design_subtheme.info.yml +++ b/html/themes/custom/common_design_subtheme/common_design_subtheme.info.yml @@ -17,7 +17,6 @@ regions: sidebar_first: First sidebar sidebar_second: Second sidebar facets: Facets - footer_soft: Soft footer footer_navigation: Footer navigation ### diff --git a/html/themes/custom/common_design_subtheme/common_design_subtheme.libraries.yml b/html/themes/custom/common_design_subtheme/common_design_subtheme.libraries.yml index c794898c4..18eb9167f 100644 --- a/html/themes/custom/common_design_subtheme/common_design_subtheme.libraries.yml +++ b/html/themes/custom/common_design_subtheme/common_design_subtheme.libraries.yml @@ -39,7 +39,6 @@ cd-footer: css: theme: components/cd/cd-footer/cd-footer.css: {} - components/cd/cd-footer/cd-soft-footer.css: {} cd-form: css: theme: diff --git a/html/themes/custom/common_design_subtheme/components/cd/cd-footer/cd-soft-footer.css b/html/themes/custom/common_design_subtheme/components/cd/cd-footer/cd-soft-footer.css deleted file mode 100644 index 47a01391c..000000000 --- a/html/themes/custom/common_design_subtheme/components/cd/cd-footer/cd-soft-footer.css +++ /dev/null @@ -1,65 +0,0 @@ -.cd-soft-footer::before { - background: var(--brand-primary); -} - -.cd-soft-footer .visually-hidden { - position: absolute !important; - overflow: hidden; - clip: rect(1px, 1px, 1px, 1px); - width: 1px; - height: 1px; - word-wrap: normal; -} - -/* Set colour despite being visually hidden so it passes WCAG AA */ -.cd-soft-footer h2.visually-hidden, -.cd-soft-footer label.visually-hidden { - color: var(--brand-primary); -} - -.cd-soft-footer .mc-field-group { - display: flex; - flex-direction: column; -} - -.cd-soft-footer .cd-button { - margin-top: 0.5rem; - padding: 10px 12px; - text-transform: uppercase; - color: var(--cd-white); - background: var(--brand-highlight); - font-weight: 700; -} - -.cd-soft-footer label { - margin: 0; - padding: 10px 12px 10px 0; - text-align: left; - color: var(--cd-white); -} - -.cd-soft-footer__inner input[type="email"] { - width: auto; - padding: 10px 12px; - color: var(--cd-default-text-color); -} - -.cd-soft-footer .cd-button:hover, -.cd-soft-footer .cd-button:focus { - background: var(--brand-primary); -} - -.cd-soft-footer input:focus, -.cd-soft-footer .cd-button:focus { - outline: 3px solid var(--brand-primary--light); - outline-offset: -2px; -} - -@media (min-width: 576px) { - .cd-soft-footer .mc-field-group { - flex-direction: row; - } - .cd-soft-footer .cd-button { - margin-top: 0; - } -} diff --git a/html/themes/custom/common_design_subtheme/templates/cd/cd-footer/cd-soft-footer.html.twig b/html/themes/custom/common_design_subtheme/templates/cd/cd-footer/cd-soft-footer.html.twig deleted file mode 100644 index b43b8f0cd..000000000 --- a/html/themes/custom/common_design_subtheme/templates/cd/cd-footer/cd-soft-footer.html.twig +++ /dev/null @@ -1,27 +0,0 @@ - diff --git a/html/themes/custom/common_design_subtheme/templates/layout/page--403.html.twig b/html/themes/custom/common_design_subtheme/templates/layout/page--403.html.twig index 833888a6c..fadf600ad 100644 --- a/html/themes/custom/common_design_subtheme/templates/layout/page--403.html.twig +++ b/html/themes/custom/common_design_subtheme/templates/layout/page--403.html.twig @@ -57,10 +57,6 @@ {% endblock %} - {% block footer_soft %} - {% include '@common_design_subtheme/cd/cd-footer/cd-soft-footer.html.twig' %} - {% endblock %} - {% block footer %} {% include '@common_design_subtheme/cd/cd-footer/cd-footer.html.twig' %} {% endblock %} diff --git a/html/themes/custom/common_design_subtheme/templates/layout/page--404.html.twig b/html/themes/custom/common_design_subtheme/templates/layout/page--404.html.twig index 6d780d5df..b2272287b 100644 --- a/html/themes/custom/common_design_subtheme/templates/layout/page--404.html.twig +++ b/html/themes/custom/common_design_subtheme/templates/layout/page--404.html.twig @@ -57,10 +57,6 @@ {% endblock %} - {% block footer_soft %} - {% include '@common_design_subtheme/cd/cd-footer/cd-soft-footer.html.twig' %} - {% endblock %} - {% block footer %} {% include '@common_design_subtheme/cd/cd-footer/cd-footer.html.twig' %} {% endblock %} diff --git a/html/themes/custom/common_design_subtheme/templates/layout/page--410.html.twig b/html/themes/custom/common_design_subtheme/templates/layout/page--410.html.twig index 7d472484c..5100a8f1b 100644 --- a/html/themes/custom/common_design_subtheme/templates/layout/page--410.html.twig +++ b/html/themes/custom/common_design_subtheme/templates/layout/page--410.html.twig @@ -57,10 +57,6 @@ {% endblock %} - {% block footer_soft %} - {% include '@common_design_subtheme/cd/cd-footer/cd-soft-footer.html.twig' %} - {% endblock %} - {% block footer %} {% include '@common_design_subtheme/cd/cd-footer/cd-footer.html.twig' %} {% endblock %} diff --git a/html/themes/custom/common_design_subtheme/templates/layout/page--node--edit.html.twig b/html/themes/custom/common_design_subtheme/templates/layout/page--node--edit.html.twig index fdc34328b..817c12c89 100644 --- a/html/themes/custom/common_design_subtheme/templates/layout/page--node--edit.html.twig +++ b/html/themes/custom/common_design_subtheme/templates/layout/page--node--edit.html.twig @@ -24,9 +24,6 @@ {% endblock %} - {% block footer_soft %} - {% endblock %} - {% block footer %} {% include '@common_design_subtheme/cd/cd-footer/cd-footer.html.twig' %} {% endblock %} diff --git a/html/themes/custom/common_design_subtheme/templates/layout/page.html.twig b/html/themes/custom/common_design_subtheme/templates/layout/page.html.twig index 2cd2e09e1..2f187d914 100644 --- a/html/themes/custom/common_design_subtheme/templates/layout/page.html.twig +++ b/html/themes/custom/common_design_subtheme/templates/layout/page.html.twig @@ -4,10 +4,6 @@ {% include '@common_design_subtheme/cd/cd-header/cd-header.html.twig' %} {% endblock %} - {% block footer_soft %} - {% include '@common_design_subtheme/cd/cd-footer/cd-soft-footer.html.twig' %} - {% endblock %} - {% block footer %} {% include '@common_design_subtheme/cd/cd-footer/cd-footer.html.twig' %} {% endblock %} diff --git a/local/docker-compose.yml b/local/docker-compose.yml index 9ecd036c8..69f63944b 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -1,4 +1,3 @@ -version: "2.2" name: $PROJECT_NAME networks: