Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/composer-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:
run: composer validate

- name: Install dependencies
run: composer install --prefer-dist --no-progress
run: composer install --prefer-dist --no-progress
30 changes: 30 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Drupal Coding Standards

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
phpcs:
name: PHPCS
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
coverage: none

- name: Install PHPCS with Drupal standards
run: |
composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer global require drupal/coder squizlabs/php_codesniffer dealerdirect/phpcodesniffer-composer-installer slevomat/coding-standard
phpcs --config-set installed_paths $HOME/.composer/vendor/slevomat/coding-standard,$HOME/.composer/vendor/drupal/coder/coder_sniffer

- name: Run PHPCS
run: |
phpcs --standard=Drupal,DrupalPractice --extensions=php,module,inc,install,test,profile,theme,css,info,txt,md,yml --ignore=vendor/,node_modules/ .
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ Drupal configuration for Bay hosting platform integrations.
This module handles patching of the [Redis](https://www.drupal.org/project/redis) module with a few key features

1. Adds support for RedisCluster client.
1. Adds NewRelic transactions for RedisCluster operations (please see docs in patches directory for how to reroll this patch)
1. Adds NewRelic transactions for RedisCluster operations (please see docs in
patches directory for how to reroll this patch)
214 changes: 205 additions & 9 deletions bay_platform_dependencies.install
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/**
* @file
* Install, update and uninstall functions for the bay-platform-dependencies module.
* Install, update, uninstall functions for bay-platform-dependencies module.
*/

use Drush\Drush;
Expand All @@ -22,20 +22,20 @@ function bay_platform_dependencies_uninstall() {
}

/**
* Update section purger plugin to sectionbundled
* Update section purger plugin to sectionbundled.
*/
function bay_platform_dependencies_update_10001() {
$config_factory = \Drupal::configFactory();

// Update section purger plugin settings
// Update section purger plugin settings.
$purge_plugins_config = $config_factory->getEditable('purge.plugins');
$purgers = $purge_plugins_config->get('purgers');

if (is_array($purgers)) {
foreach ($purgers as &$purger) {
// Only update plugin_id for section purger
// Only update plugin_id for section purger.
if (isset($purger['plugin_id']) && $purger['plugin_id'] === "section") {
// Update the plugin_id to section bundled
// Update the plugin_id to section bundled.
$purger['plugin_id'] = 'sectionbundled';
$purge_plugins_config->set('purgers', $purgers);
$purge_plugins_config->save();
Expand All @@ -44,15 +44,15 @@ function bay_platform_dependencies_update_10001() {
}
}

// Update section purger logger channels settings
// Update section purger logger channels settings.
$purge_logger_channels_config = $config_factory->getEditable('purge.logger_channels');
$channels = $purge_logger_channels_config->get('channels');
if (is_array($channels)) {
foreach ($channels as &$channel) {
// Only update channel id for section purger
// Only update channel id for section purger.
if (isset($channel['id']) && strpos($channel['id'], "purger_section_") === 0) {
// Update the id to section bundled
// channel id is in the format of purger_section_{purger_id}
// channel id is in the format of purger_section_{purger_id}.
$parts = explode('_', $channel['id']);
$purger_id = end($parts);
$channel['id'] = "purger_sectionbundled_{$purger_id}";
Expand Down Expand Up @@ -97,4 +97,200 @@ function bay_platform_dependencies_update_10003() {
function bay_platform_dependencies_update_10004() {
$process = Drush::drush(Drush::aliasManager()->getSelf(), 'section_purger:install_sensor');
$process->run();
}
}

/**
* Fix webform email handler configurations.
*
* - Ensures valid values for from_mail, return_path, and sender_mail fields.
* - Preserves original from_mail in reply_to.
*/
function bay_platform_dependencies_update_10005(&$sandbox) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is the meat and potatoes of this PR.

$message = [];
$translation = \Drupal::translation();

// Initialize batch processing if this is the first pass.
if (!isset($sandbox['progress'])) {
$sandbox['progress'] = 0;
$sandbox['current_id'] = 0;
$sandbox['max'] = \Drupal::entityTypeManager()->getStorage('webform')->getQuery()->count()->execute();

// If there are no webforms, return immediately.
if ($sandbox['max'] == 0) {
$message[] = $translation->translate('No webforms found to update.');
return $message;
}

// Initialize counters.
$sandbox['fixed_count'] = 0;
$sandbox['webforms_processed'] = 0;
}

// List of valid values for the settings.
$validValues = ['[site:mail]', '', '_default'];

// Add values from SMTP_FROM_WHITELIST environment variable if it exists.
$whitelist = getenv('SMTP_FROM_WHITELIST');
if ($whitelist) {
$whitelistEmails = explode(',', $whitelist);
// Trim whitespace from each email.
$whitelistEmails = array_map('trim', $whitelistEmails);
// Add whitelisted emails to valid values.
$validValues = array_merge($validValues, $whitelistEmails);
$message[] = $translation->translate('Added @count email addresses from SMTP_FROM_WHITELIST to valid values.',
['@count' => count($whitelistEmails)]);
}

// Process webforms in chunks.
// Number of webforms to process per batch.
$limit = 20;
$webform_ids = \Drupal::entityTypeManager()->getStorage('webform')
->getQuery()
->condition('id', $sandbox['current_id'], '>')
->sort('id')
->range(0, $limit)
->execute();

if (empty($webform_ids)) {
// If no webforms were found, we're done.
$sandbox['#finished'] = 1;
return $message;
}

$webforms = \Drupal::entityTypeManager()->getStorage('webform')->loadMultiple($webform_ids);

foreach ($webforms as $webform) {
$webformName = $webform->label();
$webformId = $webform->id();
$webformChanged = FALSE;

// Get email handlers from the webform.
$handlers = $webform->getHandlers();

foreach ($handlers as $handler) {
// Check if it's an email handler.
if ($handler->getPluginId() === 'email') {
$configuration = $handler->getConfiguration();
$settings = &$configuration['settings'];
$handlerChanged = FALSE;

$handlerLabel = $handler->getLabel();

// Check if return_path is not in the valid values list.
if (isset($settings['return_path']) && !in_array($settings['return_path'], $validValues)) {
$message[] = $translation->translate(
'Webform: @name (ID: @id), Email Handler: @handler, return_path is set to "@value" instead of a valid value',
[
'@name' => $webformName,
'@id' => $webformId,
'@handler' => $handlerLabel,
'@value' => $settings['return_path'],
],
);

// Set return_path to empty string.
$oldValue = $settings['return_path'];
$settings['return_path'] = '';
$message[] = $translation->translate(' - FIXED: Changed return_path from "@old" to ""', ['@old' => $oldValue]);
$handlerChanged = TRUE;
$sandbox['fixed_count']++;
}

// Check if sender_mail is not in the valid values list.
if (isset($settings['sender_mail']) && !in_array($settings['sender_mail'], $validValues)) {
$message[] = $translation->translate(
'Webform: @name (ID: @id), Email Handler: @handler, sender_mail is set to "@value" instead of a valid value',
[
'@name' => $webformName,
'@id' => $webformId,
'@handler' => $handlerLabel,
'@value' => $settings['sender_mail'],
],
);

// Set sender_mail to empty string.
$oldValue = $settings['sender_mail'];
$settings['sender_mail'] = '';
$message[] = $translation->translate(' - FIXED: Changed sender_mail from "@old" to ""', ['@old' => $oldValue]);
$handlerChanged = TRUE;
$sandbox['fixed_count']++;
}

// Check if from_mail is not in the valid values list and fix it by
// setting to '_default'.
if (isset($settings['from_mail']) && !in_array($settings['from_mail'], $validValues)) {
$message[] = $translation->translate(
'Webform: @name (ID: @id), Email Handler: @handler, from_mail is set to "@value" instead of a valid value',
[
'@name' => $webformName,
'@id' => $webformId,
'@handler' => $handlerLabel,
'@value' => $settings['from_mail'],
],
);

// Store the current from_mail value before changing it.
$oldFromMail = $settings['from_mail'];

// Set from_mail to '_default'.
$settings['from_mail'] = '_default';
$message[] = $translation->translate(' - FIXED: Changed from_mail from "@old" to "_default"', ['@old' => $oldFromMail]);

// Also update reply_to to preserve the original email for replies.
$oldReplyTo = $settings['reply_to'] ?? '';
$settings['reply_to'] = $oldFromMail;
$message[] = $translation->translate(' - UPDATED: Set reply_to from "@old" to "@new"', [
'@old' => $oldReplyTo,
'@new' => $oldFromMail,
]);

$handlerChanged = TRUE;
$sandbox['fixed_count']++;
}

// Update the handler configuration if changes were made.
if ($handlerChanged) {
$handler->setConfiguration($configuration);
$webformChanged = TRUE;
}
}
}

// Save the webform if any handlers were updated.
if ($webformChanged) {
$webform->save();
$message[] = $translation->translate('Saved changes to webform: @name (ID: @id)', [
'@name' => $webformName,
'@id' => $webformId,
]);
}

// Update progress information.
$sandbox['progress']++;
$sandbox['webforms_processed']++;
$sandbox['current_id'] = $webformId;
}

// Set the value for finished. If there are no webforms, we're done.
$sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);

// If we're done, provide a summary message.
if ($sandbox['#finished'] >= 1) {
if ($sandbox['fixed_count'] > 0) {
$message[] = $translation->translate('Summary: Fixed @count issues with the following settings:', ['@count' => $sandbox['fixed_count']]);
$message[] = $translation->translate('- return_path and sender_mail set to "" (empty string)');
$message[] = $translation->translate('- from_mail set to "_default" and moved original from_mail value to reply_to');

$validValuesList = "'[site:mail]', '' (empty string), '_default'";
if ($whitelist) {
$validValuesList .= ', and whitelisted emails from SMTP_FROM_WHITELIST';
}
$message[] = $translation->translate('Valid values include: @values.', ['@values' => $validValuesList]);
}
else {
$message[] = $translation->translate('No issues found. All email handlers in webforms have correct return_path, sender_mail, and from_mail settings.');
}
}

return implode("\n", $message);
}
31 changes: 19 additions & 12 deletions bay_platform_dependencies.module
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
* Primary module hooks for bay-platform-dependencies module.
*/

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\BubbleableMetadata;

const BPD_ENV_SMTP_ALLOWLIST = "SMTP_FROM_WHITELIST";
const K8S_SERVICE_ACCOUNT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
// phpcs:disable
const BAY_PLATFORM_DEPENDENCIES_ENV_SMTP_ALLOWLIST = "SMTP_FROM_WHITELIST";
const BAY_PLATFORM_DEPENDENCIES_K8S_SERVICE_ACCOUNT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
// phpcs:enable

/**
* Implements hook_mail_alter().
Expand All @@ -30,28 +33,31 @@ function bay_platform_dependencies_mail_alter(&$message) {
/**
* Implements hook_form_FORM_ID_alter().
*/
function bay_platform_dependencies_form_webform_handler_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
function bay_platform_dependencies_form_webform_handler_form_alter(&$form, FormStateInterface $form_state) {
$smtp_allowlist = _bay_platform_dependencies_smtp_allowlist();
if (!$smtp_allowlist) {
return;
}

_bay_platform_dependencies_form_webform_handler_form_options($form["settings"]["from"]["from_mail"]["from_mail"], $smtp_allowlist);
_bay_platform_dependencies_form_webform_handler_form_options($form["settings"]["additional"]["return_path"]["return_path"], $smtp_allowlist);
_bay_platform_dependencies_form_webform_handler_form_options($form["settings"]["additional"]["sender_mail"]["sender_mail"], $smtp_allowlist);
}

/**
* Helper to alter form elements related to webform email handlers.
*/
function _bay_platform_dependencies_form_webform_handler_form_options(&$element, array $smtp_allowlist) {
// Remove ability to choose element values.
unset($element['#options']["Elements"]);
unset($element['#options']["Options"]);

// Remove ability to choose contextual values.
$element['#options']["Other"] = [];
foreach ($smtp_allowlist as $email) {
$element['#options']["Other"][$email] = $email;
}

// Add validation to ensure "other" values meet allowlist.
$element["#element_validate"][] = "bay_platform_dependencies_form_webform_handler_form_element_validate";
}
Expand All @@ -64,7 +70,7 @@ function bay_platform_dependencies_form_webform_handler_form_element_validate($e
if (empty($value) || $value == "_default") {
return;
}

if (!in_array($value, _bay_platform_dependencies_smtp_allowlist())) {
$error = \Drupal::translation()->translate("Disallowed email address submitted - %email", ["%email" => $value]);
$form_state->setErrorByName(implode("][", $element['#parents']), $error);
Expand All @@ -79,7 +85,7 @@ function bay_platform_dependencies_form_webform_handler_form_element_validate($e
* FALSE is not configured.
*/
function _bay_platform_dependencies_smtp_allowlist() {
$list = getenv(BPD_ENV_SMTP_ALLOWLIST);
$list = getenv(BAY_PLATFORM_DEPENDENCIES_ENV_SMTP_ALLOWLIST);
if (empty($list)) {
return FALSE;
}
Expand Down Expand Up @@ -109,13 +115,14 @@ function bay_platform_dependencies_tokens($type, $tokens, array $data, array $op
foreach ($tokens as $name => $original) {
switch ($name) {
case 'k8s-service-account-token':
if (file_exists(K8S_SERVICE_ACCOUNT_PATH)) {
$token = file_get_contents(K8S_SERVICE_ACCOUNT_PATH);
if (file_exists(BAY_PLATFORM_DEPENDENCIES_K8S_SERVICE_ACCOUNT_PATH)) {
$token = file_get_contents(BAY_PLATFORM_DEPENDENCIES_K8S_SERVICE_ACCOUNT_PATH);
if ($token === FALSE) {
throw new \Exception(sprintf("Failed to read service account token at %s", K8S_SERVICE_ACCOUNT_PATH));
throw new \Exception(sprintf("Failed to read service account token at %s", BAY_PLATFORM_DEPENDENCIES_K8S_SERVICE_ACCOUNT_PATH));
}
$replacements[$original] = $token;
} else {
}
else {
$replacements[$original] = '';
}
break;
Expand Down