From 01a89a186e2cbc77389573ab2402f5b206a0a74a Mon Sep 17 00:00:00 2001
From: Bob Vincent <bobvin@pillars.net>
Date: Tue, 10 Apr 2012 13:57:08 -0400
Subject: [PATCH] Issue #1508104 by znerol: Replace dynamically-generated
 class with delegation.

---
 MailsystemDelegateMailSystem.inc |   42 +++++
 mailsystem.admin.inc             |  329 ++++++++++++++++++++++++++------------
 mailsystem.info                  |    1 +
 mailsystem.module                |  190 +++++++++-------------
 4 files changed, 344 insertions(+), 218 deletions(-)
 create mode 100644 MailsystemDelegateMailSystem.inc

diff --git a/MailsystemDelegateMailSystem.inc b/MailsystemDelegateMailSystem.inc
new file mode 100644
index 0000000000000000000000000000000000000000..5ca610e0e246ddc9e76edac838fee7ac8c0620a9
--- /dev/null
+++ b/MailsystemDelegateMailSystem.inc
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Helper class for dispatching calls when different classes are responsible
+ * for formatting and actually sending a message.
+ */
+
+
+/**
+ * Implements a MailSystemInterface which simply forwards calls to another
+ * MailSystem class configured previously.
+ */
+class MailsystemDelegateMailSystem implements MailSystemInterface {
+
+  /**
+   * Implements MailSystemInterface::format().
+   *
+   * Retrieve the configured class responsible for formatting a message of a
+   * given module and mail-key. Forwards the formatting to that class.
+   */
+  public function format(array $message) {
+    $module = $message['module'];
+    $key = $message['key'];
+    $mailsystem = _mailsystem_delegate_get_mailsystem($module, $key, 'format');
+    return $mailsystem->format($message);
+  }
+
+
+  /**
+   * Implements MailSystemInterface::mail().
+   *
+   * Retrieve the configured class responsible for delivering a message of a
+   * given module and mail-key. Forwards the call to that class.
+   */
+  public function mail(array $message) {
+    $module = $message['module'];
+    $key = $message['key'];
+    $mailsystem = _mailsystem_delegate_get_mailsystem($module, $key, 'mail');
+    return $mailsystem->mail($message);
+  }
+}
diff --git a/mailsystem.admin.inc b/mailsystem.admin.inc
index 2dca212cae652377ae58fb2f348cfdf634cc4c57..734bf89015da4613d26229839e852bc94aab83be 100644
--- a/mailsystem.admin.inc
+++ b/mailsystem.admin.inc
@@ -2,7 +2,11 @@
 
 /**
  * @file
- * Administrative form for setting the mail_system variable.
+ * Administrative interface for the mail_system variable.
+ */
+
+/**
+ * Form constructor for the mailsystem settings form.
  */
 function mailsystem_admin_settings() {
   $args = array(
@@ -11,26 +15,19 @@ function mailsystem_admin_settings() {
     '!format' => url('http://api.drupal.org/api/drupal/includes--mail.inc/function/MailSystemInterface%3A%3Aformat/7'),
     '@format' => 'format()',
     '!mail' => url('http://api.drupal.org/api/drupal/includes--mail.inc/function/MailSystemInterface%3A%3Amail/7'),
+    '!hook_mail' => 'http://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_mail/7',
     '@mail' => 'mail()',
     '!default_class' => url('http://api.drupal.org/api/drupal/modules--system--system.mail.inc/class/DefaultMailSystem/7'),
     '@default_class' => mailsystem_default_value(),
     '%module' => 'module',
     '%key' => 'key',
   );
-  $form = array('#submit' => array('mailsystem_admin_settings_submit'));
-  $mail_system = mailsystem_get();
-  $mail_defaults = mailsystem_defaults();
-  $mailsystem_classes = mailsystem_get_classes();
-  $descriptions = array();
-  foreach (system_rebuild_module_data() as $item) {
-    if ($item->status) {
-      $descriptions[$item->name] = (
-        empty($item->info['package'])
-        ? '' : $item->info['package']
-      ) . ' » ' . t('!module module', array('!module' => $item->info['name']));
-    }
-  }
-  asort($descriptions);
+
+  $mail_system = mailsystem_admin_read_settings();
+  $delivery_classes = mailsystem_admin_get_delivery_classes();
+  $formatter_classes = mailsystem_admin_get_formatter_classes();
+  $mail_modules = mailsystem_admin_get_mail_modules();
+
   $form['mailsystem'] = array(
     '#type' => 'fieldset',
     '#title' => t('Mail System Settings'),
@@ -41,110 +38,90 @@ function mailsystem_admin_settings() {
     '#tree' => TRUE,
   );
   $form['mailsystem'][mailsystem_default_id()] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Site-wide default mail system'),
+  );
+  $form['mailsystem'][mailsystem_default_id()]['mail'] = array(
+    '#type' => 'select',
+    '#title' => t('Delivery'),
+    '#options' => $delivery_classes,
+    '#default_value' => $mail_system[mailsystem_default_id()]['mail'],
+    '#description' => t('Class used to send the mail'),
+  );
+  $form['mailsystem'][mailsystem_default_id()]['format'] = array(
     '#type' => 'select',
-    '#title' => t(
-      'Site-wide default <a href="!interface"><code>@interface</code></a> class', $args
-    ),
-    '#options' => $mailsystem_classes,
-    '#default_value' => $mail_system[mailsystem_default_id()],
+    '#title' => t('Formatting'),
+    '#options' => $formatter_classes,
+    '#default_value' => $mail_system[mailsystem_default_id()]['format'],
+    '#description' => t('Class used to format the body of the mail'),
   );
-  $mailsystem_classes = array(
-    mailsystem_default_id() => t('Remove this setting.')
-  ) + $mailsystem_classes;
-  foreach (array_diff_key($mail_system, $mail_defaults) as $id => $class) {
-    // Separate $id into $module and $key.
-    $module = $id;
-    while ($module && empty($descriptions[$module])) {
-      // Remove a key from the end
-      $module = implode('_', explode('_', $module, -1));
-    }
-    // If an array key of the $mail_system variable is neither "default-system"
-    // nor begins with a module name, then it should be unset.
-    if (empty($module)) {
-      watchdog('mailsystem', "Removing bogus mail_system key %id.", array('%id' => $id), WATCHDOG_WARNING);
-      unset($mail_system[$id]);
-      continue;
-    }
-    // Set $title to the human-readable module name.
-    $title = preg_replace('/^.* » /', '', $descriptions[$module]);
-    if ($key = substr($id, strlen($module) + 1)) {
-      $title .= " ($key key)";
-    }
-    $title .= ' class';
+
+  unset($mail_system[mailsystem_default_id()]);
+
+  foreach ($mail_system as $id => $settings) {
     $form['mailsystem'][$id] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Custom settings for mail-id %id', array('%id' => $id)),
+    );
+    $form['mailsystem'][$id]['mail'] = array(
       '#type' => 'select',
-      '#title' => $title,
-      '#options' => $mailsystem_classes,
-      '#default_value' => $class,
+      '#title' => t('Delivery'),
+      '#options' => $delivery_classes,
+      '#default_value' => $settings['mail'],
+      '#description' => t('Class used to send the mail'),
     );
   }
-  // Generate a list of themes which may used to render emails.
-  $theme_options = array('current' => t('Current'), 'default' => t('Default'));
-  if (module_exists('domain_theme')) {
-    $theme_options['domain'] = t('Domain Theme');
-  }
-  // Get a list of all themes.
-  $themes = list_themes();
-  foreach ($themes as $name => $theme) {
-    if ($theme->status == 1) {
-      $theme_options[$name] = $theme->info['name'];
-    }
-  }
-  $form['mailsystem']['mailsystem_theme'] = array(
+    $form['mailsystem'][$id]['format'] = array(
       '#type' => 'select',
-      '#title' => t('Theme to render the emails'),
-      '#description' => t('Select the theme that will be used to render the emails. This can be either the current theme, the default theme, the domain theme or any active theme.'),
-      '#options' => $theme_options,
-      '#default_value' => variable_get('mailsystem_theme', 'current'),
-  );
-  $form['class'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('New Class'),
-    '#description' => t(
-      'Create a new <a href="!interface"><code>@interface</code></a> that inherits its methods from other classes. The new class will be named after the other classes it uses.', $args
-    ),
-    '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
-    '#tree' => TRUE,
-  );
-  $mailsystem_classes[mailsystem_default_id()] = '--Select--';
-  $form['class']['format'] = array(
-    '#type' => 'select',
-    '#title' => t(
-      'Class to use for the <a href="!format"><code>@format</code></a> method', $args
-    ),
-    '#options' => $mailsystem_classes,
-  );
-  $form['class']['mail'] = array(
-    '#type' => 'select',
-    '#title' => t(
-      'Class to use for the <a href="!mail"><code>@mail</code></a> method', $args
-    ),
-    '#options' => $mailsystem_classes,
-  );
-  $form['identifier'] = array(
+      '#title' => t('Formatting'),
+      '#options' => $formatter_classes,
+      '#default_value' => $settings['format'],
+      '#description' => t('Class used to format the body of the mail'),
+    );
+    $form['mailsystem'][$id]['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Remove custom settings for mail-id @id', array('@id' => $id)),
+      '#submit' => array('mailsystem_admin_remove_setting_submit'),
+    );
+  }
+
+  $form['mailsystem']['add-custom-settings'] = array(
     '#type' => 'fieldset',
-    '#title' => t('New Setting'),
-    '#description' => t('Add a new %module and %key to the settings list.',
-      array(
-        '%module' => 'module',
-        '%key' => 'key',
-      )
-    ),
+    '#title' => t('Add custom settings'),
     '#collapsible' => TRUE,
     '#collapsed' => TRUE,
-    '#tree' => TRUE,
+    '#element_validate' => array('mailsystem_admin_add_setting_validate'),
   );
-  array_unshift($descriptions, t('-- Select --'));
-  $form['identifier']['module'] = array(
+  $form['mailsystem']['add-custom-settings']['module'] = array(
     '#type' => 'select',
     '#title' => t('Module'),
-    '#options' => $descriptions,
+    '#options' => $mail_modules,
   );
-  $form['identifier']['key'] = array(
+  $form['mailsystem']['add-custom-settings']['key'] = array(
     '#type' => 'textfield',
     '#title' => t('Key'),
-    '#size' => 80,
+    '#size' => 30,
+    '#description' => t('An optional key which further specifies the mail in question. You may have to examine the source code of the <a href="!hook_mail">hook_mail</a> implementation of the module in question in order to find an appropriate value', $args),
+  );
+  $form['mailsystem']['add-custom-settings']['mail'] = array(
+    '#type' => 'select',
+    '#title' => t('Delivery'),
+    '#options' => $delivery_classes,
+    '#description' => t('Class used to send the mail'),
+  );
+  $form['mailsystem']['add-custom-settings']['format'] = array(
+    '#type' => 'select',
+    '#title' => t('Formatting'),
+    '#options' => $formatter_classes,
+    '#description' => t('Class used to format the body of the mail'),
+  );
+  $form['mailsystem']['add-custom-settings']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Add custom settings'),
+    '#submit' => array('mailsystem_admin_add_setting_submit'),
+    '#limit_validation_errors' => array(
+      array('mailsystem', 'add-custom-settings'),
+    ),
   );
   $form['submit'] = array(
     '#type' => 'submit',
@@ -154,7 +131,151 @@ function mailsystem_admin_settings() {
 }
 
 /**
- * Processes mailsystem_admin_settings form.
+ * Form validation handler for mailsystem settings form.
+ *
+ * Add an additional custom setting for a module/key.
+ */
+function mailsystem_admin_add_setting_validate($fieldset, &$form_state) {
+  $module = $fieldset['module']['#value'];
+  $key = $fieldset['key']['#value'];
+  $id = $module;
+  if (!empty($key)) {
+    $id .= '_' . $key;
+   }
+
+  // @todo: Check whether we already have a setting for this id.
+
+  $form_state['new_settings_id'] = $id;
+}
+
+/**
+ * Form API submit callback: Add an additional custom setting for a module/key.
+ */
+function mailsystem_admin_add_setting_submit($form, &$form_state) {
+  $values = $form_state['values']['mailsystem']['add-custom-settings'];
+
+  $id = $form_state['new_settings_id'];
+  mailsystem_admin_save_custom_setting($id, $values);
+
+  drupal_set_message(t('Added new custom setting for mail-id %id',
+    array('%id' => $id)));
+}
+
+/**
+ * Form API submit callback: Remove a custom setting.
+ */
+function mailsystem_admin_remove_setting_submit($form, &$form_state) {
+  $parents = $form_state['triggering_element']['#parents'];
+  array_pop($parents); // submit
+  $id = array_pop($parents); // fieldset
+
+  mailsystem_clear(array($id => $id));
+
+  drupal_set_message(t('Removed custom setting for mail-id %id',
+    array('%id' => $id)));
+}
+
+/**
+ * Form API submit callback: save settings.
+ */
+function mailsystem_admin_settings_submit($form, &$form_state) {
+  unset($form_state['values']['mailsystem']['add-custom-settings']);
+
+  foreach ($form_state['values']['mailsystem'] as $id => $values) {
+    mailsystem_admin_save_custom_setting($id, $values);
+  }
+
+  drupal_set_message(t('Saved settings for mailsystem'));
+}
+
+
+/**
+ * Helper function. Writes the setting values for a given mail-id.
+ */
+function mailsystem_admin_save_custom_setting($id, $values) {
+  if ($values['format'] != $values['mail']) {
+    $value = array(
+      'mail' => $values['mail'],
+      'format' => $values['format'],
+    );
+  }
+  else {
+    $value = $values['mail'];
+  }
+  mailsystem_set(array($id => $value));
+}
+
+/**
+ * Retrieves a list of modules implementing hook_mail().
+ *
+ * @return array
+ *   An associative array of module descriptions keyed by the corresponding
+ *   module machine name.
+ */
+function mailsystem_admin_get_mail_modules() {
+  $modules = module_implements('mail');
+  $result = array();
+
+  foreach ($modules as $module) {
+    $info = system_get_info('module', $module);
+    $description = empty($info['package']) ? t('Other') : $info['package'];
+    $description .= ' » ' . $info['name'];
+    $result[$module] = $description;
+  }
+
+  return $result;
+}
+
+/**
+ * Returns a list of module delegations.
+ *
+ * @todo Better docs.
+ *
+ * @return array
+ *    Description here.
+ */
+function mailsystem_admin_read_settings() {
+  $mail_system = mailsystem_get();
+
+  $settings = array();
+  foreach ($mail_system as $id => $class) {
+    if ($class == 'MailsystemDelegateMailSystem') {
+      $settings[$id] = variable_get('mailsystem_delegate:' . $id);
+    }
+    else {
+      $settings[$id]['mail'] = $class;
+      $settings[$id]['format'] = $class;
+    }
+  }
+
+  return $settings;
+}
+
+/**
+ * Returns a list of classes suitable for sending mail.
+ *
+ * @todo Better docs.
+ *
+ * @return array
+ *   Description here.
+ */
+function mailsystem_admin_get_delivery_classes() {
+  $delivery_classes = mailsystem_get_classes();
+  unset($delivery_classes['MailsystemDelegateMailSystem']);
+  return $delivery_classes;
+}
+
+/**
+ * Returns a list of classes suitable for formatting email.
+ */
+function mailsystem_admin_get_formatter_classes() {
+  $formatter_classes = mailsystem_get_classes();
+  unset($formatter_classes['MailsystemDelegateMailSystem']);
+  return $formatter_classes;
+}
+
+/**
+ * Form submission handler for mailsystem settings form.
  */
 function mailsystem_admin_settings_submit($form, &$form_state) {
   variable_set('mailsystem_theme', $form_state['values']['mailsystem']['mailsystem_theme']);
diff --git a/mailsystem.info b/mailsystem.info
index 616051ccaaa3c43a8e2a40675406b078c1bb99f4..ca28902d5cd0c7916da7dbac97cd8dc333930eca 100644
--- a/mailsystem.info
+++ b/mailsystem.info
@@ -5,3 +5,4 @@ php = 5.0
 core = 7.x
 configure = admin/config/system/mailsystem
 dependencies[] = filter
+files[] = MailsystemDelegateMailSystem.inc
diff --git a/mailsystem.module b/mailsystem.module
index e414280b252f540186c9445dea2e48938d0a057c..e795932ee5e3d9434a5dc0ac4a75eec9efd3fed7 100644
--- a/mailsystem.module
+++ b/mailsystem.module
@@ -91,115 +91,6 @@ function mailsystem_get() {
   );
 }
 
-/**
- * Returns the default list of MailSystemInterface methods.
- *
- * @return
- *   An array whose keys are the names of the methods defined by
- *   MailSystemInterface and whose values are the default class used to
- *   provide that method.
- */
-function mailsystem_default_methods() {
-  $mail_system = mailsystem_get();
-  $default_class = $mail_system[mailsystem_default_id()];
-  $methods = get_class_methods('MailSystemInterface');
-  return array_combine(
-    $methods,
-    array_fill(0, count($methods), $default_class)
-  );
-}
-
-/**
- * Creates and registers a new MailSystemInterface class.
- *
- * The newly-created class gets its name and each of its class methods from the
- * other classes specified by the $class parameter.
- *
- * @param $class An associative array of ($method_name => $class_name) tuples,
- * where each $method_name is the name of a class method to be created, and
- * each $class_name is the name of a class to use for that method.
- *
- * @return
- *   The name of the newly-created class if successful; otherwise FALSE.
- */
-function mailsystem_create_class($classes) {
-  // Merge in defaults.
-  $classes += mailsystem_default_methods();
-  ksort($classes);
-  // Do not create a new class whose methods all derive from the same class.
-  if (count(array_unique($classes)) === 1) {
-    return FALSE;
-  }
-  $class_name = implode('__', $classes);
-  // Ensure that the mailsystem directory exists.
-  $class_dir = file_build_uri('mailsystem');
-  if (!file_prepare_directory($class_dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
-    return FALSE;
-  }
-  // Build the class filename.
-  $class_file = drupal_realpath($class_dir) . DIRECTORY_SEPARATOR . "$class_name.mail.inc";
-  // Strip DRUPAL_ROOT.
-  $drupal_root = drupal_realpath(DRUPAL_ROOT) . DIRECTORY_SEPARATOR;
-  $class_file = preg_replace('#^' . preg_quote($drupal_root, '#') . '#', '', $class_file);
-  // Build the class implementation as a string.
-  $class_contents = '<?php
-class ' . $class_name . ' implements MailSystemInterface {';
-  // Create a protected variable to hold each method class.
-  foreach (array_keys($classes) as $method) {
-    $class_contents .= '
-  protected $' . $method . 'Class;';
-  }
-  // Create a class construction function to populate the variables.
-  $class_contents .= '
-  public function __construct() {';
-  foreach ($classes as $method => $class) {
-    $class_contents .= '
-    if (drupal_autoload_class(\'' . $class . '\')) {
-      $this->' . $method . 'Class = new ' . $class . ';
-    }
-    else {
-      $this->' . $method . 'Class = new ' . mailsystem_default_value() . ';
-    }';
-  }
-  $class_contents .= '
-  }';
-  // Create each class method.
-  foreach (array_keys($classes) as $method) {
-    $class_contents .= '
-  public function ' . $method . '(array $message) {
-    return $this->' . $method . 'Class->' . $method . '($message);
-  }';
-  }
-  $class_contents .= '
-}
-';
-  if (file_unmanaged_save_data($class_contents, $class_file, FILE_EXISTS_REPLACE)) {
-    // Remove any conflicting registry entries to avoid a database error.
-    $class_condition = db_and()
-      ->condition('name', $class_name)
-      ->condition('type', 'class');
-    $file_condition = db_and()
-      ->condition('filename', $class_file);
-    db_delete('registry_file')
-      ->condition($file_condition);
-    db_delete('registry')->condition(
-      db_or()->condition($class_condition)
-      ->condition($file_condition)
-    );
-    // Make sure that registry functions are available.
-    require_once 'includes/registry.inc';
-    // Parse the newly-created class file and add it to the registry.
-    _registry_parse_file($class_file, $class_contents, 'mailsystem');
-    // Clear the mailsystem cache so that it will pick up the new class.
-    drupal_static_reset('mailsystem_get_classes');
-    drupal_set_message(
-      t('Class <code>%class</code> written to <code>%file</code>.',
-        array('%class' => $class_name, '%file' => $class_file)
-      )
-    );
-  }
-  return $class_name;
-}
 
 /**
  * Helps other modules safely set their own key within mail_system.  This
@@ -214,16 +105,15 @@ class ' . $class_name . ' implements MailSystemInterface {';
  *       defined by MailSystemInterface and whose values are the names of
  *       the class to use for that method.
  *
- * @see drupal_mail(), mailsystem_default_methods()
+ * @see drupal_mail()
  */
 function mailsystem_set(array $setting) {
   $mail_system = mailsystem_get();
   foreach ($setting as $key => $class) {
+    variable_del("mailsystem_delegate:$key");
     if (is_array($class)) {
-      unset($setting[$key]);
-      if ($new_class = mailsystem_create_class($class)) {
-        $setting[$key] = $new_class;
-      }
+      // Save the settings for our delegateer-class into a variable
+      $setting[$key] = mailsystem_delegate_set_mailsystem_settings($key, $class);
     }
   }
   variable_set('mail_system', array_merge(mailsystem_get(), $setting));
@@ -251,6 +141,78 @@ function mailsystem_clear(array $setting) {
       array_diff_key(array_diff(mailsystem_get(), $setting), $setting)
     )
   );
+  foreach (array_keys($setting) as $key) {
+    variable_del("mailsystem_delegate:$key");
+  }
+}
+
+/**
+ * Save settings for the delegate mail system for a given mail-id.
+ *
+ * @param $id is the machine-readable module name optionally followed by '_'
+ *       and a key.
+ * @param $map An associative array whose keys are the names of methods
+ *       defined by MailSystemInterface and whose values are the names of
+ *       the class to use for that method.
+ *
+ * @return A string specifying the name of the delegator class.
+ */
+function mailsystem_delegate_set_mailsystem_settings($id, $settings) {
+  variable_set('mailsystem_delegate:' . $id, $settings);
+  return 'MailsystemDelegateMailSystem';
+}
+
+/**
+ * Returns an MailSystemInterface class instance for a given action.
+ *
+ *  of a class implementing the MailSystemInterface
+ * responsible for performing the given action.
+ *
+ * @param string $module
+ *   The machine-readable module name.
+ * @param string $key
+ *   The email key.
+ * @param string $action
+ *   The name of the MailSystemInterface method to be invoked. Should be one of:
+ *   - format: Format a message composed by drupal_mail() prior to sending.
+ *   - mail: Send a message composed by drupal_mail().
+ *
+ * @return MailSystemInterface
+ *
+ * @see drupal_mail_system().
+ */
+function _mailsystem_delegate_get_mailsystem($module, $key, $action) {
+  $instances = &drupal_static(__FUNCTION__, array());
+
+  $settings = variable_get('mailsystem_delegate:' . $module . '_' . $key);
+  if ($settings === NULL) {
+    $settings = variable_get('mailsystem_delegate:' . $module);
+  }
+
+  // Use the class configured for the given action.
+  if ($settings && isset($settings[$action])) {
+    $class = $settings[$action];
+  }
+  else {
+    $mailsystems = mailsystem_get();
+    $class = $mailsystems[mailsystem_default_id()];
+
+    // Fallback to drupal default in order to prevent an infinite recursion.
+    if ($class == 'MailsystemDelegateMailSystem') {
+      $class = mailsystem_default_value();
+    }
+  }
+
+  if (empty($instances[$class])) {
+    $interfaces = class_implements($class);
+    if (isset($interfaces['MailSystemInterface'])) {
+      $instances[$class] = new $class();
+    }
+    else {
+      throw new Exception(t('Class %class does not implement interface %interface', array('%class' => $class, '%interface' => 'MailSystemInterface')));
+    }
+  }
+  return $instances[$class];
 }
 
 /**
-- 
1.7.9.1

