فهرست منبع

patched for email sending on each feedback creation https://drupal.org/node/353548#comment-7130746

Bachir Soussi Chiadmi 10 سال پیش
والد
کامیت
235e57d8e9
4فایلهای تغییر یافته به همراه1231 افزوده شده و 4 حذف شده
  1. 338 0
      feedback-353548-45.patch
  2. 229 0
      feedback.module
  3. 593 0
      feedback.module.orig
  4. 71 4
      tests/feedback.test

+ 338 - 0
feedback-353548-45.patch

@@ -0,0 +1,338 @@
+diff --git a/feedback.module b/feedback.module
+index 9763575..ef5b24a 100644
+--- a/feedback.module
++++ b/feedback.module
+@@ -534,6 +534,235 @@ function feedback_user_delete($account) {
+ }
+ 
+ /**
++ * Implements hook_action_info().
++ */
++function feedback_action_info() {
++  return array(
++    'feedback_process_action' => array(
++      'label' => t('Process entry'),
++      'type' => 'feedback',
++      'configurable' => FALSE,
++      'behavior' => array('changes_property'),
++      'triggers' => array('feedback_insert', 'feedback_update'),
++    ),
++    'feedback_open_action' => array(
++      'label' => t('Open entry'),
++      'type' => 'feedback',
++      'configurable' => FALSE,
++      'behavior' => array('changes_property'),
++      'triggers' => array('feedback_insert', 'feedback_update'),
++    ),
++    'feedback_delete_action' => array(
++      'label' => t('Delete entry'),
++      'type' => 'feedback',
++      'configurable' => FALSE,
++      'triggers' => array('feedback_insert', 'feedback_update'),
++    ),
++    'feedback_send_email_action' => array(
++      'label' => t('Send e-mail of feedback'),
++      'type' => 'feedback',
++      'configurable' => TRUE,
++      'triggers' => array('feedback_insert', 'feedback_update'),
++    ),
++  );
++}
++
++/**
++* Implements hook_trigger_info().
++*/
++function feedback_trigger_info() {
++  return array(
++    'feedback' => array(
++      'feedback_insert' => array(
++        'label' => t('New feedback is added.'),
++      ),
++      'feedback_update' => array(
++        'label' => t('Feedback is marked processed or open.'),
++      ),
++    ),
++  );
++}
++
++/**
++ * Implements hook_feedback_insert().
++ */
++function feedback_feedback_insert($entry) {
++  $aids = trigger_get_assigned_actions('feedback_insert');
++  if (!$aids) {
++    return;
++  }
++
++  $context = array(
++    'group' => 'feedback',
++    'hook' => 'feedback_insert',
++  );
++
++  foreach ($aids as $aid => $info) {
++    actions_do($aid, $entry, $context);
++  }
++}
++
++/**
++ * Implements hook_feedback_update().
++ */
++function feedback_feedback_update($entry) {
++  $aids = trigger_get_assigned_actions('feedback_update');
++  if (!$aids) {
++    return;
++  }
++
++  $context = array(
++    'group' => 'feedback',
++    'hook' => 'feedback_update',
++  );
++
++  foreach ($aids as $aid => $info) {
++    actions_do($aid, $entry, $context);
++  }
++}
++
++/**
++ * Sets the status of an entry to processed.
++ *
++ * @ingroup actions
++ */
++function feedback_process_action($entry, $context) {
++  $entry->status = FEEDBACK_PROCESSED;
++  drupal_write_record('feedback', $entry, 'fid');
++}
++
++/**
++ * Sets the status of an entry to open.
++ *
++ * @ingroup actions
++ */
++function feedback_open_action($entry, $context) {
++  $entry->status = FEEDBACK_OPEN;
++  drupal_write_record('feedback', $entry, 'fid');
++}
++
++/**
++ * Deletes a feedback entry.
++ *
++ * @ingroup actions
++ */
++function feedback_delete_action($entry, $context) {
++  feedback_delete($entry->fid);
++}
++
++/**
++ * Return a form definition so the Feedback send email action can be configured.
++ *
++ * @param $context
++ *   Default values (if we are editing an existing action instance).
++ * @return
++ *   Form definition.
++ */
++function feedback_send_email_action_form($context = array()) {
++  // Set default values for form.
++  $context += array(
++   'recipients' => '',
++   'subject' => '',
++   'message' => '',
++  );
++
++  $form['recipients'] = array(
++    '#type' => 'textarea',
++    '#title' => t('Recipients'),
++    '#default_value' => $context['recipients'],
++    '#description' => t("Example: 'webmaster@example.com' or 'dev@example.com,support@example.com'. To specify multiple recipients, separate each e-mail address with a comma."),
++    '#required' => TRUE,
++  );
++  $form['subject'] = array(
++    '#type' => 'textfield',
++    '#title' => t('Subject'),
++    '#default_value' => $context['subject'],
++    '#maxlength' => '254',
++    '#description' => t('The subject of the message.'),
++    '#required' => TRUE,
++  );
++  $form['message'] = array(
++    '#type' => 'textarea',
++    '#title' => t('Message'),
++    '#default_value' => $context['message'],
++    '#cols' => '80',
++    '#rows' => '20',
++    '#description' => t('The message that should be sent. You may include the following variables: !uid, !username, !usermail, !useragent (of the user who gave the feedback), !site_name, !status, !message, !url, !date.'),
++    '#required' => TRUE,
++  );
++  return $form;
++}
++
++/**
++ * Validate the send e-mail action form submission.
++ */
++function feedback_send_email_action_validate($form, &$form_state) {
++  if (empty($form_state['values']['recipients'])) {
++    form_set_error('recipients', t('You must enter one or more recipients.'));
++  }
++  else {
++    $recipients = explode(',', $form_state['values']['recipients']);
++    foreach ($recipients as $recipient) {
++      if (!valid_email_address(trim($recipient))) {
++        form_set_error('recipients', t('%recipient is an invalid e-mail address.', array('%recipient' => $recipient)));
++      }
++    }
++  }
++}
++
++/**
++ * Process the send e-mail action form submission.
++ */
++function feedback_send_email_action_submit($form, $form_state) {
++  // Process the HTML form to store configuration. The keyed array that
++  // we return will be serialized to the database.
++  $params = array(
++    'recipients' => $form_state['values']['recipients'],
++    'subject'    => $form_state['values']['subject'],
++    'message'    => $form_state['values']['message'],
++  );
++  return $params;
++}
++
++/**
++ * Implements the feedback send e-mail action.
++ */
++function feedback_send_email_action($entry, $context) {
++  $account = user_load($entry->uid);
++  $from = variable_get('site_name', 'Drupal') . ' <' . variable_get('site_mail', '') . '>';
++  $params = array('entry' => $entry, 'account' => $account, 'context' => $context);
++  // Send the e-mail to the recipients using the site default language.
++  drupal_mail('feedback', 'send_email_action', $context['recipients'], language_default(), $params, $from);
++  watchdog('feedback', 'Feedback information sent to %recipients', array('%recipients' => $context['recipients']));
++}
++
++/**
++ * Implements hook_mail().
++ */
++function feedback_mail($key, &$message, $params) {
++  $language = $message['language'];
++  $entry = $params['entry'];
++  $account = $params['account'];
++  $context = $params['context'];
++  $variables = array(
++    '!site_name' => variable_get('site_name', 'Drupal'),
++    '!uid' => $account->uid,
++    '!username' => $account->name ? $account->name : t('Anonymous'),
++    '!usermail' => $account->mail ? $account->mail : t('unknown'),
++    '!status' => $entry->status ? t('Processed') : t('Open'),
++    '!message' => $entry->message,
++    '!url' => url($entry->location, array('absolute' => TRUE, 'language' => $language)),
++    '!useragent' => $entry->useragent,
++    '!date' => format_date($entry->timestamp, 'small', '', NULL, $language->language),
++  );
++  $subject = strtr($context['subject'], $variables);
++  $body = strtr($context['message'], $variables);
++  $message['subject'] .= str_replace(array("\r", "\n"), '', $subject);
++  $message['body'][] = drupal_html_to_text($body);
++}
++
++
++/**
+  * Implements hook_mollom_form_list().
+  */
+ function feedback_mollom_form_list() {
+diff --git a/tests/feedback.test b/tests/feedback.test
+index 4d4244c..09ce922 100644
+--- a/tests/feedback.test
++++ b/tests/feedback.test
+@@ -20,8 +20,7 @@ class FeedbackTestCase extends DrupalWebTestCase {
+   }
+ 
+   function setUp() {
+-    // @todo Remove soft-dependency on Block.
+-    parent::setUp(array('block', 'feedback'));
++    parent::setUp(array('feedback', 'trigger'));
+ 
+     $this->admin_user = $this->drupalCreateUser(array('access feedback form', 'view feedback messages', 'administer feedback'));
+     $this->web_user = $this->drupalCreateUser(array('access feedback form'));
+@@ -49,7 +48,7 @@ class FeedbackTestCase extends DrupalWebTestCase {
+     $edit = array(
+       'feedback-messages[0][1]' => TRUE,
+     );
+-    $this->drupalPost(NULL, $edit, t('Submit'));
++    $this->drupalPost(NULL, $edit, t('Update'));
+     $this->assertFieldByName('feedback-messages[1][1]', 1, t('Processed message found.'));
+   }
+ 
+@@ -89,4 +88,72 @@ class FeedbackTestCase extends DrupalWebTestCase {
+     $this->assertNoLinkByHref('admin/reports/feedback/1/delete');
+     $this->assertNoRaw(check_plain($message), t('Message not found.'));
+   }
+-}
++
++  /**
++   * Test the feedback triggers and actions.
++   */
++  function testFeedbackEmailAction() {
++    $test_user = $this->drupalCreateUser(array('administer actions', 'administer feedback', 'access feedback form'));
++    $this->drupalLogin($test_user);
++    $this->assignFeedbackEmailAction('feedback_insert', $test_user->mail);
++
++    // Insert a feedback entry.
++    $message = $this->randomString();
++    $edit = array(
++      'message' => $message,
++    );
++    $this->drupalPost('node', $edit, t('Send feedback'));
++    $this->assertFeedbackEmailTokenReplacement($test_user->mail, $message);
++
++    $this->assignFeedbackEmailAction('feedback_update', $test_user->mail);
++
++    // Update a feedback entry.
++    $fid = db_query("SELECT fid FROM {feedback} LIMIT 0,1")->fetchField();
++    $entry = feedback_load($fid);
++    feedback_save($entry);
++    $this->assertFeedbackEmailTokenReplacement($test_user->mail, $entry->message);
++  }
++
++  /**
++   * Assigns a system_send_email_action to the passed-in trigger.
++   *
++   * @param $trigger
++   *   For example, 'user_login'
++   */
++  function assignFeedbackEmailAction($trigger, $mail) {
++    $form_name = "trigger_{$trigger}_assign_form";
++    $form_html_id = strtr($form_name, '_', '-');
++
++    $edit = array(
++      'actions_label' => $trigger . "_feedback_send_email_action",
++      'recipients' => $mail,
++      'subject' => $this->randomName(),
++      'message' => '!message',
++    );
++
++    $hash = drupal_hash_base64('feedback_send_email_action');
++    $this->drupalPost("admin/config/system/actions/configure/$hash", $edit, t('Save'));
++    $this->assertText(t('The action has been successfully saved.'));
++
++    // Now we have to find out the action ID of what we created.
++    $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback AND label = :label', array(':callback' => 'feedback_send_email_action', ':label' => $edit['actions_label']))->fetchField();
++
++    $edit = array('aid' => drupal_hash_base64($aid));
++    $this->drupalPost('admin/structure/trigger/feedback', $edit, t('Assign'), array(), array(), $form_html_id);
++  }
++
++
++  /**
++   * Asserts correct token replacement for the given trigger and account.
++   *
++   * @param $account
++   *   The user account which triggered the action.
++   * @param $email_depth
++   *   Number of emails to scan, starting with most recent.
++   */
++  function assertFeedbackEmailTokenReplacement($mail, $message, $email_depth = 1) {
++    $this->verboseEmail($email_depth);
++    $this->assertMailString('body', $message, $email_depth);
++    $this->assertMail('to', $mail, t('Mail sent to correct destination'));
++  }
++}
+\ No newline at end of file

