diff --git a/.github/workflows/composer-validate.yml b/.github/workflows/composer-validate.yml index f7d486f..a4065b7 100644 --- a/.github/workflows/composer-validate.yml +++ b/.github/workflows/composer-validate.yml @@ -18,4 +18,4 @@ jobs: run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress \ No newline at end of file + run: composer install --prefer-dist --no-progress diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..480b45a --- /dev/null +++ b/.github/workflows/lint.yml @@ -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/ . diff --git a/README.md b/README.md index 3c351d3..4affe6d 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/bay_platform_dependencies.install b/bay_platform_dependencies.install index 8bb6f50..b14338a 100644 --- a/bay_platform_dependencies.install +++ b/bay_platform_dependencies.install @@ -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; @@ -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(); @@ -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}"; @@ -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(); -} \ No newline at end of file +} + +/** + * 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) { + $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); +} diff --git a/bay_platform_dependencies.module b/bay_platform_dependencies.module index c0099d6..663def3 100644 --- a/bay_platform_dependencies.module +++ b/bay_platform_dependencies.module @@ -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(). @@ -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"; } @@ -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); @@ -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; } @@ -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;