+ 229 - 0
feedback.module

@@ -549,6 +549,235 @@ function feedback_user_delete($account) {
   feedback_delete_multiple($fids);
 }
 
+/**
+ * Implements hook_action_info().
+ */
+function feedback_action_info() {
+  return array(
+    'feedback_process_action' => array(
+      'label' => t('Process entry'),
+      'type' => 'feedback',
+      'configurable' => FALSE,
+      'behavior' => array('changes_property'),
+      'triggers' => array('feedback_insert', 'feedback_update'),
+    ),
+    'feedback_open_action' => array(
+      'label' => t('Open entry'),
+      'type' => 'feedback',
+      'configurable' => FALSE,
+      'behavior' => array('changes_property'),
+      'triggers' => array('feedback_insert', 'feedback_update'),
+    ),
+    'feedback_delete_action' => array(
+      'label' => t('Delete entry'),
+      'type' => 'feedback',
+      'configurable' => FALSE,
+      'triggers' => array('feedback_insert', 'feedback_update'),
+    ),
+    'feedback_send_email_action' => array(
+      'label' => t('Send e-mail of feedback'),
+      'type' => 'feedback',
+      'configurable' => TRUE,
+      'triggers' => array('feedback_insert', 'feedback_update'),
+    ),
+  );
+}
+
+/**
+* Implements hook_trigger_info().
+*/
+function feedback_trigger_info() {
+  return array(
+    'feedback' => array(
+      'feedback_insert' => array(
+        'label' => t('New feedback is added.'),
+      ),
+      'feedback_update' => array(
+        'label' => t('Feedback is marked processed or open.'),
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_feedback_insert().
+ */
+function feedback_feedback_insert($entry) {
+  $aids = trigger_get_assigned_actions('feedback_insert');
+  if (!$aids) {
+    return;
+  }
+
+  $context = array(
+    'group' => 'feedback',
+    'hook' => 'feedback_insert',
+  );
+
+  foreach ($aids as $aid => $info) {
+    actions_do($aid, $entry, $context);
+  }
+}
+
+/**
+ * Implements hook_feedback_update().
+ */
+function feedback_feedback_update($entry) {
+  $aids = trigger_get_assigned_actions('feedback_update');
+  if (!$aids) {
+    return;
+  }
+
+  $context = array(
+    'group' => 'feedback',
+    'hook' => 'feedback_update',
+  );
+
+  foreach ($aids as $aid => $info) {
+    actions_do($aid, $entry, $context);
+  }
+}
+
+/**
+ * Sets the status of an entry to processed.
+ *
+ * @ingroup actions
+ */
+function feedback_process_action($entry, $context) {
+  $entry->status = FEEDBACK_PROCESSED;
+  drupal_write_record('feedback', $entry, 'fid');
+}
+
+/**
+ * Sets the status of an entry to open.
+ *
+ * @ingroup actions
+ */
+function feedback_open_action($entry, $context) {
+  $entry->status = FEEDBACK_OPEN;
+  drupal_write_record('feedback', $entry, 'fid');
+}
+
+/**
+ * Deletes a feedback entry.
+ *
+ * @ingroup actions
+ */
+function feedback_delete_action($entry, $context) {
+  feedback_delete($entry->fid);
+}
+
+/**
+ * Return a form definition so the Feedback send email action can be configured.
+ *
+ * @param $context
+ *   Default values (if we are editing an existing action instance).
+ * @return
+ *   Form definition.
+ */
+function feedback_send_email_action_form($context = array()) {
+  // Set default values for form.
+  $context += array(
+   'recipients' => '',
+   'subject' => '',
+   'message' => '',
+  );
+
+  $form['recipients'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Recipients'),
+    '#default_value' => $context['recipients'],
+    '#description' => t("Example: 'webmaster@example.com' or 'dev@example.com,support@example.com'. To specify multiple recipients, separate each e-mail address with a comma."),
+    '#required' => TRUE,
+  );
+  $form['subject'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Subject'),
+    '#default_value' => $context['subject'],
+    '#maxlength' => '254',
+    '#description' => t('The subject of the message.'),
+    '#required' => TRUE,
+  );
+  $form['message'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Message'),
+    '#default_value' => $context['message'],
+    '#cols' => '80',
+    '#rows' => '20',
+    '#description' => t('The message that should be sent. You may include the following variables: !uid, !username, !usermail, !useragent (of the user who gave the feedback), !site_name, !status, !message, !url, !date.'),
+    '#required' => TRUE,
+  );
+  return $form;
+}
+
+/**
+ * Validate the send e-mail action form submission.
+ */
+function feedback_send_email_action_validate($form, &$form_state) {
+  if (empty($form_state['values']['recipients'])) {
+    form_set_error('recipients', t('You must enter one or more recipients.'));
+  }
+  else {
+    $recipients = explode(',', $form_state['values']['recipients']);
+    foreach ($recipients as $recipient) {
+      if (!valid_email_address(trim($recipient))) {
+        form_set_error('recipients', t('%recipient is an invalid e-mail address.', array('%recipient' => $recipient)));
+      }
+    }
+  }
+}
+
+/**
+ * Process the send e-mail action form submission.
+ */
+function feedback_send_email_action_submit($form, $form_state) {
+  // Process the HTML form to store configuration. The keyed array that
+  // we return will be serialized to the database.
+  $params = array(
+    'recipients' => $form_state['values']['recipients'],
+    'subject'    => $form_state['values']['subject'],
+    'message'    => $form_state['values']['message'],
+  );
+  return $params;
+}
+
+/**
+ * Implements the feedback send e-mail action.
+ */
+function feedback_send_email_action($entry, $context) {
+  $account = user_load($entry->uid);
+  $from = variable_get('site_name', 'Drupal') . ' <' . variable_get('site_mail', '') . '>';
+  $params = array('entry' => $entry, 'account' => $account, 'context' => $context);
+  // Send the e-mail to the recipients using the site default language.
+  drupal_mail('feedback', 'send_email_action', $context['recipients'], language_default(), $params, $from);
+  watchdog('feedback', 'Feedback information sent to %recipients', array('%recipients' => $context['recipients']));
+}
+
+/**
+ * Implements hook_mail().
+ */
+function feedback_mail($key, &$message, $params) {
+  $language = $message['language'];
+  $entry = $params['entry'];
+  $account = $params['account'];
+  $context = $params['context'];
+  $variables = array(
+    '!site_name' => variable_get('site_name', 'Drupal'),
+    '!uid' => $account->uid,
+    '!username' => $account->name ? $account->name : t('Anonymous'),
+    '!usermail' => $account->mail ? $account->mail : t('unknown'),
+    '!status' => $entry->status ? t('Processed') : t('Open'),
+    '!message' => $entry->message,
+    '!url' => url($entry->location, array('absolute' => TRUE, 'language' => $language)),
+    '!useragent' => $entry->useragent,
+    '!date' => format_date($entry->timestamp, 'small', '', NULL, $language->language),
+  );
+  $subject = strtr($context['subject'], $variables);
+  $body = strtr($context['message'], $variables);
+  $message['subject'] .= str_replace(array("\r", "\n"), '', $subject);
+  $message['body'][] = drupal_html_to_text($body);
+}
+
+
 /**
  * Implements hook_mollom_form_list().
  */

+ 593 - 0
feedback.module.orig

@@ -0,0 +1,593 @@
+<?php
+
+/**
+ * @file
+ * Allows site visitors and users to report issues about this site.
+ */
+
+/**
+ * Open state (unprocessed) for feedback entries.
+ */
+define('FEEDBACK_OPEN', 0);
+
+/**
+ * Processed state for feedback entries.
+ */
+define('FEEDBACK_PROCESSED', 1);
+
+/**
+ * Implements hook_theme().
+ */
+function feedback_theme() {
+  return array(
+    'feedback_admin_view_form' => array(
+      'render element' => 'form',
+    ),
+    'feedback_entry' => array(
+      'render element' => 'elements',
+      'template' => 'feedback-entry',
+      'file' => 'feedback.admin.inc',
+    ),
+    'feedback_form_display' => array(
+      'template' => 'feedback-form-display',
+      'variables' => array('title' => NULL, 'content' => NULL),
+    ),
+  );
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function feedback_entity_info() {
+  $return = array(
+    'feedback' => array(
+      'label' => t('Feedback'),
+      'controller class' => 'FeedbackController',
+      'base table' => 'feedback',
+      'uri callback' => 'feedback_uri',
+      'fieldable' => TRUE,
+      'entity keys' => array(
+        'id' => 'fid',
+      ),
+      'bundles' => array(
+        'feedback' => array(
+          'label' => t('Feedback'),
+          'admin' => array(
+            'path' => 'admin/config/user-interface/feedback',
+            'access arguments' => array('administer feedback'),
+          ),
+        ),
+      ),
+      'view modes' => array(
+        'full' => array(
+          'label' => t('Full feedback entry'),
+          'custom settings' => FALSE,
+        ),
+        'teaser' => array(
+          'label' => t('Teaser'),
+          'custom settings' => FALSE,
+        ),
+        'widget' => array(
+          'label' => t('Widget'),
+          'custom settings' => FALSE,
+        ),
+      ),
+      // Disable Metatags (metatag) module's entity form additions.
+      'metatags' => FALSE,
+    ),
+  );
+
+  return $return;
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ */
+function feedback_field_extra_fields() {
+  $extras['feedback']['feedback']['form']['help'] = array(
+    'label' => t('Help'),
+    'description' => t('Feedback submission guidelines'),
+    'weight' => -10,
+  );
+  $extras['feedback']['feedback']['form']['messages'] = array(
+    'label' => t('Entries'),
+    'description' => t('Existing feedback entries for the current page'),
+    'weight' => -5,
+  );
+  $extras['feedback']['feedback']['form']['message'] = array(
+    'label' => t('Message'),
+    'description' => t('Feedback message form text field'),
+    'weight' => 0,
+  );
+
+  $extras['feedback']['feedback']['display']['location'] = array(
+    'label' => t('Location'),
+    'description' => t('The URL of the page the message was submitted on'),
+    'weight' => -15,
+  );
+  $extras['feedback']['feedback']['display']['date'] = array(
+    'label' => t('Date'),
+    'description' => t('The submission date of the message'),
+    'weight' => -10,
+  );
+  $extras['feedback']['feedback']['display']['user'] = array(
+    'label' => t('User'),
+    'description' => t('The name of the user who submitted the message'),
+    'weight' => -5,
+  );
+  $extras['feedback']['feedback']['display']['message'] = array(
+    'label' => t('Message'),
+    'description' => t('The main feedback message'),
+    'weight' => 0,
+  );
+  return $extras;
+}
+
+/**
+ * Entity uri callback.
+ */
+function feedback_uri($entry) {
+  return array(
+    'path' => 'admin/reports/feedback/' . $entry->fid,
+  );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function feedback_permission() {
+  return array(
+    'access feedback form' => array(
+      'title' => t('Access feedback form'),
+      'description' => t('Submit feedback messages.'),
+    ),
+    'view feedback messages' => array(
+      'title' => t('View feedback messages'),
+      'description' => t('View, process, and delete submitted feedback messages.'),
+    ),
+    'administer feedback' => array(
+      'title' => t('Administer feedback settings'),
+    ),
+  );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function feedback_menu() {
+  $items['admin/reports/feedback'] = array(
+    'title' => 'Feedback messages',
+    'description' => 'View feedback messages.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_admin_view_form'),
+    'access arguments' => array('view feedback messages'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback'] = array(
+    'title' => 'Feedback entry',
+    'page callback' => 'feedback_view',
+    'page arguments' => array(3),
+    'access arguments' => array('view feedback messages'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback/view'] = array(
+    'title' => 'View',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['admin/reports/feedback/%feedback/edit'] = array(
+    'title' => 'Edit',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_entry_form', 3),
+    'access arguments' => array('view feedback messages'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback/delete'] = array(
+    'title' => 'Delete feedback entry',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_delete_confirm', 3),
+    'access arguments' => array('view feedback messages'),
+    'type' => MENU_CALLBACK,
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/config/user-interface/feedback'] = array(
+    'title' => 'Feedback',
+    'description' => 'Administer feedback settings.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_admin_settings_form'),
+    'access arguments' => array('administer feedback'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/config/user-interface/feedback/settings'] = array(
+    'title' => 'Settings',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_page_build().
+ */
+function feedback_page_build(&$page) {
+  if (user_access('access feedback form') && !feedback_match_path(variable_get('feedback_excluded_paths', 'admin/reports/feedback'))) {
+    $page['page_bottom']['feedback'] = array(
+      '#theme' => 'feedback_form_display',
+      '#title' => t('Feedback'),
+      '#content' => drupal_get_form('feedback_form'),
+    );
+    $path = drupal_get_path('module', 'feedback');
+    $page['page_bottom']['feedback']['#attached']['css'][] = $path . '/feedback.css';
+    $page['page_bottom']['feedback']['#attached']['js'][] = $path . '/feedback.js';
+  }
+}
+
+/**
+ * Check if the current path matches any pattern in a set of patterns.
+ *
+ * @param $patterns
+ *   String containing a set of patterns separated by \n, \r or \r\n.
+ *
+ * @return
+ *   Boolean value: TRUE if the current path or alias matches a pattern.
+ */
+function feedback_match_path($patterns) {
+  // Convert path to lowercase. This allows comparison of the same path
+  // with different case. Ex: /Page, /page, /PAGE.
+  $patterns = drupal_strtolower($patterns);
+
+  // Convert the current path to lowercase.
+  $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
+
+  // Compare the lowercase internal and lowercase path alias (if any).
+  $page_match = drupal_match_path($path, $patterns);
+  if ($path != $_GET['q']) {
+    $page_match = $page_match || drupal_match_path($_GET['q'], $patterns);
+  }
+
+  return $page_match;
+}
+
+/**
+ * Form constructor for the feedback form.
+ *
+ * @see feedback_form_submit()
+ * @ingroup forms
+ */
+function feedback_form($form, &$form_state) {
+  $form['#attributes']['class'] = array('feedback-form');
+
+  // Store the path on which this form is displayed.
+  if (!isset($form_state['inline']['location'])) {
+    $form_state['inline']['location'] = $_GET['q'];
+  }
+  $form['location'] = array(
+    '#type' => 'value',
+    '#value' => $form_state['inline']['location'],
+  );
+
+  $form['help'] = array(
+    '#prefix' => '<div class="feedback-help">',
+    '#markup' => t('If you experience a bug or would like to see an addition on the current page, feel free to leave us a message.'),
+    '#suffix' => '</div>',
+  );
+  if (user_access('view feedback messages')) {
+    if (arg(0) != 'node') {
+      $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location_masked' => feedback_mask_path($_GET['q'])));
+    }
+    else {
+      $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location' => $_GET['q']));
+    }
+    if ($feedbacks) {
+      form_load_include($form_state, 'inc', 'feedback', 'feedback.admin');
+      $form['messages'] = array(
+        '#prefix' => '<div class="feedback-messages">',
+        '#suffix' => '</div>',
+      );
+      foreach ($feedbacks as $fid => $feedback) {
+        $form['messages'][$fid]['#feedback'] = $feedback;
+        $form['messages'][$fid]['submitted'] = array('#markup' => t('@feedback-author !feedback-date:', array('@feedback-author' => format_username($feedback), '!feedback-date' => format_date($feedback->timestamp, 'small'))));
+        $form['messages'][$fid]['submitted']['#prefix'] = '<div class="feedback-submitted">';
+        $form['messages'][$fid]['submitted']['#suffix'] = '</div>';
+        $form['messages'][$fid]['body'] = feedback_build_content($feedback, 'widget');
+        $form['messages'][$fid]['body']['#prefix'] = '<div class="feedback-body">';
+        $form['messages'][$fid]['body']['#suffix'] = '</div>';
+      }
+    }
+  }
+  $form['message'] = array(
+    '#type' => 'textarea',
+    '#attributes' => array('class' => array('feedback-message')),
+    '#cols' => 20,
+    '#title' => t('Message'),
+    '#required' => TRUE,
+    '#wysiwyg' => FALSE,
+  );
+
+  $entry = new stdClass();
+  field_attach_form('feedback', $entry, $form, $form_state);
+
+  $form['actions'] = array(
+    '#type' => 'actions',
+    // Without clearfix, the AJAX throbber wraps in an ugly way.
+    // @todo Patch #type actions in core?
+    '#attributes' => array('class' => array('clearfix')),
+  );
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Send feedback'),
+    '#id' => 'feedback-submit',
+    '#ajax' => array(
+      'wrapper' => 'feedback-form',
+      'callback' => 'feedback_form_ajax_callback',
+      'progress' => array(
+        'type' => 'throbber',
+        'message' => '',
+      ),
+    ),
+  );
+
+  return $form;
+}
+
+/**
+ * Form submission handler for feedback_form().
+ */
+function feedback_form_submit($form, &$form_state) {
+  $entry = new stdClass();
+  entity_form_submit_build_entity('feedback', $entry, $form, $form_state);
+  $entry->message = $form_state['values']['message'];
+  $entry->location = $form_state['values']['location'];
+  feedback_save($entry);
+
+  drupal_set_message(t('Thanks for your feedback!'));
+}
+
+/**
+ * AJAX callback for feedback_form() submissions.
+ */
+function feedback_form_ajax_callback($form, &$form_state) {
+  // If there was a form validation error, re-render the entire form.
+  if (!$form_state['executed']) {
+    return $form;
+  }
+
+  // Otherwise, return a fresh copy of the form, so the user may post additional
+  // feedback.
+  // Reset the static cache of drupal_html_id().
+  // @see drupal_process_form()
+  // @see drupal_html_id()
+  $seen_ids = &drupal_static('drupal_html_id');
+  $seen_ids = array();
+
+  // Prevent the form from being processed again.
+  // @see drupal_build_form()
+  list($form, $new_form_state) = ajax_get_form();
+  $new_form_state['input'] = array();
+  drupal_process_form($form['#form_id'], $form, $new_form_state);
+
+  // Return AJAX commands in order to output the special success message.
+  // @see ajax_deliver()
+  $build = array('#type' => 'ajax');
+  $html = drupal_render($form);
+  $build['#commands'][] = ajax_command_insert(NULL, $html);
+
+  // A successful form submission normally means that there were no errors, so
+  // we only render status messages.
+  $messages = drupal_get_messages();
+  $messages += array('status' => array());
+  $messages = implode('<br />', $messages['status']);
+  $html = '<div id="feedback-status-message">' . $messages . '</div>';
+  $build['#commands'][] = ajax_command_append('#block-feedback-form', $html);
+  return $build;
+}
+
+/**
+ * Loads a feedback entry from the database.
+ *
+ * @param $fid
+ *   Integer specifying the feedback ID to load.
+ *
+ * @return
+ *   A loaded feedback entry object upon successful load, or FALSE if the entry
+ *   cannot be loaded.
+ *
+ * @see feedback_load_multiple()
+ */
+function feedback_load($fid) {
+  $entries = feedback_load_multiple(array($fid));
+  return (isset($entries[$fid]) ? $entries[$fid] : FALSE);
+}
+
+/**
+ * Loads feedback entries from the database.
+ *
+ * @param $fids
+ *   An array of feedback entry IDs.
+ * @param $conditions
+ *   An associative array of conditions on the {feedback} table, where the keys
+ *   are the database fields and the values are the values those fields
+ *   must have.
+ *
+ * @return
+ *   An array of feedback entry objects indexed by fid.
+ *
+ * @see hook_feedback_load()
+ * @see feedback_load()
+ * @see entity_load()
+ * @see EntityFieldQuery
+ */
+function feedback_load_multiple($fids = array(), $conditions = array()) {
+  return entity_load('feedback', $fids, $conditions);
+}
+
+/**
+ * Saves changes to a feedback entry or adds a new feedback entry.
+ *
+ * @param $entry
+ *   The feedback entry object to modify or add. If $entry->fid is omitted, a
+ *   new entry will be added.
+ *
+ * @see hook_feedback_insert()
+ * @see hook_feedback_update()
+ */
+function feedback_save($entry) {
+  global $user;
+
+  // Load the stored entity, if any.
+  if (!empty($entry->fid) && !isset($entry->original)) {
+    $entry->original = entity_load_unchanged('feedback', $entry->fid);
+  }
+
+  field_attach_presave('feedback', $entry);
+
+  // Allow modules to alter the feedback entry before saving.
+  module_invoke_all('feedback_presave', $entry);
+  module_invoke_all('entity_presave', $entry, 'feedback');
+
+  if (empty($entry->fid)) {
+    $entry->message = trim($entry->message);
+
+    $defaults = array(
+      'uid' => $user->uid,
+      'location_masked' => feedback_mask_path($entry->location),
+      'url' => url($entry->location, array('absolute' => TRUE)),
+      'timestamp' => REQUEST_TIME,
+      'useragent' => $_SERVER['HTTP_USER_AGENT'],
+    );
+    foreach ($defaults as $key => $default) {
+      if (!isset($entry->$key)) {
+        $entry->$key = $default;
+      }
+    }
+
+    $status = drupal_write_record('feedback', $entry);
+    field_attach_insert('feedback', $entry);
+    module_invoke_all('feedback_insert', $entry);
+    module_invoke_all('entity_insert', $entry, 'feedback');
+  }
+  else {
+    $status = drupal_write_record('feedback', $entry, 'fid');
+
+    field_attach_update('feedback', $entry);
+    module_invoke_all('feedback_update', $entry);
+    module_invoke_all('entity_update', $entry, 'feedback');
+  }
+  unset($entry->original);
+
+  return $status;
+}
+
+/**
+ * Deletes a feedback entry.
+ *
+ * @param $fid
+ *   A feedback entry ID.
+ */
+function feedback_delete($fid) {
+  feedback_delete_multiple(array($fid));
+}
+
+/**
+ * Deletes multiple feedback entries.
+ *
+ * @param $fids
+ *   An array of feedback entry IDs.
+ */
+function feedback_delete_multiple($fids) {
+  if (!empty($fids)) {
+    $entries = feedback_load_multiple($fids);
+    foreach ($entries as $fid => $entry) {
+      field_attach_delete('feedback', $entry);
+      module_invoke_all('feedback_delete', $entry);
+      module_invoke_all('entity_delete', $entry, 'feedback');
+    }
+    db_delete('feedback')
+      ->condition('fid', $fids, 'IN')
+      ->execute();
+  }
+}
+
+/**
+ * 'Mask' a path, i.e. replace all numeric arguments in a path with '%' placeholders.
+ *
+ * Please note that only numeric arguments with a preceding slash will be
+ * replaced.
+ *
+ * @param $path
+ *   An internal Drupal path, f.e. 'user/123/edit'.
+ * @return
+ *   A 'masked' path, for above example 'user/%/edit'.
+ *
+ * @todo Use the untranslated patch of menu_get_item() instead.
+ */
+function feedback_mask_path($path) {
+  return preg_replace('@/\d+@', '/%', $path);
+}
+
+/**
+ * Implements hook_user_cancel().
+ */
+function feedback_user_cancel($edit, $account, $method) {
+  switch ($method) {
+    case 'user_cancel_reassign':
+      db_update('feedback')
+        ->fields(array('uid' => 0))
+        ->condition('uid', $account->uid)
+        ->execute();
+      break;
+  }
+}
+
+/**
+ * Implements hook_user_delete().
+ */
+function feedback_user_delete($account) {
+  $fids = db_query('SELECT fid FROM {feedback} WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
+  feedback_delete_multiple($fids);
+}
+
+/**
+ * Implements hook_mollom_form_list().
+ */
+function feedback_mollom_form_list() {
+  $forms['feedback_form'] = array(
+    'title' => t('Feedback form'),
+    'entity' => 'feedback',
+    'bundle' => 'feedback',
+    'entity delete multiple callback' => 'feedback_delete_multiple',
+    'delete form' => 'feedback_delete_confirm',
+    'delete form file' => array(
+      'name' => 'feedback.admin',
+    ),
+    'report access' => array('view feedback messages'),
+  );
+  return $forms;
+}
+
+/**
+ * Implements hook_mollom_form_info().
+ */
+function feedback_mollom_form_info($form_id) {
+  $form_info = array(
+    'mode' => MOLLOM_MODE_ANALYSIS,
+    'bypass access' => array('administer feedback'),
+    'elements' => array(
+      'message' => t('Message'),
+    ),
+  );
+  mollom_form_info_add_fields($form_info, 'feedback', 'feedback');
+  return $form_info;
+}
+
+/**
+ * Implements hook_views_api();
+ */
+function feedback_views_api() {
+  return array(
+    'api' => 3.0,
+    'path' => drupal_get_path('module', 'feedback') . '/views',
+  );
+}

+ 71 - 4
tests/feedback.test

@@ -20,8 +20,7 @@ class FeedbackTestCase extends DrupalWebTestCase {
   }
 
   function setUp() {
-    // @todo Remove soft-dependency on Block.
-    parent::setUp(array('block', 'feedback'));
+    parent::setUp(array('feedback', 'trigger'));
 
     $this->admin_user = $this->drupalCreateUser(array('access feedback form', 'view feedback messages', 'administer feedback'));
     $this->web_user = $this->drupalCreateUser(array('access feedback form'));
@@ -49,7 +48,7 @@ class FeedbackTestCase extends DrupalWebTestCase {
     $edit = array(
       'feedback-messages[0][1]' => TRUE,
     );
-    $this->drupalPost(NULL, $edit, t('Submit'));
+    $this->drupalPost(NULL, $edit, t('Update'));
     $this->assertFieldByName('feedback-messages[1][1]', 1, t('Processed message found.'));
   }
 
@@ -89,4 +88,72 @@ class FeedbackTestCase extends DrupalWebTestCase {
     $this->assertNoLinkByHref('admin/reports/feedback/1/delete');
     $this->assertNoRaw(check_plain($message), t('Message not found.'));
   }
-}
+
+  /**
+   * Test the feedback triggers and actions.
+   */
+  function testFeedbackEmailAction() {
+    $test_user = $this->drupalCreateUser(array('administer actions', 'administer feedback', 'access feedback form'));
+    $this->drupalLogin($test_user);
+    $this->assignFeedbackEmailAction('feedback_insert', $test_user->mail);
+
+    // Insert a feedback entry.
+    $message = $this->randomString();
+    $edit = array(
+      'message' => $message,
+    );
+    $this->drupalPost('node', $edit, t('Send feedback'));
+    $this->assertFeedbackEmailTokenReplacement($test_user->mail, $message);
+
+    $this->assignFeedbackEmailAction('feedback_update', $test_user->mail);
+
+    // Update a feedback entry.
+    $fid = db_query("SELECT fid FROM {feedback} LIMIT 0,1")->fetchField();
+    $entry = feedback_load($fid);
+    feedback_save($entry);
+    $this->assertFeedbackEmailTokenReplacement($test_user->mail, $entry->message);
+  }
+
+  /**
+   * Assigns a system_send_email_action to the passed-in trigger.
+   *
+   * @param $trigger
+   *   For example, 'user_login'
+   */
+  function assignFeedbackEmailAction($trigger, $mail) {
+    $form_name = "trigger_{$trigger}_assign_form";
+    $form_html_id = strtr($form_name, '_', '-');
+
+    $edit = array(
+      'actions_label' => $trigger . "_feedback_send_email_action",
+      'recipients' => $mail,
+      'subject' => $this->randomName(),
+      'message' => '!message',
+    );
+
+    $hash = drupal_hash_base64('feedback_send_email_action');
+    $this->drupalPost("admin/config/system/actions/configure/$hash", $edit, t('Save'));
+    $this->assertText(t('The action has been successfully saved.'));
+
+    // Now we have to find out the action ID of what we created.
+    $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback AND label = :label', array(':callback' => 'feedback_send_email_action', ':label' => $edit['actions_label']))->fetchField();
+
+    $edit = array('aid' => drupal_hash_base64($aid));
+    $this->drupalPost('admin/structure/trigger/feedback', $edit, t('Assign'), array(), array(), $form_html_id);
+  }
+
+
+  /**
+   * Asserts correct token replacement for the given trigger and account.
+   *
+   * @param $account
+   *   The user account which triggered the action.
+   * @param $email_depth
+   *   Number of emails to scan, starting with most recent.
+   */
+  function assertFeedbackEmailTokenReplacement($mail, $message, $email_depth = 1) {
+    $this->verboseEmail($email_depth);
+    $this->assertMailString('body', $message, $email_depth);
+    $this->assertMail('to', $mail, t('Mail sent to correct destination'));
+  }
+}