From 8914866aa6316caa50741c14bc57cb0f9cc0e4c9 Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 10 Jul 2017 18:24:39 -0500 Subject: [PATCH 01/11] Noah's attempt to understand drupal's node API --- .../modules/custom/class/GeoJsonService.php | 26 ++++++++-------- .../custom/class/PostContentEmailService.php | 31 ++++++++++++++++++- .../themes/responsive_bartik/css/style.css | 7 +++++ 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/sites/all/modules/custom/class/GeoJsonService.php b/sites/all/modules/custom/class/GeoJsonService.php index 0eb0c14..aac6e83 100644 --- a/sites/all/modules/custom/class/GeoJsonService.php +++ b/sites/all/modules/custom/class/GeoJsonService.php @@ -36,13 +36,13 @@ public function getProjectsGeoJson() foreach ($projects as $project) { - $projectTypes = []; + $projectTypes = []; if (empty($project->field_project_type['und'])) { - $projectTypes = array_map(function ($obj) { - return $obj['tid']; - }, $project->field_project_type['und']); - $projectTypes = $this->projectTypes($projectTypes); - } + $projectTypes = array_map(function ($obj) { + return $obj['tid']; + }, $project->field_project_type['und']); + $projectTypes = $this->projectTypes($projectTypes); + } $projectTypeNames = []; $projectTypeMarkers = []; @@ -55,13 +55,13 @@ public function getProjectsGeoJson() $projectTypeMarkers = array_values(array_unique($projectTypeMarkers)); - $neighborhoods = []; - if (!empty($project->field_neighborhood['und'])) { - $neighborhoods = array_map(function ($obj) { - return $obj['tid']; - }, $project->field_neighborhood['und']); - $neighborhoods = $this->neighborhoods($neighborhoods); - } + $neighborhoods = []; + if (!empty($project->field_neighborhood['und'])) { + $neighborhoods = array_map(function ($obj) { + return $obj['tid']; + }, $project->field_neighborhood['und']); + $neighborhoods = $this->neighborhoods($neighborhoods); + } $properties = [ 'title' => $project->title, diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index 1113c62..9b7e091 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -1,6 +1,7 @@ fetchAllKeyed()); + $projects = node_load_multiple($projectNodeIds); + //Go through each project to 1) Send email to owner and 2) Set the timestamp to now + + //Trying to get a field from node + foreach ($projects as &$project){ + $some_variable = $project->field_project_type[$project->language][0]['value']; + } + //Watchdog - used for testing + //watchdog([module name], [message string], [array of token replacements], [severity constant]); + watchdog( + 'custom', + 'Result: !some', + array('!some'=>$some_variable), + WATCHDOG_DEBUG + ); } } diff --git a/sites/all/themes/responsive_bartik/css/style.css b/sites/all/themes/responsive_bartik/css/style.css index c5292c0..420bc06 100644 --- a/sites/all/themes/responsive_bartik/css/style.css +++ b/sites/all/themes/responsive_bartik/css/style.css @@ -1937,3 +1937,10 @@ a#menu-toggle:after { clip: rect(1px, 1px, 1px, 1px); left: 0; } + + +/* ----------------- Add Project Form ---------------------- */ +.field-multiple-table thead tr field-label{ + padding: 10px; + margin: 15px; +} From c5e2452e5db31c3d0c275e9a25510321b94df408 Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 10 Jul 2017 19:46:25 -0500 Subject: [PATCH 02/11] Working code - need to implement email feature --- .../modules/custom/class/GeoJsonService.php | 26 ++++----- .../custom/class/PostContentEmailService.php | 55 +++++++++++-------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/sites/all/modules/custom/class/GeoJsonService.php b/sites/all/modules/custom/class/GeoJsonService.php index aac6e83..0eb0c14 100644 --- a/sites/all/modules/custom/class/GeoJsonService.php +++ b/sites/all/modules/custom/class/GeoJsonService.php @@ -36,13 +36,13 @@ public function getProjectsGeoJson() foreach ($projects as $project) { - $projectTypes = []; + $projectTypes = []; if (empty($project->field_project_type['und'])) { - $projectTypes = array_map(function ($obj) { - return $obj['tid']; - }, $project->field_project_type['und']); - $projectTypes = $this->projectTypes($projectTypes); - } + $projectTypes = array_map(function ($obj) { + return $obj['tid']; + }, $project->field_project_type['und']); + $projectTypes = $this->projectTypes($projectTypes); + } $projectTypeNames = []; $projectTypeMarkers = []; @@ -55,13 +55,13 @@ public function getProjectsGeoJson() $projectTypeMarkers = array_values(array_unique($projectTypeMarkers)); - $neighborhoods = []; - if (!empty($project->field_neighborhood['und'])) { - $neighborhoods = array_map(function ($obj) { - return $obj['tid']; - }, $project->field_neighborhood['und']); - $neighborhoods = $this->neighborhoods($neighborhoods); - } + $neighborhoods = []; + if (!empty($project->field_neighborhood['und'])) { + $neighborhoods = array_map(function ($obj) { + return $obj['tid']; + }, $project->field_neighborhood['und']); + $neighborhoods = $this->neighborhoods($neighborhoods); + } $properties = [ 'title' => $project->title, diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index 9b7e091..c491883 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -11,35 +11,44 @@ public function __construct() public function run() { - - $sql_query = "SELECT + $sql_query = "SELECT n.nid, - n.uid, - n.changed - FROM ccf_node n - WHERE + n.uid + FROM ccf_node n + WHERE n.type = 'project' AND n.status = 1 AND n.changed <= UNIX_TIMESTAMP( DATE_ADD(DATE_ADD(CURDATE(), INTERVAL -6 MONTH), INTERVAL -1 SECOND) ); "; - - //Using above query, load the projects - $projectNodeIds = array_keys(db_query($sql_query)->fetchAllKeyed()); - $projects = node_load_multiple($projectNodeIds); - //Go through each project to 1) Send email to owner and 2) Set the timestamp to now - - //Trying to get a field from node - foreach ($projects as &$project){ - $some_variable = $project->field_project_type[$project->language][0]['value']; + + $queryResults = db_query($sql_query)->fetchAll(); + if (empty($queryResults)) + return; + + $length = count($queryResults); + $projectNodeIds = array_fill(0, $queryResults, null); + $projectUserIds = array_fill(0, $queryResults, null); + + for ($i = 0; $i < $length; $i++) { + $projectId = intval($queryResults[$i]->nid); + $projectNodeIds[$i] = $projectId; + $projectUserIds[$projectId] = intval($queryResults[$i]->uid); + } + + $users = user_load_multiple($projectUserIds); + $projects = node_load_multiple($projectNodeIds); + + + foreach ($projects as $project) { + $user = $users[(int)$project->uid]; + $email = $user->mail; + $fullname = $user->field_full_name['und'][0]['value']; + $project_name = $project->title; + $last_rev = $project->revision_timestamp; + $test = 0; + + // .. do code here } - //Watchdog - used for testing - //watchdog([module name], [message string], [array of token replacements], [severity constant]); - watchdog( - 'custom', - 'Result: !some', - array('!some'=>$some_variable), - WATCHDOG_DEBUG - ); } } From cef22fe631a9b50d780f7d9e6f0c65375cb7033a Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 10 Jul 2017 19:48:34 -0500 Subject: [PATCH 03/11] Forgot to save - Working code - need to implement email feature --- sites/all/modules/custom/class/PostContentEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index c491883..15bfe6f 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -47,7 +47,7 @@ public function run() $last_rev = $project->revision_timestamp; $test = 0; - // .. do code here + // Using above four vars, send email to myself. } } From c27cadcfa21aaffe01cb74c6bf4e151faf716b8f Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 17 Jul 2017 19:15:25 -0500 Subject: [PATCH 04/11] Basic mailing works --- comment/comment-node-form.js | 32 + comment/comment-rtl.css | 5 + comment/comment-wrapper.tpl.php | 52 + comment/comment.admin.inc | 284 ++ comment/comment.api.php | 145 + comment/comment.css | 13 + comment/comment.info | 16 + comment/comment.install | 578 ++++ comment/comment.module | 2746 +++++++++++++++++ comment/comment.pages.inc | 123 + comment/comment.test | 2260 ++++++++++++++ comment/comment.tokens.inc | 243 ++ comment/comment.tpl.php | 92 + .../custom/class/PostContentEmailService.php | 18 +- sites/all/modules/custom/custom.module | 74 +- 15 files changed, 6662 insertions(+), 19 deletions(-) create mode 100644 comment/comment-node-form.js create mode 100644 comment/comment-rtl.css create mode 100644 comment/comment-wrapper.tpl.php create mode 100644 comment/comment.admin.inc create mode 100644 comment/comment.api.php create mode 100644 comment/comment.css create mode 100644 comment/comment.info create mode 100644 comment/comment.install create mode 100644 comment/comment.module create mode 100644 comment/comment.pages.inc create mode 100644 comment/comment.test create mode 100644 comment/comment.tokens.inc create mode 100644 comment/comment.tpl.php diff --git a/comment/comment-node-form.js b/comment/comment-node-form.js new file mode 100644 index 0000000..76db240 --- /dev/null +++ b/comment/comment-node-form.js @@ -0,0 +1,32 @@ + +(function ($) { + +Drupal.behaviors.commentFieldsetSummaries = { + attach: function (context) { + $('fieldset.comment-node-settings-form', context).drupalSetSummary(function (context) { + return Drupal.checkPlain($('.form-item-comment input:checked', context).next('label').text()); + }); + + // Provide the summary for the node type form. + $('fieldset.comment-node-type-settings-form', context).drupalSetSummary(function(context) { + var vals = []; + + // Default comment setting. + vals.push($(".form-item-comment select option:selected", context).text()); + + // Threading. + var threading = $(".form-item-comment-default-mode input:checked", context).next('label').text(); + if (threading) { + vals.push(threading); + } + + // Comments per page. + var number = $(".form-item-comment-default-per-page select option:selected", context).val(); + vals.push(Drupal.t('@number comments per page', {'@number': number})); + + return Drupal.checkPlain(vals.join(', ')); + }); + } +}; + +})(jQuery); diff --git a/comment/comment-rtl.css b/comment/comment-rtl.css new file mode 100644 index 0000000..39c3929 --- /dev/null +++ b/comment/comment-rtl.css @@ -0,0 +1,5 @@ + +.indented { + margin-left: 0; + margin-right: 25px; +} diff --git a/comment/comment-wrapper.tpl.php b/comment/comment-wrapper.tpl.php new file mode 100644 index 0000000..c691459 --- /dev/null +++ b/comment/comment-wrapper.tpl.php @@ -0,0 +1,52 @@ + +
> + type != 'forum'): ?> + +

+ + + + + + +

+ + +
diff --git a/comment/comment.admin.inc b/comment/comment.admin.inc new file mode 100644 index 0000000..43b53e2 --- /dev/null +++ b/comment/comment.admin.inc @@ -0,0 +1,284 @@ + 'fieldset', + '#title' => t('Update options'), + '#attributes' => array('class' => array('container-inline')), + ); + + if ($arg == 'approval') { + $options['publish'] = t('Publish the selected comments'); + } + else { + $options['unpublish'] = t('Unpublish the selected comments'); + } + $options['delete'] = t('Delete the selected comments'); + + $form['options']['operation'] = array( + '#type' => 'select', + '#title' => t('Operation'), + '#title_display' => 'invisible', + '#options' => $options, + '#default_value' => 'publish', + ); + $form['options']['submit'] = array( + '#type' => 'submit', + '#value' => t('Update'), + ); + + // Load the comments that need to be displayed. + $status = ($arg == 'approval') ? COMMENT_NOT_PUBLISHED : COMMENT_PUBLISHED; + $header = array( + 'subject' => array('data' => t('Subject'), 'field' => 'subject'), + 'author' => array('data' => t('Author'), 'field' => 'name'), + 'posted_in' => array('data' => t('Posted in'), 'field' => 'node_title'), + 'changed' => array('data' => t('Updated'), 'field' => 'c.changed', 'sort' => 'desc'), + 'operations' => array('data' => t('Operations')), + ); + + $query = db_select('comment', 'c')->extend('PagerDefault')->extend('TableSort'); + $query->join('node', 'n', 'n.nid = c.nid'); + $query->addField('n', 'title', 'node_title'); + $query->addTag('node_access'); + $result = $query + ->fields('c', array('cid', 'subject', 'name', 'changed')) + ->condition('c.status', $status) + ->limit(50) + ->orderByHeader($header) + ->execute(); + + $cids = array(); + + // We collect a sorted list of node_titles during the query to attach to the + // comments later. + foreach ($result as $row) { + $cids[] = $row->cid; + $node_titles[] = $row->node_title; + } + $comments = comment_load_multiple($cids); + + // Build a table listing the appropriate comments. + $options = array(); + $destination = drupal_get_destination(); + + foreach ($comments as $comment) { + // Remove the first node title from the node_titles array and attach to + // the comment. + $comment->node_title = array_shift($node_titles); + $comment_body = field_get_items('comment', $comment, 'comment_body'); + $options[$comment->cid] = array( + 'subject' => array( + 'data' => array( + '#type' => 'link', + '#title' => $comment->subject, + '#href' => 'comment/' . $comment->cid, + '#options' => array('attributes' => array('title' => truncate_utf8($comment_body[0]['value'], 128)), 'fragment' => 'comment-' . $comment->cid), + ), + ), + 'author' => theme('username', array('account' => $comment)), + 'posted_in' => array( + 'data' => array( + '#type' => 'link', + '#title' => $comment->node_title, + '#href' => 'node/' . $comment->nid, + ), + ), + 'changed' => format_date($comment->changed, 'short'), + 'operations' => array( + 'data' => array( + '#type' => 'link', + '#title' => t('edit'), + '#href' => 'comment/' . $comment->cid . '/edit', + '#options' => array('query' => $destination), + ), + ), + ); + } + + $form['comments'] = array( + '#type' => 'tableselect', + '#header' => $header, + '#options' => $options, + '#empty' => t('No comments available.'), + ); + + $form['pager'] = array('#theme' => 'pager'); + + return $form; +} + +/** + * Validate comment_admin_overview form submissions. + */ +function comment_admin_overview_validate($form, &$form_state) { + $form_state['values']['comments'] = array_diff($form_state['values']['comments'], array(0)); + // We can't execute any 'Update options' if no comments were selected. + if (count($form_state['values']['comments']) == 0) { + form_set_error('', t('Select one or more comments to perform the update on.')); + } +} + +/** + * Process comment_admin_overview form submissions. + * + * Execute the chosen 'Update option' on the selected comments, such as + * publishing, unpublishing or deleting. + */ +function comment_admin_overview_submit($form, &$form_state) { + $operation = $form_state['values']['operation']; + $cids = $form_state['values']['comments']; + + if ($operation == 'delete') { + comment_delete_multiple($cids); + } + else { + foreach ($cids as $cid => $value) { + $comment = comment_load($value); + + if ($operation == 'unpublish') { + $comment->status = COMMENT_NOT_PUBLISHED; + } + elseif ($operation == 'publish') { + $comment->status = COMMENT_PUBLISHED; + } + comment_save($comment); + } + } + drupal_set_message(t('The update has been performed.')); + $form_state['redirect'] = 'admin/content/comment'; + cache_clear_all(); +} + +/** + * List the selected comments and verify that the admin wants to delete them. + * + * @param $form_state + * An associative array containing the current state of the form. + * @return + * TRUE if the comments should be deleted, FALSE otherwise. + * @ingroup forms + * @see comment_multiple_delete_confirm_submit() + */ +function comment_multiple_delete_confirm($form, &$form_state) { + $edit = $form_state['input']; + + $form['comments'] = array( + '#prefix' => '
    ', + '#suffix' => '
', + '#tree' => TRUE, + ); + // array_filter() returns only elements with actual values. + $comment_counter = 0; + foreach (array_filter($edit['comments']) as $cid => $value) { + $comment = comment_load($cid); + if (is_object($comment) && is_numeric($comment->cid)) { + $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField(); + $form['comments'][$cid] = array('#type' => 'hidden', '#value' => $cid, '#prefix' => '
  • ', '#suffix' => check_plain($subject) . '
  • '); + $comment_counter++; + } + } + $form['operation'] = array('#type' => 'hidden', '#value' => 'delete'); + + if (!$comment_counter) { + drupal_set_message(t('There do not appear to be any comments to delete, or your selected comment was deleted by another administrator.')); + drupal_goto('admin/content/comment'); + } + else { + return confirm_form($form, + t('Are you sure you want to delete these comments and all their children?'), + 'admin/content/comment', t('This action cannot be undone.'), + t('Delete comments'), t('Cancel')); + } +} + +/** + * Process comment_multiple_delete_confirm form submissions. + */ +function comment_multiple_delete_confirm_submit($form, &$form_state) { + if ($form_state['values']['confirm']) { + comment_delete_multiple(array_keys($form_state['values']['comments'])); + cache_clear_all(); + $count = count($form_state['values']['comments']); + watchdog('content', 'Deleted @count comments.', array('@count' => $count)); + drupal_set_message(format_plural($count, 'Deleted 1 comment.', 'Deleted @count comments.')); + } + $form_state['redirect'] = 'admin/content/comment'; +} + +/** + * Page callback for comment deletions. + */ +function comment_confirm_delete_page($cid) { + if ($comment = comment_load($cid)) { + return drupal_get_form('comment_confirm_delete', $comment); + } + return MENU_NOT_FOUND; +} + +/** + * Form builder; Builds the confirmation form for deleting a single comment. + * + * @ingroup forms + * @see comment_confirm_delete_submit() + */ +function comment_confirm_delete($form, &$form_state, $comment) { + $form['#comment'] = $comment; + // Always provide entity id in the same form key as in the entity edit form. + $form['cid'] = array('#type' => 'value', '#value' => $comment->cid); + return confirm_form( + $form, + t('Are you sure you want to delete the comment %title?', array('%title' => $comment->subject)), + 'node/' . $comment->nid, + t('Any replies to this comment will be lost. This action cannot be undone.'), + t('Delete'), + t('Cancel'), + 'comment_confirm_delete'); +} + +/** + * Process comment_confirm_delete form submissions. + */ +function comment_confirm_delete_submit($form, &$form_state) { + $comment = $form['#comment']; + // Delete the comment and its replies. + comment_delete($comment->cid); + drupal_set_message(t('The comment and all its replies have been deleted.')); + watchdog('content', 'Deleted comment @cid and its replies.', array('@cid' => $comment->cid)); + // Clear the cache so an anonymous user sees that his comment was deleted. + cache_clear_all(); + + $form_state['redirect'] = "node/$comment->nid"; +} diff --git a/comment/comment.api.php b/comment/comment.api.php new file mode 100644 index 0000000..0591265 --- /dev/null +++ b/comment/comment.api.php @@ -0,0 +1,145 @@ +subject = trim($comment->subject); +} + +/** + * The comment is being inserted. + * + * @param $comment + * The comment object. + */ +function hook_comment_insert($comment) { + // Reindex the node when comments are added. + search_touch_node($comment->nid); +} + +/** + * The comment is being updated. + * + * @param $comment + * The comment object. + */ +function hook_comment_update($comment) { + // Reindex the node when comments are updated. + search_touch_node($comment->nid); +} + +/** + * Comments are being loaded from the database. + * + * @param $comments + * An array of comment objects indexed by cid. + */ +function hook_comment_load($comments) { + $result = db_query('SELECT cid, foo FROM {mytable} WHERE cid IN (:cids)', array(':cids' => array_keys($comments))); + foreach ($result as $record) { + $comments[$record->cid]->foo = $record->foo; + } +} + +/** + * The comment is being viewed. This hook can be used to add additional data to the comment before theming. + * + * @param $comment + * Passes in the comment the action is being performed on. + * @param $view_mode + * View mode, e.g. 'full', 'teaser'... + * @param $langcode + * The language code used for rendering. + * + * @see hook_entity_view() + */ +function hook_comment_view($comment, $view_mode, $langcode) { + // how old is the comment + $comment->time_ago = time() - $comment->changed; +} + +/** + * The comment was built; the module may modify the structured content. + * + * This hook is called after the content has been assembled in a structured array + * and may be used for doing processing which requires that the complete comment + * content structure has been built. + * + * If the module wishes to act on the rendered HTML of the comment rather than the + * structured content array, it may use this hook to add a #post_render callback. + * Alternatively, it could also implement hook_preprocess_comment(). See + * drupal_render() and theme() documentation respectively for details. + * + * @param $build + * A renderable array representing the comment. + * + * @see comment_view() + * @see hook_entity_view_alter() + */ +function hook_comment_view_alter(&$build) { + // Check for the existence of a field added by another module. + if ($build['#view_mode'] == 'full' && isset($build['an_additional_field'])) { + // Change its weight. + $build['an_additional_field']['#weight'] = -10; + } + + // Add a #post_render callback to act on the rendered HTML of the comment. + $build['#post_render'][] = 'my_module_comment_post_render'; +} + +/** + * The comment is being published by the moderator. + * + * @param $comment + * Passes in the comment the action is being performed on. + * @return + * Nothing. + */ +function hook_comment_publish($comment) { + drupal_set_message(t('Comment: @subject has been published', array('@subject' => $comment->subject))); +} + +/** + * The comment is being unpublished by the moderator. + * + * @param $comment + * Passes in the comment the action is being performed on. + * @return + * Nothing. + */ +function hook_comment_unpublish($comment) { + drupal_set_message(t('Comment: @subject has been unpublished', array('@subject' => $comment->subject))); +} + +/** + * The comment is being deleted by the moderator. + * + * @param $comment + * Passes in the comment the action is being performed on. + * @return + * Nothing. + */ +function hook_comment_delete($comment) { + drupal_set_message(t('Comment: @subject has been deleted', array('@subject' => $comment->subject))); +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/comment/comment.css b/comment/comment.css new file mode 100644 index 0000000..a55f527 --- /dev/null +++ b/comment/comment.css @@ -0,0 +1,13 @@ + +#comments { + margin-top: 15px; +} +.indented { + margin-left: 25px; /* LTR */ +} +.comment-unpublished { + background-color: #fff4f4; +} +.comment-preview { + background-color: #ffffea; +} diff --git a/comment/comment.info b/comment/comment.info new file mode 100644 index 0000000..749da0d --- /dev/null +++ b/comment/comment.info @@ -0,0 +1,16 @@ +name = Comment +description = Allows users to comment on and discuss published content. +package = Core +version = VERSION +core = 7.x +dependencies[] = text +files[] = comment.module +files[] = comment.test +configure = admin/content/comment +stylesheets[all][] = comment.css + +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" +project = "drupal" +datestamp = "1481152423" + diff --git a/comment/comment.install b/comment/comment.install new file mode 100644 index 0000000..e4da58f --- /dev/null +++ b/comment/comment.install @@ -0,0 +1,578 @@ +leftJoin('node_comment_statistics', 'ncs', 'ncs.nid = n.nid'); + $query->addField('n', 'created', 'last_comment_timestamp'); + $query->addField('n', 'uid', 'last_comment_uid'); + $query->addField('n', 'nid'); + $query->addExpression('0', 'comment_count'); + $query->addExpression('NULL', 'last_comment_name'); + $query->isNull('ncs.comment_count'); + + db_insert('node_comment_statistics') + ->from($query) + ->execute(); +} + +/** + * Implements hook_modules_enabled(). + * + * Creates comment body fields for node types existing before the comment module + * is enabled. We use hook_modules_enabled() rather than hook_enable() so we can + * react to node types of existing modules, and those of modules being enabled + * both before and after comment module in the loop of module_enable(). + * + * There is a separate comment bundle for each node type to allow for + * per-node-type customization of comment fields. Each one of these bundles + * needs a comment body field instance. A comment bundle is needed even for + * node types whose comments are disabled by default, because individual nodes + * may override that default. + * + * @see comment_node_type_insert() + */ +function comment_modules_enabled($modules) { + // Only react if comment module is one of the modules being enabled. + // hook_node_type_insert() is used to create body fields while the comment + // module is enabled. + if (in_array('comment', $modules)) { + // Ensure that the list of node types reflects newly enabled modules. + node_types_rebuild(); + + // Create comment body fields for each node type, if needed. + foreach (node_type_get_types() as $type => $info) { + _comment_body_field_create($info); + } + } +} + +/** + * Implements hook_update_dependencies(). + */ +function comment_update_dependencies() { + // comment_update_7005() creates the comment body field and therefore must + // run after all Field modules have been enabled, which happens in + // system_update_7027(). + $dependencies['comment'][7005] = array( + 'system' => 7027, + ); + + // comment_update_7006() needs to query the {filter_format} table to get a + // list of existing text formats, so it must run after filter_update_7000(), + // which creates that table. + $dependencies['comment'][7006] = array( + 'filter' => 7000, + ); + + return $dependencies; +} + +/** + * @addtogroup updates-6.x-to-7.x + * @{ + */ + +/** + * Rename comment display setting variables. + */ +function comment_update_7000() { + $types = _update_7000_node_get_types(); + foreach ($types as $type => $type_object) { + variable_del('comment_default_order' . $type); + + // Drupal 6 had four display modes: + // - COMMENT_MODE_FLAT_COLLAPSED = 1 + // - COMMENT_MODE_FLAT_EXPANDED = 2 + // - COMMENT_MODE_THREADED_COLLAPSED = 3 + // - COMMENT_MODE_THREADED_EXPANDED = 4 + // + // Drupal 7 doesn't support collapsed/expanded modes anymore, so we + // migrate all the flat modes to COMMENT_MODE_FLAT (0) and all the threaded + // modes to COMMENT_MODE_THREADED (1). + $setting = variable_get('comment_default_mode_' . $type, 4); + if ($setting == 3 || $setting == 4) { + variable_set('comment_default_mode_' . $type, 1); + } + else { + variable_set('comment_default_mode_' . $type, 0); + } + + // There were only two comment modes in the past: + // - 1 was 'required' previously, convert into DRUPAL_REQUIRED (2). + // - 0 was 'optional' previously, convert into DRUPAL_OPTIONAL (1). + $preview = variable_get('comment_preview_' . $type, 1) ? 2 : 1; + variable_set('comment_preview_' . $type, $preview); + } +} + +/** + * Change comment status from published being 0 to being 1 + */ +function comment_update_7001() { + // Choose a temporary status value different from the existing status values. + $tmp_status = db_query('SELECT MAX(status) FROM {comments}')->fetchField() + 1; + + $changes = array( + 0 => $tmp_status, + 1 => 0, + $tmp_status => 1, + ); + + foreach ($changes as $old => $new) { + db_update('comments') + ->fields(array('status' => $new)) + ->condition('status', $old) + ->execute(); + } +} + +/** + * Rename {comments} table to {comment} and upgrade it. + */ +function comment_update_7002() { + db_rename_table('comments', 'comment'); + + // Add user-related indexes. These may already exist from Drupal 6. + if (!db_index_exists('comment', 'comment_uid')) { + db_add_index('comment', 'comment_uid', array('uid')); + db_add_index('node_comment_statistics', 'last_comment_uid', array('last_comment_uid')); + } + + // Create a language column. + db_add_field('comment', 'language', array( + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + )); + db_add_index('comment', 'comment_nid_language', array('nid', 'language')); +} + +/** + * Split {comment}.timestamp into 'created' and 'changed', improve indexing on {comment}. + */ +function comment_update_7003() { + // Drop the old indexes. + db_drop_index('comment', 'status'); + db_drop_index('comment', 'pid'); + + // Create a created column. + db_add_field('comment', 'created', array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + )); + + // Rename the timestamp column to changed. + db_change_field('comment', 'timestamp', 'changed', array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + )); + + // Migrate the data. + // @todo db_update() should support this. + db_query('UPDATE {comment} SET created = changed'); + + // Recreate the indexes. + // The 'comment_num_new' index is optimized for comment_num_new() + // and comment_new_page_count(). + db_add_index('comment', 'comment_num_new', array('nid', 'status', 'created', 'cid', 'thread')); + db_add_index('comment', 'comment_pid_status', array('pid', 'status')); +} + +/** + * Upgrade the {node_comment_statistics} table. + */ +function comment_update_7004() { + db_add_field('node_comment_statistics', 'cid', array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {comment}.cid of the last comment.', + )); + db_add_index('node_comment_statistics', 'cid', array('cid')); + + // The comment_count index may have been added in Drupal 6. + if (!db_index_exists('node_comment_statistics', 'comment_count')) { + // Add an index on the comment_count. + db_add_index('node_comment_statistics', 'comment_count', array('comment_count')); + } +} + +/** + * Create the comment_body field. + */ +function comment_update_7005() { + // Create comment body field. + $field = array( + 'field_name' => 'comment_body', + 'type' => 'text_long', + 'module' => 'text', + 'entity_types' => array( + 'comment', + ), + 'settings' => array(), + 'cardinality' => 1, + ); + _update_7000_field_create_field($field); + + // Add the field to comments for all existing bundles. + $generic_instance = array( + 'entity_type' => 'comment', + 'label' => t('Comment'), + 'settings' => array( + 'text_processing' => 1, + ), + 'required' => TRUE, + 'display' => array( + 'default' => array( + 'label' => 'hidden', + 'type' => 'text_default', + 'weight' => 0, + 'settings' => array(), + 'module' => 'text', + ), + ), + 'widget' => array( + 'type' => 'text_textarea', + 'settings' => array( + 'rows' => 5, + ), + 'weight' => 0, + 'module' => 'text', + ), + 'description' => '', + ); + + $types = _update_7000_node_get_types(); + foreach ($types as $type => $type_object) { + $instance = $generic_instance; + $instance['bundle'] = 'comment_node_' . $type; + _update_7000_field_create_instance($field, $instance); + } +} + +/** + * Migrate data from the comment field to field storage. + */ +function comment_update_7006(&$sandbox) { + // This is a multipass update. First set up some comment variables. + if (empty($sandbox['total'])) { + $comments = (bool) db_query_range('SELECT 1 FROM {comment}', 0, 1)->fetchField(); + $sandbox['types'] = array(); + if ($comments) { + $sandbox['types'] = array_keys(_update_7000_node_get_types()); + } + $sandbox['total'] = count($sandbox['types']); + } + + if (!empty($sandbox['types'])) { + $type = array_shift($sandbox['types']); + + $query = db_select('comment', 'c'); + $query->innerJoin('node', 'n', 'c.nid = n.nid AND n.type = :type', array(':type' => $type)); + $query->addField('c', 'cid', 'entity_id'); + $query->addExpression("'comment_node_$type'", 'bundle'); + $query->addExpression("'comment'", 'entity_type'); + $query->addExpression('0', 'deleted'); + $query->addExpression("'" . LANGUAGE_NONE . "'", 'language'); + $query->addExpression('0', 'delta'); + $query->addField('c', 'comment', 'comment_body_value'); + $query->addField('c', 'format', 'comment_body_format'); + + db_insert('field_data_comment_body') + ->from($query) + ->execute(); + + $sandbox['#finished'] = 1 - count($sandbox['types']) / $sandbox['total']; + } + + // On the last pass of the update, $sandbox['types'] will be empty. + if (empty($sandbox['types'])) { + // Update the comment body text formats. For an explanation of these + // updates, see the code comments in user_update_7010(). + db_update('field_data_comment_body') + ->fields(array('comment_body_format' => NULL)) + ->condition('comment_body_value', '') + ->condition('comment_body_format', 0) + ->execute(); + $existing_formats = db_query("SELECT format FROM {filter_format}")->fetchCol(); + $default_format = variable_get('filter_default_format', 1); + db_update('field_data_comment_body') + ->fields(array('comment_body_format' => $default_format)) + ->isNotNull('comment_body_format') + ->condition('comment_body_format', $existing_formats, 'NOT IN') + ->execute(); + + // Finally, remove the old comment data. + db_drop_field('comment', 'comment'); + db_drop_field('comment', 'format'); + } +} + +/** + * @} End of "addtogroup updates-6.x-to-7.x". + */ + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Add an index to the created column. + */ +function comment_update_7007() { + db_add_index('comment', 'comment_created', array('created')); +} + +/** + * Update database to match Drupal 7 schema. + */ +function comment_update_7008() { + // Update default status to 1. + db_change_field('comment', 'status', 'status', array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 1, + 'size' => 'tiny', + )); + + // Realign indexes. + db_drop_index('comment', 'comment_status_pid'); + db_add_index('comment', 'comment_status_pid', array('pid', 'status')); + db_drop_index('comment', 'comment_pid_status'); + db_drop_index('comment', 'nid'); +} + +/** + * Change the last_comment_timestamp column description. + */ +function comment_update_7009() { + db_change_field('node_comment_statistics', 'last_comment_timestamp', 'last_comment_timestamp', array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.', + )); +} + +/** + * @} End of "addtogroup updates-7.x-extra". + */ + +/** + * Implements hook_schema(). + */ +function comment_schema() { + $schema['comment'] = array( + 'description' => 'Stores comments and associated data.', + 'fields' => array( + 'cid' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique comment ID.', + ), + 'pid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {comment}.cid to which this comment is a reply. If set to 0, this comment is not a reply to an existing comment.', + ), + 'nid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {node}.nid to which this comment is a reply.', + ), + 'uid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.', + ), + 'subject' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The comment title.', + ), + 'hostname' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => "The author's host name.", + ), + 'created' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The time that the comment was created, as a Unix timestamp.', + ), + 'changed' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The time that the comment was last edited, as a Unix timestamp.', + ), + 'status' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 1, + 'size' => 'tiny', + 'description' => 'The published status of a comment. (0 = Not Published, 1 = Published)', + ), + 'thread' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => "The vancode representation of the comment's place in a thread.", + ), + 'name' => array( + 'type' => 'varchar', + 'length' => 60, + 'not null' => FALSE, + 'description' => "The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form.", + ), + 'mail' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => FALSE, + 'description' => "The comment author's e-mail address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.", + ), + 'homepage' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + 'description' => "The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.", + ), + 'language' => array( + 'description' => 'The {languages}.language of this comment.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + ), + 'indexes' => array( + 'comment_status_pid' => array('pid', 'status'), + 'comment_num_new' => array('nid', 'status', 'created', 'cid', 'thread'), + 'comment_uid' => array('uid'), + 'comment_nid_language' => array('nid', 'language'), + 'comment_created' => array('created'), + ), + 'primary key' => array('cid'), + 'foreign keys' => array( + 'comment_node' => array( + 'table' => 'node', + 'columns' => array('nid' => 'nid'), + ), + 'comment_author' => array( + 'table' => 'users', + 'columns' => array('uid' => 'uid'), + ), + ), + ); + + $schema['node_comment_statistics'] = array( + 'description' => 'Maintains statistics of node and comments posts to show "new" and "updated" flags.', + 'fields' => array( + 'nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {node}.nid for which the statistics are compiled.', + ), + 'cid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The {comment}.cid of the last comment.', + ), + 'last_comment_timestamp' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.', + ), + 'last_comment_name' => array( + 'type' => 'varchar', + 'length' => 60, + 'not null' => FALSE, + 'description' => 'The name of the latest author to post a comment on this node, from {comment}.name.', + ), + 'last_comment_uid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The user ID of the latest author to post a comment on this node, from {comment}.uid.', + ), + 'comment_count' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The total number of comments on this node.', + ), + ), + 'primary key' => array('nid'), + 'indexes' => array( + 'node_comment_timestamp' => array('last_comment_timestamp'), + 'comment_count' => array('comment_count'), + 'last_comment_uid' => array('last_comment_uid'), + ), + 'foreign keys' => array( + 'statistics_node' => array( + 'table' => 'node', + 'columns' => array('nid' => 'nid'), + ), + 'last_comment_author' => array( + 'table' => 'users', + 'columns' => array( + 'last_comment_uid' => 'uid', + ), + ), + ), + ); + + return $schema; +} diff --git a/comment/comment.module b/comment/comment.module new file mode 100644 index 0000000..6f0df6c --- /dev/null +++ b/comment/comment.module @@ -0,0 +1,2746 @@ +' . t('About') . ''; + $output .= '

    ' . t('The Comment module allows users to comment on site content, set commenting defaults and permissions, and moderate comments. For more information, see the online handbook entry for Comment module.', array('@comment' => 'http://drupal.org/documentation/modules/comment/')) . '

    '; + $output .= '

    ' . t('Uses') . '

    '; + $output .= '
    '; + $output .= '
    ' . t('Default and custom settings') . '
    '; + $output .= '
    ' . t("Each content type can have its own default comment settings configured as: Open to allow new comments, Hidden to hide existing comments and prevent new comments, or Closed to view existing comments, but prevent new comments. These defaults will apply to all new content created (changes to the settings on existing content must be done manually). Other comment settings can also be customized per content type, and can be overridden for any given item of content. When a comment has no replies, it remains editable by its author, as long as the author has a user account and is logged in.", array('@content-type' => url('admin/structure/types'))) . '
    '; + $output .= '
    ' . t('Comment approval') . '
    '; + $output .= '
    ' . t("Comments from users who have the Skip comment approval permission are published immediately. All other comments are placed in the Unapproved comments queue, until a user who has permission to Administer comments publishes or deletes them. Published comments can be bulk managed on the Published comments administration page.", array('@comment-approval' => url('admin/content/comment/approval'), '@admin-comment' => url('admin/content/comment'))) . '
    '; + $output .= '
    '; + return $output; + } +} + +/** + * Implements hook_entity_info(). + */ +function comment_entity_info() { + $return = array( + 'comment' => array( + 'label' => t('Comment'), + 'base table' => 'comment', + 'uri callback' => 'comment_uri', + 'fieldable' => TRUE, + 'controller class' => 'CommentController', + 'entity keys' => array( + 'id' => 'cid', + 'bundle' => 'node_type', + 'label' => 'subject', + 'language' => 'language', + ), + 'bundles' => array(), + 'view modes' => array( + 'full' => array( + 'label' => t('Full comment'), + 'custom settings' => FALSE, + ), + ), + 'static cache' => FALSE, + ), + ); + + foreach (node_type_get_names() as $type => $name) { + $return['comment']['bundles']['comment_node_' . $type] = array( + 'label' => t('@node_type comment', array('@node_type' => $name)), + // Provide the node type/bundle name for other modules, so it does not + // have to be extracted manually from the bundle name. + 'node bundle' => $type, + 'admin' => array( + // Place the Field UI paths for comments one level below the + // corresponding paths for nodes, so that they appear in the same set + // of local tasks. Note that the paths use a different placeholder name + // and thus a different menu loader callback, so that Field UI page + // callbacks get a comment bundle name from the node type in the URL. + // See comment_node_type_load() and comment_menu_alter(). + 'path' => 'admin/structure/types/manage/%comment_node_type/comment', + 'bundle argument' => 4, + 'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type) . '/comment', + 'access arguments' => array('administer content types'), + ), + ); + } + + return $return; +} + +/** + * Menu loader callback for Field UI paths. + * + * Return a comment bundle name from a node type in the URL. + */ +function comment_node_type_load($name) { + if ($type = node_type_get_type(strtr($name, array('-' => '_')))) { + return 'comment_node_' . $type->type; + } +} + +/** + * Implements callback_entity_info_uri(). + */ +function comment_uri($comment) { + return array( + 'path' => 'comment/' . $comment->cid, + 'options' => array('fragment' => 'comment-' . $comment->cid), + ); +} + +/** + * Implements hook_field_extra_fields(). + */ +function comment_field_extra_fields() { + $return = array(); + + foreach (node_type_get_types() as $type) { + if (variable_get('comment_subject_field_' . $type->type, 1) == 1) { + $return['comment']['comment_node_' . $type->type] = array( + 'form' => array( + 'author' => array( + 'label' => t('Author'), + 'description' => t('Author textfield'), + 'weight' => -2, + ), + 'subject' => array( + 'label' => t('Subject'), + 'description' => t('Subject textfield'), + 'weight' => -1, + ), + ), + ); + } + } + + return $return; +} + +/** + * Implements hook_theme(). + */ +function comment_theme() { + return array( + 'comment_block' => array( + 'variables' => array(), + ), + 'comment_preview' => array( + 'variables' => array('comment' => NULL), + ), + 'comment' => array( + 'template' => 'comment', + 'render element' => 'elements', + ), + 'comment_post_forbidden' => array( + 'variables' => array('node' => NULL), + ), + 'comment_wrapper' => array( + 'template' => 'comment-wrapper', + 'render element' => 'content', + ), + ); +} + +/** + * Implements hook_menu(). + */ +function comment_menu() { + $items['admin/content/comment'] = array( + 'title' => 'Comments', + 'description' => 'List and edit site comments and the comment approval queue.', + 'page callback' => 'comment_admin', + 'access arguments' => array('administer comments'), + 'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM, + 'file' => 'comment.admin.inc', + ); + // Tabs begin here. + $items['admin/content/comment/new'] = array( + 'title' => 'Published comments', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items['admin/content/comment/approval'] = array( + 'title' => 'Unapproved comments', + 'title callback' => 'comment_count_unpublished', + 'page arguments' => array('approval'), + 'access arguments' => array('administer comments'), + 'type' => MENU_LOCAL_TASK, + ); + $items['comment/%'] = array( + 'title' => 'Comment permalink', + 'page callback' => 'comment_permalink', + 'page arguments' => array(1), + 'access arguments' => array('access comments'), + ); + $items['comment/%/view'] = array( + 'title' => 'View comment', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + // Every other comment path uses %, but this one loads the comment directly, + // so we don't end up loading it twice (in the page and access callback). + $items['comment/%comment/edit'] = array( + 'title' => 'Edit', + 'page callback' => 'comment_edit_page', + 'page arguments' => array(1), + 'access callback' => 'comment_access', + 'access arguments' => array('edit', 1), + 'type' => MENU_LOCAL_TASK, + 'weight' => 0, + ); + $items['comment/%/approve'] = array( + 'title' => 'Approve', + 'page callback' => 'comment_approve', + 'page arguments' => array(1), + 'access arguments' => array('administer comments'), + 'file' => 'comment.pages.inc', + 'weight' => 1, + ); + $items['comment/%/delete'] = array( + 'title' => 'Delete', + 'page callback' => 'comment_confirm_delete_page', + 'page arguments' => array(1), + 'access arguments' => array('administer comments'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'comment.admin.inc', + 'weight' => 2, + ); + $items['comment/reply/%node'] = array( + 'title' => 'Add new comment', + 'page callback' => 'comment_reply', + 'page arguments' => array(2), + 'access callback' => 'node_access', + 'access arguments' => array('view', 2), + 'file' => 'comment.pages.inc', + ); + + return $items; +} + +/** + * Implements hook_menu_alter(). + */ +function comment_menu_alter(&$items) { + // Add comments to the description for admin/content. + $items['admin/content']['description'] = 'Administer content and comments.'; + + // Adjust the Field UI tabs on admin/structure/types/manage/[node-type]. + // See comment_entity_info(). + $items['admin/structure/types/manage/%comment_node_type/comment/fields']['title'] = 'Comment fields'; + $items['admin/structure/types/manage/%comment_node_type/comment/fields']['weight'] = 3; + $items['admin/structure/types/manage/%comment_node_type/comment/display']['title'] = 'Comment display'; + $items['admin/structure/types/manage/%comment_node_type/comment/display']['weight'] = 4; +} + +/** + * Returns a menu title which includes the number of unapproved comments. + */ +function comment_count_unpublished() { + $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array( + ':status' => COMMENT_NOT_PUBLISHED, + ))->fetchField(); + return t('Unapproved comments (@count)', array('@count' => $count)); +} + +/** + * Implements hook_node_type_insert(). + * + * Creates a comment body field for a node type created while the comment module + * is enabled. For node types created before the comment module is enabled, + * hook_modules_enabled() serves to create the body fields. + * + * @see comment_modules_enabled() + */ +function comment_node_type_insert($info) { + _comment_body_field_create($info); +} + +/** + * Implements hook_node_type_update(). + */ +function comment_node_type_update($info) { + if (!empty($info->old_type) && $info->type != $info->old_type) { + field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type); + } +} + +/** + * Implements hook_node_type_delete(). + */ +function comment_node_type_delete($info) { + field_attach_delete_bundle('comment', 'comment_node_' . $info->type); + $settings = array( + 'comment', + 'comment_default_mode', + 'comment_default_per_page', + 'comment_anonymous', + 'comment_subject_field', + 'comment_preview', + 'comment_form_location', + ); + foreach ($settings as $setting) { + variable_del($setting . '_' . $info->type); + } +} + + /** + * Creates a comment_body field instance for a given node type. + */ +function _comment_body_field_create($info) { + // Create the field if needed. + if (!field_read_field('comment_body', array('include_inactive' => TRUE))) { + $field = array( + 'field_name' => 'comment_body', + 'type' => 'text_long', + 'entity_types' => array('comment'), + ); + field_create_field($field); + } + // Create the instance if needed. + if (!field_read_instance('comment', 'comment_body', 'comment_node_' . $info->type, array('include_inactive' => TRUE))) { + field_attach_create_bundle('comment', 'comment_node_' . $info->type); + // Attaches the body field by default. + $instance = array( + 'field_name' => 'comment_body', + 'label' => 'Comment', + 'entity_type' => 'comment', + 'bundle' => 'comment_node_' . $info->type, + 'settings' => array('text_processing' => 1), + 'required' => TRUE, + 'display' => array( + 'default' => array( + 'label' => 'hidden', + 'type' => 'text_default', + 'weight' => 0, + ), + ), + ); + field_create_instance($instance); + } +} + +/** + * Implements hook_permission(). + */ +function comment_permission() { + return array( + 'administer comments' => array( + 'title' => t('Administer comments and comment settings'), + ), + 'access comments' => array( + 'title' => t('View comments'), + ), + 'post comments' => array( + 'title' => t('Post comments'), + ), + 'skip comment approval' => array( + 'title' => t('Skip comment approval'), + ), + 'edit own comments' => array( + 'title' => t('Edit own comments'), + ), + ); +} + +/** + * Implements hook_block_info(). + */ +function comment_block_info() { + $blocks['recent']['info'] = t('Recent comments'); + $blocks['recent']['properties']['administrative'] = TRUE; + + return $blocks; +} + +/** + * Implements hook_block_configure(). + */ +function comment_block_configure($delta = '') { + $form['comment_block_count'] = array( + '#type' => 'select', + '#title' => t('Number of recent comments'), + '#default_value' => variable_get('comment_block_count', 10), + '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)), + ); + + return $form; +} + +/** + * Implements hook_block_save(). + */ +function comment_block_save($delta = '', $edit = array()) { + variable_set('comment_block_count', (int) $edit['comment_block_count']); +} + +/** + * Implements hook_block_view(). + * + * Generates a block with the most recent comments. + */ +function comment_block_view($delta = '') { + if (user_access('access comments')) { + $block['subject'] = t('Recent comments'); + $block['content'] = theme('comment_block'); + + return $block; + } +} + +/** + * Redirects comment links to the correct page depending on comment settings. + * + * Since comments are paged there is no way to guarantee which page a comment + * appears on. Comment paging and threading settings may be changed at any time. + * With threaded comments, an individual comment may move between pages as + * comments can be added either before or after it in the overall discussion. + * Therefore we use a central routing function for comment links, which + * calculates the page number based on current comment settings and returns + * the full comment view with the pager set dynamically. + * + * @param $cid + * A comment identifier. + * @return + * The comment listing set to the page on which the comment appears. + */ +function comment_permalink($cid) { + if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) { + + // Find the current display page for this comment. + $page = comment_get_display_page($comment->cid, $node->type); + + // Set $_GET['q'] and $_GET['page'] ourselves so that the node callback + // behaves as it would when visiting the page directly. + $_GET['q'] = 'node/' . $node->nid; + $_GET['page'] = $page; + + // Return the node view, this will show the correct comment in context. + return menu_execute_active_handler('node/' . $node->nid, FALSE); + } + return MENU_NOT_FOUND; +} + +/** + * Find the most recent comments that are available to the current user. + * + * @param integer $number + * (optional) The maximum number of comments to find. Defaults to 10. + * + * @return + * An array of comment objects or an empty array if there are no recent + * comments visible to the current user. + */ +function comment_get_recent($number = 10) { + $query = db_select('comment', 'c'); + $query->innerJoin('node', 'n', 'n.nid = c.nid'); + $query->addTag('node_access'); + $comments = $query + ->fields('c') + ->condition('c.status', COMMENT_PUBLISHED) + ->condition('n.status', NODE_PUBLISHED) + ->orderBy('c.created', 'DESC') + // Additionally order by cid to ensure that comments with the same timestamp + // are returned in the exact order posted. + ->orderBy('c.cid', 'DESC') + ->range(0, $number) + ->execute() + ->fetchAll(); + + return $comments ? $comments : array(); +} + +/** + * Calculate page number for first new comment. + * + * @param $num_comments + * Number of comments. + * @param $new_replies + * Number of new replies. + * @param $node + * The first new comment node. + * @return + * "page=X" if the page number is greater than zero; empty string otherwise. + */ +function comment_new_page_count($num_comments, $new_replies, $node) { + $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED); + $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50); + $pagenum = NULL; + $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE; + if ($num_comments <= $comments_per_page) { + // Only one page of comments. + $pageno = 0; + } + elseif ($flat) { + // Flat comments. + $count = $num_comments - $new_replies; + $pageno = $count / $comments_per_page; + } + else { + // Threaded comments: we build a query with a subquery to find the first + // thread with a new comment. + + // 1. Find all the threads with a new comment. + $unread_threads_query = db_select('comment') + ->fields('comment', array('thread')) + ->condition('nid', $node->nid) + ->condition('status', COMMENT_PUBLISHED) + ->orderBy('created', 'DESC') + ->orderBy('cid', 'DESC') + ->range(0, $new_replies); + + // 2. Find the first thread. + $first_thread = db_select($unread_threads_query, 'thread') + ->fields('thread', array('thread')) + ->orderBy('SUBSTRING(thread, 1, (LENGTH(thread) - 1))') + ->range(0, 1) + ->execute() + ->fetchField(); + + // Remove the final '/'. + $first_thread = substr($first_thread, 0, -1); + + // Find the number of the first comment of the first unread thread. + $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array( + ':status' => COMMENT_PUBLISHED, + ':nid' => $node->nid, + ':thread' => $first_thread, + ))->fetchField(); + + $pageno = $count / $comments_per_page; + } + + if ($pageno >= 1) { + $pagenum = array('page' => intval($pageno)); + } + + return $pagenum; +} + +/** + * Returns HTML for a list of recent comments to be displayed in the comment block. + * + * @ingroup themeable + */ +function theme_comment_block() { + $items = array(); + $number = variable_get('comment_block_count', 10); + foreach (comment_get_recent($number) as $comment) { + $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . ' ' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed))) . ''; + } + + if ($items) { + return theme('item_list', array('items' => $items)); + } + else { + return t('No comments available.'); + } +} + +/** + * Implements hook_node_view(). + */ +function comment_node_view($node, $view_mode) { + $links = array(); + + if ($node->comment != COMMENT_NODE_HIDDEN) { + if ($view_mode == 'rss') { + // Add a comments RSS element which is a URL to the comments of this node. + $node->rss_elements[] = array( + 'key' => 'comments', + 'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE)) + ); + } + elseif ($view_mode == 'teaser') { + // Teaser view: display the number of comments that have been posted, + // or a link to add new comments if the user has permission, the node + // is open to new comments, and there currently are none. + if (user_access('access comments')) { + if (!empty($node->comment_count)) { + $links['comment-comments'] = array( + 'title' => format_plural($node->comment_count, '1 comment', '@count comments'), + 'href' => "node/$node->nid", + 'attributes' => array('title' => t('Jump to the first comment of this posting.')), + 'fragment' => 'comments', + 'html' => TRUE, + ); + // Show a link to the first new comment. + if ($new = comment_num_new($node->nid)) { + $links['comment-new-comments'] = array( + 'title' => format_plural($new, '1 new comment', '@count new comments'), + 'href' => "node/$node->nid", + 'query' => comment_new_page_count($node->comment_count, $new, $node), + 'attributes' => array('title' => t('Jump to the first new comment of this posting.')), + 'fragment' => 'new', + 'html' => TRUE, + ); + } + } + } + if ($node->comment == COMMENT_NODE_OPEN) { + if (user_access('post comments')) { + $links['comment-add'] = array( + 'title' => t('Add new comment'), + 'href' => "comment/reply/$node->nid", + 'attributes' => array('title' => t('Add a new comment to this page.')), + 'fragment' => 'comment-form', + ); + } + else { + $links['comment_forbidden'] = array( + 'title' => theme('comment_post_forbidden', array('node' => $node)), + 'html' => TRUE, + ); + } + } + } + elseif ($view_mode != 'search_index' && $view_mode != 'search_result') { + // Node in other view modes: add a "post comment" link if the user is + // allowed to post comments and if this node is allowing new comments. + // But we don't want this link if we're building the node for search + // indexing or constructing a search result excerpt. + if ($node->comment == COMMENT_NODE_OPEN) { + $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW); + if (user_access('post comments')) { + // Show the "post comment" link if the form is on another page, or + // if there are existing comments that the link will skip past. + if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($node->comment_count) && user_access('access comments'))) { + $links['comment-add'] = array( + 'title' => t('Add new comment'), + 'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')), + 'href' => "node/$node->nid", + 'fragment' => 'comment-form', + ); + if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) { + $links['comment-add']['href'] = "comment/reply/$node->nid"; + } + } + } + else { + $links['comment_forbidden'] = array( + 'title' => theme('comment_post_forbidden', array('node' => $node)), + 'html' => TRUE, + ); + } + } + } + + $node->content['links']['comment'] = array( + '#theme' => 'links__node__comment', + '#links' => $links, + '#attributes' => array('class' => array('links', 'inline')), + ); + + // Only append comments when we are building a node on its own node detail + // page. We compare $node and $page_node to ensure that comments are not + // appended to other nodes shown on the page, for example a node_reference + // displayed in 'full' view mode within another node. + if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) { + $node->content['comments'] = comment_node_page_additions($node); + } + } +} + +/** + * Build the comment-related elements for node detail pages. + * + * @param $node + * A node object. + */ +function comment_node_page_additions($node) { + $additions = array(); + + // Only attempt to render comments if the node has visible comments. + // Unpublished comments are not included in $node->comment_count, so show + // comments unconditionally if the user is an administrator. + if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) { + $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED); + $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50); + if ($cids = comment_get_thread($node, $mode, $comments_per_page)) { + $comments = comment_load_multiple($cids); + comment_prepare_thread($comments); + $build = comment_view_multiple($comments, $node); + $build['pager']['#theme'] = 'pager'; + $additions['comments'] = $build; + } + } + + // Append comment form if needed. + if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) { + $build = drupal_get_form("comment_node_{$node->type}_form", (object) array('nid' => $node->nid)); + $additions['comment_form'] = $build; + } + + if ($additions) { + $additions += array( + '#theme' => 'comment_wrapper__node_' . $node->type, + '#node' => $node, + 'comments' => array(), + 'comment_form' => array(), + ); + } + + return $additions; +} + +/** + * Retrieve comments for a thread. + * + * @param $node + * The node whose comment(s) needs rendering. + * @param $mode + * The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED. + * @param $comments_per_page + * The amount of comments to display per page. + * + * To display threaded comments in the correct order we keep a 'thread' field + * and order by that value. This field keeps this data in + * a way which is easy to update and convenient to use. + * + * A "thread" value starts at "1". If we add a child (A) to this comment, + * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next + * brother of (A) will get "1.2". Next brother of the parent of (A) will get + * "2" and so on. + * + * First of all note that the thread field stores the depth of the comment: + * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc. + * + * Now to get the ordering right, consider this example: + * + * 1 + * 1.1 + * 1.1.1 + * 1.2 + * 2 + * + * If we "ORDER BY thread ASC" we get the above result, and this is the + * natural order sorted by time. However, if we "ORDER BY thread DESC" + * we get: + * + * 2 + * 1.2 + * 1.1.1 + * 1.1 + * 1 + * + * Clearly, this is not a natural way to see a thread, and users will get + * confused. The natural order to show a thread by time desc would be: + * + * 2 + * 1 + * 1.2 + * 1.1 + * 1.1.1 + * + * which is what we already did before the standard pager patch. To achieve + * this we simply add a "/" at the end of each "thread" value. This way, the + * thread fields will look like this: + * + * 1/ + * 1.1/ + * 1.1.1/ + * 1.2/ + * 2/ + * + * we add "/" since this char is, in ASCII, higher than every number, so if + * now we "ORDER BY thread DESC" we get the correct order. However this would + * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need + * to consider the trailing "/" so we use a substring only. + */ +function comment_get_thread($node, $mode, $comments_per_page) { + $query = db_select('comment', 'c')->extend('PagerDefault'); + $query->addField('c', 'cid'); + $query + ->condition('c.nid', $node->nid) + ->addTag('node_access') + ->addTag('comment_filter') + ->addMetaData('node', $node) + ->limit($comments_per_page); + + $count_query = db_select('comment', 'c'); + $count_query->addExpression('COUNT(*)'); + $count_query + ->condition('c.nid', $node->nid) + ->addTag('node_access') + ->addTag('comment_filter') + ->addMetaData('node', $node); + + if (!user_access('administer comments')) { + $query->condition('c.status', COMMENT_PUBLISHED); + $count_query->condition('c.status', COMMENT_PUBLISHED); + } + if ($mode === COMMENT_MODE_FLAT) { + $query->orderBy('c.cid', 'ASC'); + } + else { + // See comment above. Analysis reveals that this doesn't cost too + // much. It scales much much better than having the whole comment + // structure. + $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder'); + $query->orderBy('torder', 'ASC'); + } + + $query->setCountQuery($count_query); + $cids = $query->execute()->fetchCol(); + + return $cids; +} + +/** + * Loop over comment thread, noting indentation level. + * + * @param array $comments + * An array of comment objects, keyed by cid. + * @return + * The $comments argument is altered by reference with indentation information. + */ +function comment_prepare_thread(&$comments) { + // A flag stating if we are still searching for first new comment on the thread. + $first_new = TRUE; + + // A counter that helps track how indented we are. + $divs = 0; + + foreach ($comments as $key => $comment) { + if ($first_new && $comment->new != MARK_READ) { + // Assign the anchor only for the first new comment. This avoids duplicate + // id attributes on a page. + $first_new = FALSE; + $comment->first_new = TRUE; + } + + // The $divs element instructs #prefix whether to add an indent div or + // close existing divs (a negative value). + $comment->depth = count(explode('.', $comment->thread)) - 1; + if ($comment->depth > $divs) { + $comment->divs = 1; + $divs++; + } + else { + $comment->divs = $comment->depth - $divs; + while ($comment->depth < $divs) { + $divs--; + } + } + $comments[$key] = $comment; + } + + // The final comment must close up some hanging divs + $comments[$key]->divs_final = $divs; +} + +/** + * Generate an array for rendering the given comment. + * + * @param $comment + * A comment object. + * @param $node + * The node the comment is attached to. + * @param $view_mode + * View mode, e.g. 'full', 'teaser'... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * + * @return + * An array as expected by drupal_render(). + */ +function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) { + if (!isset($langcode)) { + $langcode = $GLOBALS['language_content']->language; + } + + // Populate $comment->content with a render() array. + comment_build_content($comment, $node, $view_mode, $langcode); + + $build = $comment->content; + // We don't need duplicate rendering info in comment->content. + unset($comment->content); + + $build += array( + '#theme' => 'comment__node_' . $node->type, + '#comment' => $comment, + '#node' => $node, + '#view_mode' => $view_mode, + '#language' => $langcode, + ); + + if (empty($comment->in_preview)) { + $prefix = ''; + $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED; + + // Add 'new' anchor if needed. + if (!empty($comment->first_new)) { + $prefix .= "\n"; + } + + // Add indentation div or close open divs as needed. + if ($is_threaded) { + $prefix .= $comment->divs <= 0 ? str_repeat('', abs($comment->divs)) : "\n" . '
    '; + } + + // Add anchor for each comment. + $prefix .= "cid\">\n"; + $build['#prefix'] = $prefix; + + // Close all open divs. + if ($is_threaded && !empty($comment->divs_final)) { + $build['#suffix'] = str_repeat('
    ', $comment->divs_final); + } + } + + // Allow modules to modify the structured comment. + $type = 'comment'; + drupal_alter(array('comment_view', 'entity_view'), $build, $type); + + return $build; +} + +/** + * Builds a structured array representing the comment's content. + * + * The content built for the comment (field values, comments, file attachments or + * other comment components) will vary depending on the $view_mode parameter. + * + * @param $comment + * A comment object. + * @param $node + * The node the comment is attached to. + * @param $view_mode + * View mode, e.g. 'full', 'teaser'... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + */ +function comment_build_content($comment, $node, $view_mode = 'full', $langcode = NULL) { + if (!isset($langcode)) { + $langcode = $GLOBALS['language_content']->language; + } + + // Remove previously built content, if exists. + $comment->content = array(); + + // Allow modules to change the view mode. + $view_mode = key(entity_view_mode_prepare('comment', array($comment->cid => $comment), $view_mode, $langcode)); + + // Build fields content. + field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode); + entity_prepare_view('comment', array($comment->cid => $comment), $langcode); + $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode); + + $comment->content['links'] = array( + '#theme' => 'links__comment', + '#pre_render' => array('drupal_pre_render_links'), + '#attributes' => array('class' => array('links', 'inline')), + ); + if (empty($comment->in_preview)) { + $comment->content['links']['comment'] = array( + '#theme' => 'links__comment__comment', + '#links' => comment_links($comment, $node), + '#attributes' => array('class' => array('links', 'inline')), + ); + } + + // Allow modules to make their own additions to the comment. + module_invoke_all('comment_view', $comment, $view_mode, $langcode); + module_invoke_all('entity_view', $comment, 'comment', $view_mode, $langcode); + + // Make sure the current view mode is stored if no module has already + // populated the related key. + $comment->content += array('#view_mode' => $view_mode); +} + +/** + * Helper function, build links for an individual comment. + * + * Adds reply, edit, delete etc. depending on the current user permissions. + * + * @param $comment + * The comment object. + * @param $node + * The node the comment is attached to. + * @return + * A structured array of links. + */ +function comment_links($comment, $node) { + $links = array(); + if ($node->comment == COMMENT_NODE_OPEN) { + if (user_access('administer comments') && user_access('post comments')) { + $links['comment-delete'] = array( + 'title' => t('delete'), + 'href' => "comment/$comment->cid/delete", + 'html' => TRUE, + ); + $links['comment-edit'] = array( + 'title' => t('edit'), + 'href' => "comment/$comment->cid/edit", + 'html' => TRUE, + ); + $links['comment-reply'] = array( + 'title' => t('reply'), + 'href' => "comment/reply/$comment->nid/$comment->cid", + 'html' => TRUE, + ); + if ($comment->status == COMMENT_NOT_PUBLISHED) { + $links['comment-approve'] = array( + 'title' => t('approve'), + 'href' => "comment/$comment->cid/approve", + 'html' => TRUE, + 'query' => array('token' => drupal_get_token("comment/$comment->cid/approve")), + ); + } + } + elseif (user_access('post comments')) { + if (comment_access('edit', $comment)) { + $links['comment-edit'] = array( + 'title' => t('edit'), + 'href' => "comment/$comment->cid/edit", + 'html' => TRUE, + ); + } + $links['comment-reply'] = array( + 'title' => t('reply'), + 'href' => "comment/reply/$comment->nid/$comment->cid", + 'html' => TRUE, + ); + } + else { + $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node)); + $links['comment_forbidden']['html'] = TRUE; + } + } + return $links; +} + +/** + * Construct a drupal_render() style array from an array of loaded comments. + * + * @param $comments + * An array of comments as returned by comment_load_multiple(). + * @param $node + * The node the comments are attached to. + * @param $view_mode + * View mode, e.g. 'full', 'teaser'... + * @param $weight + * An integer representing the weight of the first comment in the list. + * @param $langcode + * A string indicating the language field values are to be shown in. If no + * language is provided the current content language is used. + * + * @return + * An array in the format expected by drupal_render(). + */ +function comment_view_multiple($comments, $node, $view_mode = 'full', $weight = 0, $langcode = NULL) { + $build = array(); + $entities_by_view_mode = entity_view_mode_prepare('comment', $comments, $view_mode, $langcode); + foreach ($entities_by_view_mode as $entity_view_mode => $entities) { + field_attach_prepare_view('comment', $entities, $entity_view_mode, $langcode); + entity_prepare_view('comment', $entities, $langcode); + + foreach ($entities as $entity) { + $build[$entity->cid] = comment_view($entity, $node, $entity_view_mode, $langcode); + } + } + + foreach ($comments as $comment) { + $build[$comment->cid]['#weight'] = $weight; + $weight++; + } + // Sort here, to preserve the input order of the entities that were passed to + // this function. + uasort($build, 'element_sort'); + $build['#sorted'] = TRUE; + + return $build; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function comment_form_node_type_form_alter(&$form, $form_state) { + if (isset($form['type'])) { + $form['comment'] = array( + '#type' => 'fieldset', + '#title' => t('Comment settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#group' => 'additional_settings', + '#attributes' => array( + 'class' => array('comment-node-type-settings-form'), + ), + '#attached' => array( + 'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'), + ), + ); + // Unlike coment_form_node_form_alter(), all of these settings are applied + // as defaults to all new nodes. Therefore, it would be wrong to use #states + // to hide the other settings based on the primary comment setting. + $form['comment']['comment'] = array( + '#type' => 'select', + '#title' => t('Default comment setting for new content'), + '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN), + '#options' => array( + COMMENT_NODE_OPEN => t('Open'), + COMMENT_NODE_CLOSED => t('Closed'), + COMMENT_NODE_HIDDEN => t('Hidden'), + ), + ); + $form['comment']['comment_default_mode'] = array( + '#type' => 'checkbox', + '#title' => t('Threading'), + '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED), + '#description' => t('Show comment replies in a threaded list.'), + ); + $form['comment']['comment_default_per_page'] = array( + '#type' => 'select', + '#title' => t('Comments per page'), + '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50), + '#options' => _comment_per_page(), + ); + $form['comment']['comment_anonymous'] = array( + '#type' => 'select', + '#title' => t('Anonymous commenting'), + '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT), + '#options' => array( + COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'), + COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'), + COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'), + ), + '#access' => user_access('post comments', drupal_anonymous_user()), + ); + $form['comment']['comment_subject_field'] = array( + '#type' => 'checkbox', + '#title' => t('Allow comment title'), + '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1), + ); + $form['comment']['comment_form_location'] = array( + '#type' => 'checkbox', + '#title' => t('Show reply form on the same page as comments'), + '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW), + ); + $form['comment']['comment_preview'] = array( + '#type' => 'radios', + '#title' => t('Preview comment'), + '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, DRUPAL_OPTIONAL), + '#options' => array( + DRUPAL_DISABLED => t('Disabled'), + DRUPAL_OPTIONAL => t('Optional'), + DRUPAL_REQUIRED => t('Required'), + ), + ); + } +} + +/** + * Implements hook_form_BASE_FORM_ID_alter(). + */ +function comment_form_node_form_alter(&$form, $form_state) { + $node = $form['#node']; + $form['comment_settings'] = array( + '#type' => 'fieldset', + '#access' => user_access('administer comments'), + '#title' => t('Comment settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#group' => 'additional_settings', + '#attributes' => array( + 'class' => array('comment-node-settings-form'), + ), + '#attached' => array( + 'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'), + ), + '#weight' => 30, + ); + $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0; + $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment; + $form['comment_settings']['comment'] = array( + '#type' => 'radios', + '#title' => t('Comments'), + '#title_display' => 'invisible', + '#parents' => array('comment'), + '#default_value' => $comment_settings, + '#options' => array( + COMMENT_NODE_OPEN => t('Open'), + COMMENT_NODE_CLOSED => t('Closed'), + COMMENT_NODE_HIDDEN => t('Hidden'), + ), + COMMENT_NODE_OPEN => array( + '#description' => t('Users with the "Post comments" permission can post comments.'), + ), + COMMENT_NODE_CLOSED => array( + '#description' => t('Users cannot post comments, but existing comments will be displayed.'), + ), + COMMENT_NODE_HIDDEN => array( + '#description' => t('Comments are hidden from view.'), + ), + ); + // If the node doesn't have any comments, the "hidden" option makes no + // sense, so don't even bother presenting it to the user. + if (empty($comment_count)) { + $form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]['#access'] = FALSE; + // Also adjust the description of the "closed" option. + $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = t('Users cannot post comments.'); + } +} + +/** + * Implements hook_node_load(). + */ +function comment_node_load($nodes, $types) { + $comments_enabled = array(); + + // Check if comments are enabled for each node. If comments are disabled, + // assign values without hitting the database. + foreach ($nodes as $node) { + // Store whether comments are enabled for this node. + if ($node->comment != COMMENT_NODE_HIDDEN) { + $comments_enabled[] = $node->nid; + } + else { + $node->cid = 0; + $node->last_comment_timestamp = $node->created; + $node->last_comment_name = ''; + $node->last_comment_uid = $node->uid; + $node->comment_count = 0; + } + } + + // For nodes with comments enabled, fetch information from the database. + if (!empty($comments_enabled)) { + $result = db_query('SELECT nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count FROM {node_comment_statistics} WHERE nid IN (:comments_enabled)', array(':comments_enabled' => $comments_enabled)); + foreach ($result as $record) { + $nodes[$record->nid]->cid = $record->cid; + $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp; + $nodes[$record->nid]->last_comment_name = $record->last_comment_name; + $nodes[$record->nid]->last_comment_uid = $record->last_comment_uid; + $nodes[$record->nid]->comment_count = $record->comment_count; + } + } +} + +/** + * Implements hook_node_prepare(). + */ +function comment_node_prepare($node) { + if (!isset($node->comment)) { + $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN); + } +} + +/** + * Implements hook_node_insert(). + */ +function comment_node_insert($node) { + // Allow bulk updates and inserts to temporarily disable the + // maintenance of the {node_comment_statistics} table. + if (variable_get('comment_maintain_node_statistics', TRUE)) { + db_insert('node_comment_statistics') + ->fields(array( + 'nid' => $node->nid, + 'cid' => 0, + 'last_comment_timestamp' => $node->changed, + 'last_comment_name' => NULL, + 'last_comment_uid' => $node->uid, + 'comment_count' => 0, + )) + ->execute(); + } +} + +/** + * Implements hook_node_delete(). + */ +function comment_node_delete($node) { + $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol(); + comment_delete_multiple($cids); + db_delete('node_comment_statistics') + ->condition('nid', $node->nid) + ->execute(); +} + +/** + * Implements hook_node_update_index(). + */ +function comment_node_update_index($node) { + $index_comments = &drupal_static(__FUNCTION__); + + if ($index_comments === NULL) { + // Find and save roles that can 'access comments' or 'search content'. + $perms = array('access comments' => array(), 'search content' => array()); + $result = db_query("SELECT rid, permission FROM {role_permission} WHERE permission IN ('access comments', 'search content')"); + foreach ($result as $record) { + $perms[$record->permission][$record->rid] = $record->rid; + } + + // Prevent indexing of comments if there are any roles that can search but + // not view comments. + $index_comments = TRUE; + foreach ($perms['search content'] as $rid) { + if (!isset($perms['access comments'][$rid]) && ($rid <= DRUPAL_AUTHENTICATED_RID || !isset($perms['access comments'][DRUPAL_AUTHENTICATED_RID]))) { + $index_comments = FALSE; + break; + } + } + } + + if ($index_comments) { + $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED); + $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50); + if ($node->comment && $cids = comment_get_thread($node, $mode, $comments_per_page)) { + $comments = comment_load_multiple($cids); + comment_prepare_thread($comments); + $build = comment_view_multiple($comments, $node); + return drupal_render($build); + } + } + return ''; +} + +/** + * Implements hook_update_index(). + */ +function comment_update_index() { + // Store the maximum possible comments per thread (used for ranking by reply count) + variable_set('node_cron_comments_scale', 1.0 / max(1, db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')->fetchField())); +} + +/** + * Implements hook_node_search_result(). + * + * Formats a comment count string and returns it, for display with search + * results. + */ +function comment_node_search_result($node) { + // Do not make a string if comments are hidden. + if (user_access('access comments') && $node->comment != COMMENT_NODE_HIDDEN) { + $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField(); + // Do not make a string if comments are closed and there are currently + // zero comments. + if ($node->comment != COMMENT_NODE_CLOSED || $comments > 0) { + return array('comment' => format_plural($comments, '1 comment', '@count comments')); + } + } +} + +/** + * Implements hook_user_cancel(). + */ +function comment_user_cancel($edit, $account, $method) { + switch ($method) { + case 'user_cancel_block_unpublish': + $comments = comment_load_multiple(array(), array('uid' => $account->uid)); + foreach ($comments as $comment) { + $comment->status = 0; + comment_save($comment); + } + break; + + case 'user_cancel_reassign': + $comments = comment_load_multiple(array(), array('uid' => $account->uid)); + foreach ($comments as $comment) { + $comment->uid = 0; + comment_save($comment); + } + break; + } +} + +/** + * Implements hook_user_delete(). + */ +function comment_user_delete($account) { + $cids = db_query('SELECT c.cid FROM {comment} c WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol(); + comment_delete_multiple($cids); +} + +/** + * Determines whether the current user has access to a particular comment. + * + * Authenticated users can edit their comments as long they have not been + * replied to. This prevents people from changing or revising their statements + * based on the replies to their posts. + * + * @param $op + * The operation that is to be performed on the comment. Only 'edit' is + * recognized now. + * @param $comment + * The comment object. + * @return + * TRUE if the current user has acces to the comment, FALSE otherwise. + */ +function comment_access($op, $comment) { + global $user; + + if ($op == 'edit') { + return ($user->uid && $user->uid == $comment->uid && $comment->status == COMMENT_PUBLISHED && user_access('edit own comments')) || user_access('administer comments'); + } +} + +/** + * Accepts a submission of new or changed comment content. + * + * @param $comment + * A comment object. + */ +function comment_save($comment) { + global $user; + + $transaction = db_transaction(); + try { + $defaults = array( + 'mail' => '', + 'homepage' => '', + 'name' => '', + 'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED, + ); + foreach ($defaults as $key => $default) { + if (!isset($comment->$key)) { + $comment->$key = $default; + } + } + // Make sure we have a bundle name. + if (!isset($comment->node_type)) { + $node = node_load($comment->nid); + $comment->node_type = 'comment_node_' . $node->type; + } + + // Load the stored entity, if any. + if (!empty($comment->cid) && !isset($comment->original)) { + $comment->original = entity_load_unchanged('comment', $comment->cid); + } + + field_attach_presave('comment', $comment); + + // Allow modules to alter the comment before saving. + module_invoke_all('comment_presave', $comment); + module_invoke_all('entity_presave', $comment, 'comment'); + + if ($comment->cid) { + + drupal_write_record('comment', $comment, 'cid'); + + // Ignore slave server temporarily to give time for the + // saved comment to be propagated to the slave. + db_ignore_slave(); + + // Update the {node_comment_statistics} table prior to executing hooks. + _comment_update_node_statistics($comment->nid); + + field_attach_update('comment', $comment); + // Allow modules to respond to the updating of a comment. + module_invoke_all('comment_update', $comment); + module_invoke_all('entity_update', $comment, 'comment'); + } + else { + // Add the comment to database. This next section builds the thread field. + // Also see the documentation for comment_view(). + if (!empty($comment->thread)) { + // Allow calling code to set thread itself. + $thread = $comment->thread; + } + elseif ($comment->pid == 0) { + // This is a comment with no parent comment (depth 0): we start + // by retrieving the maximum thread level. + $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField(); + // Strip the "/" from the end of the thread. + $max = rtrim($max, '/'); + // We need to get the value at the correct depth. + $parts = explode('.', $max); + $firstsegment = $parts[0]; + // Finally, build the thread field for this new comment. + $thread = int2vancode(vancode2int($firstsegment) + 1) . '/'; + } + else { + // This is a comment with a parent comment, so increase the part of the + // thread value at the proper depth. + + // Get the parent comment: + $parent = comment_load($comment->pid); + // Strip the "/" from the end of the parent thread. + $parent->thread = (string) rtrim((string) $parent->thread, '/'); + // Get the max value in *this* thread. + $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array( + ':thread' => $parent->thread . '.%', + ':nid' => $comment->nid, + ))->fetchField(); + + if ($max == '') { + // First child of this parent. + $thread = $parent->thread . '.' . int2vancode(0) . '/'; + } + else { + // Strip the "/" at the end of the thread. + $max = rtrim($max, '/'); + // Get the value at the correct depth. + $parts = explode('.', $max); + $parent_depth = count(explode('.', $parent->thread)); + $last = $parts[$parent_depth]; + // Finally, build the thread field for this new comment. + $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/'; + } + } + + if (empty($comment->created)) { + $comment->created = REQUEST_TIME; + } + + if (empty($comment->changed)) { + $comment->changed = $comment->created; + } + + if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well. + $comment->name = $user->name; + } + + // Ensure the parent id (pid) has a value set. + if (empty($comment->pid)) { + $comment->pid = 0; + } + + // Add the values which aren't passed into the function. + $comment->thread = $thread; + $comment->hostname = ip_address(); + + drupal_write_record('comment', $comment); + + // Ignore slave server temporarily to give time for the + // created comment to be propagated to the slave. + db_ignore_slave(); + + // Update the {node_comment_statistics} table prior to executing hooks. + _comment_update_node_statistics($comment->nid); + + field_attach_insert('comment', $comment); + + // Tell the other modules a new comment has been submitted. + module_invoke_all('comment_insert', $comment); + module_invoke_all('entity_insert', $comment, 'comment'); + } + if ($comment->status == COMMENT_PUBLISHED) { + module_invoke_all('comment_publish', $comment); + } + unset($comment->original); + } + catch (Exception $e) { + $transaction->rollback('comment'); + watchdog_exception('comment', $e); + throw $e; + } + +} + +/** + * Delete a comment and all its replies. + * + * @param $cid + * The comment to delete. + */ +function comment_delete($cid) { + comment_delete_multiple(array($cid)); +} + +/** + * Delete comments and all their replies. + * + * @param $cids + * The comment to delete. + */ +function comment_delete_multiple($cids) { + $comments = comment_load_multiple($cids); + if ($comments) { + $transaction = db_transaction(); + try { + // Delete the comments. + db_delete('comment') + ->condition('cid', array_keys($comments), 'IN') + ->execute(); + foreach ($comments as $comment) { + field_attach_delete('comment', $comment); + module_invoke_all('comment_delete', $comment); + module_invoke_all('entity_delete', $comment, 'comment'); + + // Delete the comment's replies. + $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol(); + comment_delete_multiple($child_cids); + _comment_update_node_statistics($comment->nid); + } + } + catch (Exception $e) { + $transaction->rollback(); + watchdog_exception('comment', $e); + throw $e; + } + } +} + +/** + * Load comments from the database. + * + * @param $cids + * An array of comment IDs. + * @param $conditions + * (deprecated) An associative array of conditions on the {comments} + * table, where the keys are the database fields and the values are the + * values those fields must have. Instead, it is preferable to use + * EntityFieldQuery to retrieve a list of entity IDs loadable by + * this function. + * @param $reset + * Whether to reset the internal static entity cache. Note that the static + * cache is disabled in comment_entity_info() by default. + * + * @return + * An array of comment objects, indexed by comment ID. + * + * @see entity_load() + * @see EntityFieldQuery + * + * @todo Remove $conditions in Drupal 8. + */ +function comment_load_multiple($cids = array(), $conditions = array(), $reset = FALSE) { + return entity_load('comment', $cids, $conditions, $reset); +} + +/** + * Load the entire comment by cid. + * + * @param $cid + * The identifying comment id. + * @param $reset + * Whether to reset the internal static entity cache. Note that the static + * cache is disabled in comment_entity_info() by default. + * + * @return + * The comment object. + */ +function comment_load($cid, $reset = FALSE) { + $comment = comment_load_multiple(array($cid), array(), $reset); + return $comment ? $comment[$cid] : FALSE; +} + +/** + * Controller class for comments. + * + * This extends the DrupalDefaultEntityController class, adding required + * special handling for comment objects. + */ +class CommentController extends DrupalDefaultEntityController { + + protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { + $query = parent::buildQuery($ids, $conditions, $revision_id); + // Specify additional fields from the user and node tables. + $query->innerJoin('node', 'n', 'base.nid = n.nid'); + $query->addField('n', 'type', 'node_type'); + $query->innerJoin('users', 'u', 'base.uid = u.uid'); + $query->addField('u', 'name', 'registered_name'); + $query->fields('u', array('uid', 'signature', 'signature_format', 'picture')); + return $query; + } + + protected function attachLoad(&$comments, $revision_id = FALSE) { + // Setup standard comment properties. + foreach ($comments as $key => $comment) { + $comment->name = $comment->uid ? $comment->registered_name : $comment->name; + $comment->new = node_mark($comment->nid, $comment->changed); + $comment->node_type = 'comment_node_' . $comment->node_type; + $comments[$key] = $comment; + } + parent::attachLoad($comments, $revision_id); + } +} + +/** + * Get number of new comments for current user and specified node. + * + * @param $nid + * Node-id to count comments for. + * @param $timestamp + * Time to count from (defaults to time of last user access + * to node). + * @return The result or FALSE on error. + */ +function comment_num_new($nid, $timestamp = 0) { + global $user; + + if ($user->uid) { + // Retrieve the timestamp at which the current user last viewed this node. + if (!$timestamp) { + $timestamp = node_last_viewed($nid); + } + $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT); + + // Use the timestamp to retrieve the number of new comments. + return db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND created > :timestamp AND status = :status', array( + ':nid' => $nid, + ':timestamp' => $timestamp, + ':status' => COMMENT_PUBLISHED, + ))->fetchField(); + } + else { + return FALSE; + } + +} + +/** + * Get the display ordinal for a comment, starting from 0. + * + * Count the number of comments which appear before the comment we want to + * display, taking into account display settings and threading. + * + * @param $cid + * The comment ID. + * @param $node_type + * The node type of the comment's parent. + * @return + * The display ordinal for the comment. + * @see comment_get_display_page() + */ +function comment_get_display_ordinal($cid, $node_type) { + // Count how many comments (c1) are before $cid (c2) in display order. This is + // the 0-based display ordinal. + $query = db_select('comment', 'c1'); + $query->innerJoin('comment', 'c2', 'c2.nid = c1.nid'); + $query->addExpression('COUNT(*)', 'count'); + $query->condition('c2.cid', $cid); + if (!user_access('administer comments')) { + $query->condition('c1.status', COMMENT_PUBLISHED); + } + $mode = variable_get('comment_default_mode_' . $node_type, COMMENT_MODE_THREADED); + + if ($mode == COMMENT_MODE_FLAT) { + // For flat comments, cid is used for ordering comments due to + // unpredicatable behavior with timestamp, so we make the same assumption + // here. + $query->condition('c1.cid', $cid, '<'); + } + else { + // For threaded comments, the c.thread column is used for ordering. We can + // use the vancode for comparison, but must remove the trailing slash. + // See comment_view_multiple(). + $query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) -1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) -1))'); + } + + return $query->execute()->fetchField(); +} + +/** + * Return the page number for a comment. + * + * Finds the correct page number for a comment taking into account display + * and paging settings. + * + * @param $cid + * The comment ID. + * @param $node_type + * The node type the comment is attached to. + * @return + * The page number. + */ +function comment_get_display_page($cid, $node_type) { + $ordinal = comment_get_display_ordinal($cid, $node_type); + $comments_per_page = variable_get('comment_default_per_page_' . $node_type, 50); + return floor($ordinal / $comments_per_page); +} + +/** + * Page callback for comment editing. + */ +function comment_edit_page($comment) { + drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH); + $node = node_load($comment->nid); + return drupal_get_form("comment_node_{$node->type}_form", $comment); +} + +/** + * Implements hook_forms(). + */ +function comment_forms() { + $forms = array(); + foreach (node_type_get_types() as $type) { + $forms["comment_node_{$type->type}_form"]['callback'] = 'comment_form'; + } + return $forms; +} + +/** + * Generate the basic commenting form, for appending to a node or display on a separate page. + * + * @see comment_form_validate() + * @see comment_form_submit() + * + * @ingroup forms + */ +function comment_form($form, &$form_state, $comment) { + global $user; + + // During initial form build, add the comment entity to the form state for + // use during form building and processing. During a rebuild, use what is in + // the form state. + if (!isset($form_state['comment'])) { + $defaults = array( + 'name' => '', + 'mail' => '', + 'homepage' => '', + 'subject' => '', + 'comment' => '', + 'cid' => NULL, + 'pid' => NULL, + 'language' => LANGUAGE_NONE, + 'uid' => 0, + ); + foreach ($defaults as $key => $value) { + if (!isset($comment->$key)) { + $comment->$key = $value; + } + } + $form_state['comment'] = $comment; + } + else { + $comment = $form_state['comment']; + } + + $node = node_load($comment->nid); + $form['#node'] = $node; + + // Use #comment-form as unique jump target, regardless of node type. + $form['#id'] = drupal_html_id('comment_form'); + $form['#attributes']['class'][] = 'comment-form'; + $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form'); + + $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT); + $is_admin = (!empty($comment->cid) && user_access('administer comments')); + + if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) { + $form['#attached']['library'][] = array('system', 'jquery.cookie'); + $form['#attributes']['class'][] = 'user-info-from-cookie'; + } + + // If not replying to a comment, use our dedicated page callback for new + // comments on nodes. + if (empty($comment->cid) && empty($comment->pid)) { + $form['#action'] = url('comment/reply/' . $comment->nid); + } + + if (isset($form_state['comment_preview'])) { + $form += $form_state['comment_preview']; + } + + // Display author information in a fieldset for comment moderators. + if ($is_admin) { + $form['author'] = array( + '#type' => 'fieldset', + '#title' => t('Administration'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => -2, + ); + } + else { + // Sets the author form elements above the subject. + $form['author'] = array( + '#weight' => -2, + ); + } + + // Prepare default values for form elements. + if ($is_admin) { + $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name); + $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED); + $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O')); + } + else { + if ($user->uid) { + $author = $user->name; + } + else { + $author = ($comment->name ? $comment->name : ''); + } + $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED); + $date = ''; + } + + // Add the author name field depending on the current user. + if ($is_admin) { + $form['author']['name'] = array( + '#type' => 'textfield', + '#title' => t('Authored by'), + '#default_value' => $author, + '#maxlength' => 60, + '#size' => 30, + '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))), + '#autocomplete_path' => 'user/autocomplete', + ); + } + elseif ($user->uid) { + $form['author']['_author'] = array( + '#type' => 'item', + '#title' => t('Your name'), + '#markup' => theme('username', array('account' => $user)), + ); + $form['author']['name'] = array( + '#type' => 'value', + '#value' => $author, + ); + } + else { + $form['author']['name'] = array( + '#type' => 'textfield', + '#title' => t('Your name'), + '#default_value' => $author, + '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT), + '#maxlength' => 60, + '#size' => 30, + ); + } + + // Add author e-mail and homepage fields depending on the current user. + $form['author']['mail'] = array( + '#type' => 'textfield', + '#title' => t('E-mail'), + '#default_value' => $comment->mail, + '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT), + '#maxlength' => 64, + '#size' => 30, + '#description' => t('The content of this field is kept private and will not be shown publicly.'), + '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), + ); + $form['author']['homepage'] = array( + '#type' => 'textfield', + '#title' => t('Homepage'), + '#default_value' => $comment->homepage, + '#maxlength' => 255, + '#size' => 30, + '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), + ); + + // Add administrative comment publishing options. + $form['author']['date'] = array( + '#type' => 'textfield', + '#title' => t('Authored on'), + '#default_value' => $date, + '#maxlength' => 25, + '#size' => 20, + '#access' => $is_admin, + ); + $form['author']['status'] = array( + '#type' => 'radios', + '#title' => t('Status'), + '#default_value' => $status, + '#options' => array( + COMMENT_PUBLISHED => t('Published'), + COMMENT_NOT_PUBLISHED => t('Not published'), + ), + '#access' => $is_admin, + ); + + $form['subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#maxlength' => 64, + '#default_value' => $comment->subject, + '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1, + '#weight' => -1, + ); + + // Used for conditional validation of author fields. + $form['is_anonymous'] = array( + '#type' => 'value', + '#value' => ($comment->cid ? !$comment->uid : !$user->uid), + ); + + // Add internal comment properties. + foreach (array('cid', 'pid', 'nid', 'language', 'uid') as $key) { + $form[$key] = array('#type' => 'value', '#value' => $comment->$key); + } + $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type); + + // Only show the save button if comment previews are optional or if we are + // already previewing the submission. + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#access' => ($comment->cid && user_access('administer comments')) || variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || isset($form_state['comment_preview']), + '#weight' => 19, + ); + $form['actions']['preview'] = array( + '#type' => 'submit', + '#value' => t('Preview'), + '#access' => (variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED), + '#weight' => 20, + '#submit' => array('comment_form_build_preview'), + ); + + // Attach fields. + $comment->node_type = 'comment_node_' . $node->type; + $langcode = entity_language('comment', $comment); + field_attach_form('comment', $comment, $form, $form_state, $langcode); + + return $form; +} + +/** + * Build a preview from submitted form values. + */ +function comment_form_build_preview($form, &$form_state) { + $comment = comment_form_submit_build_comment($form, $form_state); + $form_state['comment_preview'] = comment_preview($comment); + $form_state['rebuild'] = TRUE; +} + +/** + * Generate a comment preview. + */ +function comment_preview($comment) { + global $user; + + drupal_set_title(t('Preview comment'), PASS_THROUGH); + + $node = node_load($comment->nid); + + if (!form_get_errors()) { + $comment_body = field_get_items('comment', $comment, 'comment_body'); + $comment->format = $comment_body[0]['format']; + // Attach the user and time information. + if (!empty($comment->name)) { + $account = user_load_by_name($comment->name); + } + elseif ($user->uid && empty($comment->is_anonymous)) { + $account = $user; + } + + if (!empty($account->uid)) { + $comment->uid = $account->uid; + $comment->name = check_plain($account->name); + $comment->signature = $account->signature; + $comment->signature_format = $account->signature_format; + $comment->picture = $account->picture; + } + elseif (empty($comment->name)) { + $comment->name = variable_get('anonymous', t('Anonymous')); + } + + $comment->created = !empty($comment->created) ? $comment->created : REQUEST_TIME; + $comment->changed = REQUEST_TIME; + $comment->in_preview = TRUE; + $comment_build = comment_view($comment, $node); + $comment_build['#weight'] = -100; + + $form['comment_preview'] = $comment_build; + } + + if ($comment->pid) { + $build = array(); + if ($comments = comment_load_multiple(array($comment->pid), array('status' => COMMENT_PUBLISHED))) { + $parent_comment = $comments[$comment->pid]; + $build = comment_view($parent_comment, $node); + } + } + else { + $build = node_view($node); + } + + $form['comment_output_below'] = $build; + $form['comment_output_below']['#weight'] = 100; + + return $form; +} + +/** + * Validate comment form submissions. + */ +function comment_form_validate($form, &$form_state) { + global $user; + + entity_form_field_validate('comment', $form, $form_state); + + if (!empty($form_state['values']['cid'])) { + // Verify the name in case it is being changed from being anonymous. + $account = user_load_by_name($form_state['values']['name']); + $form_state['values']['uid'] = $account ? $account->uid : 0; + + if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) { + form_set_error('date', t('You have to specify a valid date.')); + } + if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) { + form_set_error('name', t('You have to specify a valid author.')); + } + } + elseif ($form_state['values']['is_anonymous']) { + // Validate anonymous comment author fields (if given). If the (original) + // author of this comment was an anonymous user, verify that no registered + // user with this name exists. + if ($form_state['values']['name']) { + $query = db_select('users', 'u'); + $query->addField('u', 'uid', 'uid'); + $taken = $query + ->condition('name', db_like($form_state['values']['name']), 'LIKE') + ->countQuery() + ->execute() + ->fetchField(); + if ($taken) { + form_set_error('name', t('The name you used belongs to a registered user.')); + } + } + } + if ($form_state['values']['mail'] && !valid_email_address($form_state['values']['mail'])) { + form_set_error('mail', t('The e-mail address you specified is not valid.')); + } + if ($form_state['values']['homepage'] && !valid_url($form_state['values']['homepage'], TRUE)) { + form_set_error('homepage', t('The URL of your homepage is not valid. Remember that it must be fully qualified, i.e. of the form http://example.com/directory.')); + } +} + +/** + * Prepare a comment for submission. + */ +function comment_submit($comment) { + // @todo Legacy support. Remove in Drupal 8. + if (is_array($comment)) { + $comment += array('subject' => ''); + $comment = (object) $comment; + } + + if (empty($comment->date)) { + $comment->date = 'now'; + } + $comment->created = strtotime($comment->date); + $comment->changed = REQUEST_TIME; + + // If the comment was posted by a registered user, assign the author's ID. + // @todo Too fragile. Should be prepared and stored in comment_form() already. + if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) { + $comment->uid = $account->uid; + } + // If the comment was posted by an anonymous user and no author name was + // required, use "Anonymous" by default. + if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) { + $comment->name = variable_get('anonymous', t('Anonymous')); + } + + // Validate the comment's subject. If not specified, extract from comment body. + if (trim($comment->subject) == '') { + // The body may be in any format, so: + // 1) Filter it into HTML + // 2) Strip out all HTML tags + // 3) Convert entities back to plain-text. + $field = field_info_field('comment_body'); + $langcode = field_is_translatable('comment', $field) ? entity_language('comment', $comment) : LANGUAGE_NONE; + $comment_body = $comment->comment_body[$langcode][0]; + if (isset($comment_body['format'])) { + $comment_text = check_markup($comment_body['value'], $comment_body['format']); + } + else { + $comment_text = check_plain($comment_body['value']); + } + $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE); + // Edge cases where the comment body is populated only by HTML tags will + // require a default subject. + if ($comment->subject == '') { + $comment->subject = t('(No subject)'); + } + } + return $comment; +} + +/** + * Updates the form state's comment entity by processing this submission's values. + * + * This is the default builder function for the comment form. It is called + * during the "Save" and "Preview" submit handlers to retrieve the entity to + * save or preview. This function can also be called by a "Next" button of a + * wizard to update the form state's entity with the current step's values + * before proceeding to the next step. + * + * @see comment_form() + */ +function comment_form_submit_build_comment($form, &$form_state) { + $comment = $form_state['comment']; + entity_form_submit_build_entity('comment', $comment, $form, $form_state); + comment_submit($comment); + return $comment; +} + +/** + * Process comment form submissions; prepare the comment, store it, and set a redirection target. + */ +function comment_form_submit($form, &$form_state) { + $node = node_load($form_state['values']['nid']); + $comment = comment_form_submit_build_comment($form, $form_state); + if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) { + // Save the anonymous user information to a cookie for reuse. + if (user_is_anonymous()) { + user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage')))); + } + + comment_save($comment); + $form_state['values']['cid'] = $comment->cid; + + // Add an entry to the watchdog log. + watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid))); + + // Explain the approval queue if necessary. + if ($comment->status == COMMENT_NOT_PUBLISHED) { + if (!user_access('administer comments')) { + drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.')); + } + } + else { + drupal_set_message(t('Your comment has been posted.')); + } + $query = array(); + // Find the current display page for this comment. + $page = comment_get_display_page($comment->cid, $node->type); + if ($page > 0) { + $query['page'] = $page; + } + // Redirect to the newly posted comment. + $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid)); + } + else { + watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING); + drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error'); + // Redirect the user to the node they are commenting on. + $redirect = 'node/' . $node->nid; + } + $form_state['redirect'] = $redirect; + // Clear the block and page caches so that anonymous users see the comment + // they have posted. + cache_clear_all(); +} + +/** + * Process variables for comment.tpl.php. + * + * @see comment.tpl.php + */ +function template_preprocess_comment(&$variables) { + $comment = $variables['elements']['#comment']; + $node = $variables['elements']['#node']; + $variables['comment'] = $comment; + $variables['node'] = $node; + $variables['author'] = theme('username', array('account' => $comment)); + + $variables['created'] = format_date($comment->created); + + // Avoid calling format_date() twice on the same timestamp. + if ($comment->changed == $comment->created) { + $variables['changed'] = $variables['created']; + } + else { + $variables['changed'] = format_date($comment->changed); + } + + $variables['new'] = !empty($comment->new) ? t('new') : ''; + $variables['picture'] = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : ''; + $variables['signature'] = $comment->signature; + + $uri = entity_uri('comment', $comment); + $uri['options'] += array('attributes' => array('class' => array('permalink'), 'rel' => 'bookmark')); + + $variables['title'] = l($comment->subject, $uri['path'], $uri['options']); + $variables['permalink'] = l(t('Permalink'), $uri['path'], $uri['options']); + $variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['author'], '!datetime' => $variables['created'])); + + // Preprocess fields. + field_attach_preprocess('comment', $comment, $variables['elements'], $variables); + + // Helpful $content variable for templates. + foreach (element_children($variables['elements']) as $key) { + $variables['content'][$key] = $variables['elements'][$key]; + } + + // Set status to a string representation of comment->status. + if (isset($comment->in_preview)) { + $variables['status'] = 'comment-preview'; + } + else { + $variables['status'] = ($comment->status == COMMENT_NOT_PUBLISHED) ? 'comment-unpublished' : 'comment-published'; + } + + // Gather comment classes. + // 'comment-published' class is not needed, it is either 'comment-preview' or + // 'comment-unpublished'. + if ($variables['status'] != 'comment-published') { + $variables['classes_array'][] = $variables['status']; + } + if ($variables['new']) { + $variables['classes_array'][] = 'comment-new'; + } + if (!$comment->uid) { + $variables['classes_array'][] = 'comment-by-anonymous'; + } + else { + if ($comment->uid == $variables['node']->uid) { + $variables['classes_array'][] = 'comment-by-node-author'; + } + if ($comment->uid == $variables['user']->uid) { + $variables['classes_array'][] = 'comment-by-viewer'; + } + } +} + +/** + * Returns HTML for a "you can't post comments" notice. + * + * @param $variables + * An associative array containing: + * - node: The comment node. + * + * @ingroup themeable + */ +function theme_comment_post_forbidden($variables) { + $node = $variables['node']; + global $user; + + // Since this is expensive to compute, we cache it so that a page with many + // comments only has to query the database once for all the links. + $authenticated_post_comments = &drupal_static(__FUNCTION__, NULL); + + if (!$user->uid) { + if (!isset($authenticated_post_comments)) { + // We only output a link if we are certain that users will get permission + // to post comments by logging in. + $comment_roles = user_roles(TRUE, 'post comments'); + $authenticated_post_comments = isset($comment_roles[DRUPAL_AUTHENTICATED_RID]); + } + + if ($authenticated_post_comments) { + // We cannot use drupal_get_destination() because these links + // sometimes appear on /node and taxonomy listing pages. + if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) { + $destination = array('destination' => "comment/reply/$node->nid#comment-form"); + } + else { + $destination = array('destination' => "node/$node->nid#comment-form"); + } + + if (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)) { + // Users can register themselves. + return t('Log in or register to post comments', array('@login' => url('user/login', array('query' => $destination)), '@register' => url('user/register', array('query' => $destination)))); + } + else { + // Only admins can add new users, no public registration. + return t('Log in to post comments', array('@login' => url('user/login', array('query' => $destination)))); + } + } + } +} + +/** + * Process variables for comment-wrapper.tpl.php. + * + * @see comment-wrapper.tpl.php + */ +function template_preprocess_comment_wrapper(&$variables) { + // Provide contextual information. + $variables['node'] = $variables['content']['#node']; + $variables['display_mode'] = variable_get('comment_default_mode_' . $variables['node']->type, COMMENT_MODE_THREADED); + // The comment form is optional and may not exist. + $variables['content'] += array('comment_form' => array()); +} + +/** + * Return an array of viewing modes for comment listings. + * + * We can't use a global variable array because the locale system + * is not initialized yet when the comment module is loaded. + */ +function _comment_get_modes() { + return array( + COMMENT_MODE_FLAT => t('Flat list'), + COMMENT_MODE_THREADED => t('Threaded list') + ); +} + +/** + * Return an array of "comments per page" settings from which the user + * can choose. + */ +function _comment_per_page() { + return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300)); +} + +/** + * Updates the comment statistics for a given node. This should be called any + * time a comment is added, deleted, or updated. + * + * The following fields are contained in the node_comment_statistics table. + * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node. + * - last_comment_name: the name of the anonymous poster for the last comment + * - last_comment_uid: the uid of the poster for the last comment for this node or the node authors uid if no comments exists for the node. + * - comment_count: the total number of approved/published comments on this node. + */ +function _comment_update_node_statistics($nid) { + // Allow bulk updates and inserts to temporarily disable the + // maintenance of the {node_comment_statistics} table. + if (!variable_get('comment_maintain_node_statistics', TRUE)) { + return; + } + + $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array( + ':nid' => $nid, + ':status' => COMMENT_PUBLISHED, + ))->fetchField(); + + if ($count > 0) { + // Comments exist. + $last_reply = db_query_range('SELECT cid, name, changed, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array( + ':nid' => $nid, + ':status' => COMMENT_PUBLISHED, + ))->fetchObject(); + db_update('node_comment_statistics') + ->fields(array( + 'cid' => $last_reply->cid, + 'comment_count' => $count, + 'last_comment_timestamp' => $last_reply->changed, + 'last_comment_name' => $last_reply->uid ? '' : $last_reply->name, + 'last_comment_uid' => $last_reply->uid, + )) + ->condition('nid', $nid) + ->execute(); + } + else { + // Comments do not exist. + $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject(); + db_update('node_comment_statistics') + ->fields(array( + 'cid' => 0, + 'comment_count' => 0, + 'last_comment_timestamp' => $node->created, + 'last_comment_name' => '', + 'last_comment_uid' => $node->uid, + )) + ->condition('nid', $nid) + ->execute(); + } +} + +/** + * Generate vancode. + * + * Consists of a leading character indicating length, followed by N digits + * with a numerical value in base 36. Vancodes can be sorted as strings + * without messing up numerical order. + * + * It goes: + * 00, 01, 02, ..., 0y, 0z, + * 110, 111, ... , 1zy, 1zz, + * 2100, 2101, ..., 2zzy, 2zzz, + * 31000, 31001, ... + */ +function int2vancode($i = 0) { + $num = base_convert((int) $i, 10, 36); + $length = strlen($num); + + return chr($length + ord('0') - 1) . $num; +} + +/** + * Decode vancode back to an integer. + */ +function vancode2int($c = '00') { + return base_convert(substr($c, 1), 36, 10); +} + +/** + * Implements hook_action_info(). + */ +function comment_action_info() { + return array( + 'comment_publish_action' => array( + 'label' => t('Publish comment'), + 'type' => 'comment', + 'configurable' => FALSE, + 'behavior' => array('changes_property'), + 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'), + ), + 'comment_unpublish_action' => array( + 'label' => t('Unpublish comment'), + 'type' => 'comment', + 'configurable' => FALSE, + 'behavior' => array('changes_property'), + 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'), + ), + 'comment_unpublish_by_keyword_action' => array( + 'label' => t('Unpublish comment containing keyword(s)'), + 'type' => 'comment', + 'configurable' => TRUE, + 'behavior' => array('changes_property'), + 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'), + ), + 'comment_save_action' => array( + 'label' => t('Save comment'), + 'type' => 'comment', + 'configurable' => FALSE, + 'triggers' => array('comment_insert', 'comment_update'), + ), + ); +} + +/** + * Publishes a comment. + * + * @param $comment + * An optional comment object. + * @param array $context + * Array with components: + * - 'cid': Comment ID. Required if $comment is not given. + * + * @ingroup actions + */ +function comment_publish_action($comment, $context = array()) { + if (isset($comment->subject)) { + $subject = $comment->subject; + $comment->status = COMMENT_PUBLISHED; + } + else { + $cid = $context['cid']; + $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField(); + db_update('comment') + ->fields(array('status' => COMMENT_PUBLISHED)) + ->condition('cid', $cid) + ->execute(); + } + watchdog('action', 'Published comment %subject.', array('%subject' => $subject)); +} + +/** + * Unpublishes a comment. + * + * @param $comment + * An optional comment object. + * @param array $context + * Array with components: + * - 'cid': Comment ID. Required if $comment is not given. + * + * @ingroup actions + */ +function comment_unpublish_action($comment, $context = array()) { + if (isset($comment->subject)) { + $subject = $comment->subject; + $comment->status = COMMENT_NOT_PUBLISHED; + } + else { + $cid = $context['cid']; + $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField(); + db_update('comment') + ->fields(array('status' => COMMENT_NOT_PUBLISHED)) + ->condition('cid', $cid) + ->execute(); + } + watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject)); +} + +/** + * Unpublishes a comment if it contains certain keywords. + * + * @param object $comment + * Comment object to modify. + * @param array $context + * Array with components: + * - 'keywords': Keywords to look for. If the comment contains at least one + * of the keywords, it is unpublished. + * + * @ingroup actions + * @see comment_unpublish_by_keyword_action_form() + * @see comment_unpublish_by_keyword_action_submit() + */ +function comment_unpublish_by_keyword_action($comment, $context) { + $node = node_load($comment->nid); + $build = comment_view($comment, $node); + $text = drupal_render($build); + foreach ($context['keywords'] as $keyword) { + if (strpos($text, $keyword) !== FALSE) { + $comment->status = COMMENT_NOT_PUBLISHED; + comment_save($comment); + watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject)); + break; + } + } +} + +/** + * Form builder; Prepare a form for blacklisted keywords. + * + * @ingroup forms + * @see comment_unpublish_by_keyword_action() + * @see comment_unpublish_by_keyword_action_submit() + */ +function comment_unpublish_by_keyword_action_form($context) { + $form['keywords'] = array( + '#title' => t('Keywords'), + '#type' => 'textarea', + '#description' => t('The comment will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'), + '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '', + ); + + return $form; +} + +/** + * Process comment_unpublish_by_keyword_action_form form submissions. + * + * @see comment_unpublish_by_keyword_action() + */ +function comment_unpublish_by_keyword_action_submit($form, $form_state) { + return array('keywords' => drupal_explode_tags($form_state['values']['keywords'])); +} + +/** + * Saves a comment. + * + * @ingroup actions + */ +function comment_save_action($comment) { + comment_save($comment); + cache_clear_all(); + watchdog('action', 'Saved comment %title', array('%title' => $comment->subject)); +} + +/** + * Implements hook_ranking(). + */ +function comment_ranking() { + return array( + 'comments' => array( + 'title' => t('Number of comments'), + 'join' => array( + 'type' => 'LEFT', + 'table' => 'node_comment_statistics', + 'alias' => 'node_comment_statistics', + 'on' => 'node_comment_statistics.nid = i.sid', + ), + // Inverse law that maps the highest reply count on the site to 1 and 0 to 0. + 'score' => '2.0 - 2.0 / (1.0 + node_comment_statistics.comment_count * CAST(:scale AS DECIMAL))', + 'arguments' => array(':scale' => variable_get('node_cron_comments_scale', 0)), + ), + ); +} + +/** + * Implements hook_rdf_mapping(). + */ +function comment_rdf_mapping() { + return array( + array( + 'type' => 'comment', + 'bundle' => RDF_DEFAULT_BUNDLE, + 'mapping' => array( + 'rdftype' => array('sioc:Post', 'sioct:Comment'), + 'title' => array( + 'predicates' => array('dc:title'), + ), + 'created' => array( + 'predicates' => array('dc:date', 'dc:created'), + 'datatype' => 'xsd:dateTime', + 'callback' => 'date_iso8601', + ), + 'changed' => array( + 'predicates' => array('dc:modified'), + 'datatype' => 'xsd:dateTime', + 'callback' => 'date_iso8601', + ), + 'comment_body' => array( + 'predicates' => array('content:encoded'), + ), + 'pid' => array( + 'predicates' => array('sioc:reply_of'), + 'type' => 'rel', + ), + 'uid' => array( + 'predicates' => array('sioc:has_creator'), + 'type' => 'rel', + ), + 'name' => array( + 'predicates' => array('foaf:name'), + ), + ), + ), + ); +} + +/** + * Implements hook_file_download_access(). + */ +function comment_file_download_access($field, $entity_type, $entity) { + if ($entity_type == 'comment') { + if (user_access('access comments') && $entity->status == COMMENT_PUBLISHED || user_access('administer comments')) { + $node = node_load($entity->nid); + return node_access('view', $node); + } + return FALSE; + } +} diff --git a/comment/comment.pages.inc b/comment/comment.pages.inc new file mode 100644 index 0000000..482e3f2 --- /dev/null +++ b/comment/comment.pages.inc @@ -0,0 +1,123 @@ +title, 'node/' . $node->nid))); + $op = isset($_POST['op']) ? $_POST['op'] : ''; + $build = array(); + + // The user is previewing a comment prior to submitting it. + if ($op == t('Preview')) { + if (user_access('post comments')) { + $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) array('pid' => $pid, 'nid' => $node->nid)); + } + else { + drupal_set_message(t('You are not authorized to post comments.'), 'error'); + drupal_goto("node/$node->nid"); + } + } + else { + // $pid indicates that this is a reply to a comment. + if ($pid) { + if (user_access('access comments')) { + // Load the comment whose cid = $pid + $comment = db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data FROM {comment} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = :cid AND c.status = :status', array( + ':cid' => $pid, + ':status' => COMMENT_PUBLISHED, + ))->fetchObject(); + if ($comment) { + // If that comment exists, make sure that the current comment and the + // parent comment both belong to the same parent node. + if ($comment->nid != $node->nid) { + // Attempting to reply to a comment not belonging to the current nid. + drupal_set_message(t('The comment you are replying to does not exist.'), 'error'); + drupal_goto("node/$node->nid"); + } + // Display the parent comment + $comment->node_type = 'comment_node_' . $node->type; + field_attach_load('comment', array($comment->cid => $comment)); + $comment->name = $comment->uid ? $comment->registered_name : $comment->name; + $build['comment_parent'] = comment_view($comment, $node); + } + else { + drupal_set_message(t('The comment you are replying to does not exist.'), 'error'); + drupal_goto("node/$node->nid"); + } + } + else { + drupal_set_message(t('You are not authorized to view comments.'), 'error'); + drupal_goto("node/$node->nid"); + } + } + // This is the case where the comment is in response to a node. Display the node. + elseif (user_access('access content')) { + $build['comment_node'] = node_view($node); + } + + // Should we show the reply box? + if ($node->comment != COMMENT_NODE_OPEN) { + drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error'); + drupal_goto("node/$node->nid"); + } + elseif (user_access('post comments')) { + $edit = array('nid' => $node->nid, 'pid' => $pid); + $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) $edit); + } + else { + drupal_set_message(t('You are not authorized to post comments.'), 'error'); + drupal_goto("node/$node->nid"); + } + } + + return $build; +} + +/** + * Menu callback; publish specified comment. + * + * @param $cid + * A comment identifier. + */ +function comment_approve($cid) { + if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], "comment/$cid/approve")) { + return MENU_ACCESS_DENIED; + } + if ($comment = comment_load($cid)) { + $comment->status = COMMENT_PUBLISHED; + comment_save($comment); + + drupal_set_message(t('Comment approved.')); + drupal_goto('node/' . $comment->nid); + } + return MENU_NOT_FOUND; +} diff --git a/comment/comment.test b/comment/comment.test new file mode 100644 index 0000000..534b2c1 --- /dev/null +++ b/comment/comment.test @@ -0,0 +1,2260 @@ +admin_user = $this->drupalCreateUser(array('administer content types', 'administer comments', 'administer blocks', 'administer actions', 'administer fields')); + $this->web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'create article content', 'edit own comments')); + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->uid)); + } + + /** + * Post comment. + * + * @param $node + * Node to post comment on. + * @param $comment + * Comment body. + * @param $subject + * Comment subject. + * @param $contact + * Set to NULL for no contact info, TRUE to ignore success checking, and + * array of values to set contact info. + */ + function postComment($node, $comment, $subject = '', $contact = NULL) { + $langcode = LANGUAGE_NONE; + $edit = array(); + $edit['comment_body[' . $langcode . '][0][value]'] = $comment; + + $preview_mode = variable_get('comment_preview_article', DRUPAL_OPTIONAL); + $subject_mode = variable_get('comment_subject_field_article', 1); + + // Must get the page before we test for fields. + if ($node !== NULL) { + $this->drupalGet('comment/reply/' . $node->nid); + } + + if ($subject_mode == TRUE) { + $edit['subject'] = $subject; + } + else { + $this->assertNoFieldByName('subject', '', 'Subject field not found.'); + } + + if ($contact !== NULL && is_array($contact)) { + $edit += $contact; + } + switch ($preview_mode) { + case DRUPAL_REQUIRED: + // Preview required so no save button should be found. + $this->assertNoFieldByName('op', t('Save'), 'Save button not found.'); + $this->drupalPost(NULL, $edit, t('Preview')); + // Don't break here so that we can test post-preview field presence and + // function below. + case DRUPAL_OPTIONAL: + $this->assertFieldByName('op', t('Preview'), 'Preview button found.'); + $this->assertFieldByName('op', t('Save'), 'Save button found.'); + $this->drupalPost(NULL, $edit, t('Save')); + break; + + case DRUPAL_DISABLED: + $this->assertNoFieldByName('op', t('Preview'), 'Preview button not found.'); + $this->assertFieldByName('op', t('Save'), 'Save button found.'); + $this->drupalPost(NULL, $edit, t('Save')); + break; + } + $match = array(); + // Get comment ID + preg_match('/#comment-([0-9]+)/', $this->getURL(), $match); + + // Get comment. + if ($contact !== TRUE) { // If true then attempting to find error message. + if ($subject) { + $this->assertText($subject, 'Comment subject posted.'); + } + $this->assertText($comment, 'Comment body posted.'); + $this->assertTrue((!empty($match) && !empty($match[1])), 'Comment id found.'); + } + + if (isset($match[1])) { + return (object) array('id' => $match[1], 'subject' => $subject, 'comment' => $comment); + } + } + + /** + * Checks current page for specified comment. + * + * @param object $comment Comment object. + * @param boolean $reply The comment is a reply to another comment. + * @return boolean Comment found. + */ + function commentExists($comment, $reply = FALSE) { + if ($comment && is_object($comment)) { + $regex = '/' . ($reply ? '
    (.*?)' : ''); + $regex .= 'subject . '(.*?)'; // Match subject. + $regex .= $comment->comment . '(.*?)'; // Match comment. + $regex .= '/s'; + + return (boolean)preg_match($regex, $this->drupalGetContent()); + } + else { + return FALSE; + } + } + + /** + * Delete comment. + * + * @param object $comment + * Comment to delete. + */ + function deleteComment($comment) { + $this->drupalPost('comment/' . $comment->id . '/delete', array(), t('Delete')); + $this->assertText(t('The comment and all its replies have been deleted.'), 'Comment deleted.'); + } + + /** + * Set comment subject setting. + * + * @param boolean $enabled + * Subject value. + */ + function setCommentSubject($enabled) { + $this->setCommentSettings('comment_subject_field', ($enabled ? '1' : '0'), 'Comment subject ' . ($enabled ? 'enabled' : 'disabled') . '.'); + } + + /** + * Set comment preview setting. + * + * @param int $mode + * Preview value. + */ + function setCommentPreview($mode) { + switch ($mode) { + case DRUPAL_DISABLED: + $mode_text = 'disabled'; + break; + + case DRUPAL_OPTIONAL: + $mode_text = 'optional'; + break; + + case DRUPAL_REQUIRED: + $mode_text = 'required'; + break; + } + $this->setCommentSettings('comment_preview', $mode, format_string('Comment preview @mode_text.', array('@mode_text' => $mode_text))); + } + + /** + * Set comment form location setting. + * + * @param boolean $enabled + * Form value. + */ + function setCommentForm($enabled) { + $this->setCommentSettings('comment_form_location', ($enabled ? COMMENT_FORM_BELOW : COMMENT_FORM_SEPARATE_PAGE), 'Comment controls ' . ($enabled ? 'enabled' : 'disabled') . '.'); + } + + /** + * Set comment anonymous level setting. + * + * @param integer $level + * Anonymous level. + */ + function setCommentAnonymous($level) { + $this->setCommentSettings('comment_anonymous', $level, format_string('Anonymous commenting set to level @level.', array('@level' => $level))); + } + + /** + * Set the default number of comments per page. + * + * @param integer $comments + * Comments per page value. + */ + function setCommentsPerPage($number) { + $this->setCommentSettings('comment_default_per_page', $number, format_string('Number of comments per page set to @number.', array('@number' => $number))); + } + + /** + * Set comment setting for article content type. + * + * @param string $name + * Name of variable. + * @param string $value + * Value of variable. + * @param string $message + * Status message to display. + */ + function setCommentSettings($name, $value, $message) { + variable_set($name . '_article', $value); + // Display status message. + $this->pass($message); + } + + /** + * Check for contact info. + * + * @return boolean Contact info is available. + */ + function commentContactInfoAvailable() { + return preg_match('/(input).*?(name="name").*?(input).*?(name="mail").*?(input).*?(name="homepage")/s', $this->drupalGetContent()); + } + + /** + * Perform the specified operation on the specified comment. + * + * @param object $comment + * Comment to perform operation on. + * @param string $operation + * Operation to perform. + * @param boolean $aproval + * Operation is found on approval page. + */ + function performCommentOperation($comment, $operation, $approval = FALSE) { + $edit = array(); + $edit['operation'] = $operation; + $edit['comments[' . $comment->id . ']'] = TRUE; + $this->drupalPost('admin/content/comment' . ($approval ? '/approval' : ''), $edit, t('Update')); + + if ($operation == 'delete') { + $this->drupalPost(NULL, array(), t('Delete comments')); + $this->assertRaw(format_plural(1, 'Deleted 1 comment.', 'Deleted @count comments.'), format_string('Operation @operation was performed on comment.', array('@operation' => $operation))); + } + else { + $this->assertText(t('The update has been performed.'), format_string('Operation @operation was performed on comment.', array('@operation' => $operation))); + } + } + + /** + * Get the comment ID for an unapproved comment. + * + * @param string $subject + * Comment subject to find. + * @return integer + * Comment id. + */ + function getUnapprovedComment($subject) { + $this->drupalGet('admin/content/comment/approval'); + preg_match('/href="(.*?)#comment-([^"]+)"(.*?)>(' . $subject . ')/', $this->drupalGetContent(), $match); + + return $match[2]; + } +} + +class CommentInterfaceTest extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment interface', + 'description' => 'Test comment user interfaces.', + 'group' => 'Comment', + ); + } + + /** + * Test comment interface. + */ + function testCommentInterface() { + $langcode = LANGUAGE_NONE; + // Set comments to have subject and preview disabled. + $this->drupalLogin($this->admin_user); + $this->setCommentPreview(DRUPAL_DISABLED); + $this->setCommentForm(TRUE); + $this->setCommentSubject(FALSE); + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Comment paging changed.'); + $this->drupalLogout(); + + // Post comment #1 without subject or preview. + $this->drupalLogin($this->web_user); + $comment_text = $this->randomName(); + $comment = $this->postComment($this->node, $comment_text); + $comment_loaded = comment_load($comment->id); + $this->assertTrue($this->commentExists($comment), 'Comment found.'); + + // Set comments to have subject and preview to required. + $this->drupalLogout(); + $this->drupalLogin($this->admin_user); + $this->setCommentSubject(TRUE); + $this->setCommentPreview(DRUPAL_REQUIRED); + $this->drupalLogout(); + + // Create comment #2 that allows subject and requires preview. + $this->drupalLogin($this->web_user); + $subject_text = $this->randomName(); + $comment_text = $this->randomName(); + $comment = $this->postComment($this->node, $comment_text, $subject_text, TRUE); + $comment_loaded = comment_load($comment->id); + $this->assertTrue($this->commentExists($comment), 'Comment found.'); + + // Check comment display. + $this->drupalGet('node/' . $this->node->nid . '/' . $comment->id); + $this->assertText($subject_text, 'Individual comment subject found.'); + $this->assertText($comment_text, 'Individual comment body found.'); + + // Set comments to have subject and preview to optional. + $this->drupalLogout(); + $this->drupalLogin($this->admin_user); + $this->setCommentSubject(TRUE); + $this->setCommentPreview(DRUPAL_OPTIONAL); + + // Test changing the comment author to "Anonymous". + $this->drupalGet('comment/' . $comment->id . '/edit'); + $comment = $this->postComment(NULL, $comment->comment, $comment->subject, array('name' => '')); + $comment_loaded = comment_load($comment->id); + $this->assertTrue(empty($comment_loaded->name) && $comment_loaded->uid == 0, 'Comment author successfully changed to anonymous.'); + + // Test changing the comment author to an unverified user. + $random_name = $this->randomName(); + $this->drupalGet('comment/' . $comment->id . '/edit'); + $comment = $this->postComment(NULL, $comment->comment, $comment->subject, array('name' => $random_name)); + $this->drupalGet('node/' . $this->node->nid); + $this->assertText($random_name . ' (' . t('not verified') . ')', 'Comment author successfully changed to an unverified user.'); + + // Test changing the comment author to a verified user. + $this->drupalGet('comment/' . $comment->id . '/edit'); + $comment = $this->postComment(NULL, $comment->comment, $comment->subject, array('name' => $this->web_user->name)); + $comment_loaded = comment_load($comment->id); + $this->assertTrue($comment_loaded->name == $this->web_user->name && $comment_loaded->uid == $this->web_user->uid, 'Comment author successfully changed to a registered user.'); + + $this->drupalLogout(); + + // Reply to comment #2 creating comment #3 with optional preview and no + // subject though field enabled. + $this->drupalLogin($this->web_user); + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $comment->id); + $this->assertText($subject_text, 'Individual comment-reply subject found.'); + $this->assertText($comment_text, 'Individual comment-reply body found.'); + $reply = $this->postComment(NULL, $this->randomName(), '', TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($reply, TRUE), 'Reply found.'); + $this->assertEqual($comment->id, $reply_loaded->pid, 'Pid of a reply to a comment is set correctly.'); + $this->assertEqual(rtrim($comment_loaded->thread, '/') . '.00/', $reply_loaded->thread, 'Thread of reply grows correctly.'); + + // Second reply to comment #3 creating comment #4. + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $comment->id); + $this->assertText($subject_text, 'Individual comment-reply subject found.'); + $this->assertText($comment_text, 'Individual comment-reply body found.'); + $reply = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($reply, TRUE), 'Second reply found.'); + $this->assertEqual(rtrim($comment_loaded->thread, '/') . '.01/', $reply_loaded->thread, 'Thread of second reply grows correctly.'); + + // Edit reply. + $this->drupalGet('comment/' . $reply->id . '/edit'); + $reply = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + $this->assertTrue($this->commentExists($reply, TRUE), 'Modified reply found.'); + + // Correct link count + $this->drupalGet('node'); + $this->assertRaw('4 comments', 'Link to the 4 comments exist.'); + + // Confirm a new comment is posted to the correct page. + $this->setCommentsPerPage(2); + $comment_new_page = $this->postComment($this->node, $this->randomName(), $this->randomName(), TRUE); + $this->assertTrue($this->commentExists($comment_new_page), 'Page one exists. %s'); + $this->drupalGet('node/' . $this->node->nid, array('query' => array('page' => 1))); + $this->assertTrue($this->commentExists($reply, TRUE), 'Page two exists. %s'); + $this->setCommentsPerPage(50); + + // Attempt to post to node with comments disabled. + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'comment' => COMMENT_NODE_HIDDEN)); + $this->assertTrue($this->node, 'Article node created.'); + $this->drupalGet('comment/reply/' . $this->node->nid); + $this->assertText('This discussion is closed', 'Posting to node with comments disabled'); + $this->assertNoField('edit-comment', 'Comment body field found.'); + + // Attempt to post to node with read-only comments. + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'comment' => COMMENT_NODE_CLOSED)); + $this->assertTrue($this->node, 'Article node created.'); + $this->drupalGet('comment/reply/' . $this->node->nid); + $this->assertText('This discussion is closed', 'Posting to node with comments read-only'); + $this->assertNoField('edit-comment', 'Comment body field found.'); + + // Attempt to post to node with comments enabled (check field names etc). + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'comment' => COMMENT_NODE_OPEN)); + $this->assertTrue($this->node, 'Article node created.'); + $this->drupalGet('comment/reply/' . $this->node->nid); + $this->assertNoText('This discussion is closed', 'Posting to node with comments enabled'); + $this->assertField('edit-comment-body-' . $langcode . '-0-value', 'Comment body field found.'); + + // Delete comment and make sure that reply is also removed. + $this->drupalLogout(); + $this->drupalLogin($this->admin_user); + $this->deleteComment($comment); + $this->deleteComment($comment_new_page); + + $this->drupalGet('node/' . $this->node->nid); + $this->assertFalse($this->commentExists($comment), 'Comment not found.'); + $this->assertFalse($this->commentExists($reply, TRUE), 'Reply not found.'); + + // Enabled comment form on node page. + $this->drupalLogin($this->admin_user); + $this->setCommentForm(TRUE); + $this->drupalLogout(); + + // Submit comment through node form. + $this->drupalLogin($this->web_user); + $this->drupalGet('node/' . $this->node->nid); + $form_comment = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + $this->assertTrue($this->commentExists($form_comment), 'Form comment found.'); + + // Disable comment form on node page. + $this->drupalLogout(); + $this->drupalLogin($this->admin_user); + $this->setCommentForm(FALSE); + } + + /** + * Tests new comment marker. + */ + public function testCommentNewCommentsIndicator() { + // Test if the right links are displayed when no comment is present for the + // node. + $this->drupalLogin($this->admin_user); + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'comment' => COMMENT_NODE_OPEN)); + $this->drupalGet('node'); + $this->assertNoLink(t('@count comments', array('@count' => 0))); + $this->assertNoLink(t('@count new comments', array('@count' => 0))); + $this->assertLink(t('Read more')); + $count = $this->xpath('//div[@id=:id]/div[@class=:class]/ul/li', array(':id' => 'node-' . $this->node->nid, ':class' => 'link-wrapper')); + $this->assertTrue(count($count) == 1, 'One child found'); + + // Create a new comment. This helper function may be run with different + // comment settings so use comment_save() to avoid complex setup. + $comment = (object) array( + 'cid' => NULL, + 'nid' => $this->node->nid, + 'node_type' => $this->node->type, + 'pid' => 0, + 'uid' => $this->loggedInUser->uid, + 'status' => COMMENT_PUBLISHED, + 'subject' => $this->randomName(), + 'hostname' => ip_address(), + 'language' => LANGUAGE_NONE, + 'comment_body' => array(LANGUAGE_NONE => array($this->randomName())), + ); + comment_save($comment); + $this->drupalLogout(); + + // Log in with 'web user' and check comment links. + $this->drupalLogin($this->web_user); + $this->drupalGet('node'); + $this->assertLink(t('1 new comment')); + $this->clickLink(t('1 new comment')); + $this->assertRaw('', 'Found "new" marker.'); + $this->assertTrue($this->xpath('//a[@id=:new]/following-sibling::a[1][@id=:comment_id]', array(':new' => 'new', ':comment_id' => 'comment-1')), 'The "new" anchor is positioned at the right comment.'); + + // Test if "new comment" link is correctly removed. + $this->drupalGet('node'); + $this->assertLink(t('1 comment')); + $this->assertLink(t('Read more')); + $this->assertNoLink(t('1 new comment')); + $this->assertNoLink(t('@count new comments', array('@count' => 0))); + $count = $this->xpath('//div[@id=:id]/div[@class=:class]/ul/li', array(':id' => 'node-' . $this->node->nid, ':class' => 'link-wrapper')); + $this->assertTrue(count($count) == 2, print_r($count, TRUE)); + } + + /** + * Tests CSS classes on comments. + */ + function testCommentClasses() { + // Create all permutations for comments, users, and nodes. + $parameters = array( + 'node_uid' => array(0, $this->web_user->uid), + 'comment_uid' => array(0, $this->web_user->uid, $this->admin_user->uid), + 'comment_status' => array(COMMENT_PUBLISHED, COMMENT_NOT_PUBLISHED), + 'user' => array('anonymous', 'authenticated', 'admin'), + ); + $permutations = $this->generatePermutations($parameters); + + foreach ($permutations as $case) { + // Create a new node. + $node = $this->drupalCreateNode(array('type' => 'article', 'uid' => $case['node_uid'])); + + // Add a comment. + $comment = (object) array( + 'cid' => NULL, + 'nid' => $node->nid, + 'pid' => 0, + 'uid' => $case['comment_uid'], + 'status' => $case['comment_status'], + 'subject' => $this->randomName(), + 'language' => LANGUAGE_NONE, + 'comment_body' => array(LANGUAGE_NONE => array($this->randomName())), + ); + comment_save($comment); + + // Adjust the current/viewing user. + switch ($case['user']) { + case 'anonymous': + $this->drupalLogout(); + $case['user_uid'] = 0; + break; + + case 'authenticated': + $this->drupalLogin($this->web_user); + $case['user_uid'] = $this->web_user->uid; + break; + + case 'admin': + $this->drupalLogin($this->admin_user); + $case['user_uid'] = $this->admin_user->uid; + break; + } + // Request the node with the comment. + $this->drupalGet('node/' . $node->nid); + + // Verify classes if the comment is visible for the current user. + if ($case['comment_status'] == COMMENT_PUBLISHED || $case['user'] == 'admin') { + // Verify the comment-by-anonymous class. + $comments = $this->xpath('//*[contains(@class, "comment-by-anonymous")]'); + if ($case['comment_uid'] == 0) { + $this->assertTrue(count($comments) == 1, 'comment-by-anonymous class found.'); + } + else { + $this->assertFalse(count($comments), 'comment-by-anonymous class not found.'); + } + + // Verify the comment-by-node-author class. + $comments = $this->xpath('//*[contains(@class, "comment-by-node-author")]'); + if ($case['comment_uid'] > 0 && $case['comment_uid'] == $case['node_uid']) { + $this->assertTrue(count($comments) == 1, 'comment-by-node-author class found.'); + } + else { + $this->assertFalse(count($comments), 'comment-by-node-author class not found.'); + } + + // Verify the comment-by-viewer class. + $comments = $this->xpath('//*[contains(@class, "comment-by-viewer")]'); + if ($case['comment_uid'] > 0 && $case['comment_uid'] == $case['user_uid']) { + $this->assertTrue(count($comments) == 1, 'comment-by-viewer class found.'); + } + else { + $this->assertFalse(count($comments), 'comment-by-viewer class not found.'); + } + } + + // Verify the comment-unpublished class. + $comments = $this->xpath('//*[contains(@class, "comment-unpublished")]'); + if ($case['comment_status'] == COMMENT_NOT_PUBLISHED && $case['user'] == 'admin') { + $this->assertTrue(count($comments) == 1, 'comment-unpublished class found.'); + } + else { + $this->assertFalse(count($comments), 'comment-unpublished class not found.'); + } + + // Verify the comment-new class. + if ($case['comment_status'] == COMMENT_PUBLISHED || $case['user'] == 'admin') { + $comments = $this->xpath('//*[contains(@class, "comment-new")]'); + if ($case['user'] != 'anonymous') { + $this->assertTrue(count($comments) == 1, 'comment-new class found.'); + + // Request the node again. The comment-new class should disappear. + $this->drupalGet('node/' . $node->nid); + $comments = $this->xpath('//*[contains(@class, "comment-new")]'); + $this->assertFalse(count($comments), 'comment-new class not found.'); + } + else { + $this->assertFalse(count($comments), 'comment-new class not found.'); + } + } + } + } + + /** + * Tests the node comment statistics. + */ + function testCommentNodeCommentStatistics() { + $langcode = LANGUAGE_NONE; + // Set comments to have subject and preview disabled. + $this->drupalLogin($this->admin_user); + $this->setCommentPreview(DRUPAL_DISABLED); + $this->setCommentForm(TRUE); + $this->setCommentSubject(FALSE); + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Comment paging changed.'); + $this->drupalLogout(); + + // Creates a second user to post comments. + $this->web_user2 = $this->drupalCreateUser(array('access comments', 'post comments', 'create article content', 'edit own comments')); + + // Checks the initial values of node comment statistics with no comment. + $node = node_load($this->node->nid); + $this->assertEqual($node->last_comment_timestamp, $this->node->created, 'The initial value of node last_comment_timestamp is the node created date.'); + $this->assertEqual($node->last_comment_name, NULL, 'The initial value of node last_comment_name is NULL.'); + $this->assertEqual($node->last_comment_uid, $this->web_user->uid, 'The initial value of node last_comment_uid is the node uid.'); + $this->assertEqual($node->comment_count, 0, 'The initial value of node comment_count is zero.'); + + // Post comment #1 as web_user2. + $this->drupalLogin($this->web_user2); + $comment_text = $this->randomName(); + $comment = $this->postComment($this->node, $comment_text); + $comment_loaded = comment_load($comment->id); + + // Checks the new values of node comment statistics with comment #1. + // The node needs to be reloaded with a node_load_multiple cache reset. + $node = node_load($this->node->nid, NULL, TRUE); + $this->assertEqual($node->last_comment_name, NULL, 'The value of node last_comment_name is NULL.'); + $this->assertEqual($node->last_comment_uid, $this->web_user2->uid, 'The value of node last_comment_uid is the comment #1 uid.'); + $this->assertEqual($node->comment_count, 1, 'The value of node comment_count is 1.'); + + // Prepare for anonymous comment submission (comment approval enabled). + variable_set('user_register', USER_REGISTER_VISITORS); + $this->drupalLogin($this->admin_user); + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access comments' => TRUE, + 'post comments' => TRUE, + 'skip comment approval' => FALSE, + )); + // Ensure that the poster can leave some contact info. + $this->setCommentAnonymous('1'); + $this->drupalLogout(); + + // Post comment #2 as anonymous (comment approval enabled). + $this->drupalGet('comment/reply/' . $this->node->nid); + $anonymous_comment = $this->postComment($this->node, $this->randomName(), '', TRUE); + $comment_unpublished_loaded = comment_load($anonymous_comment->id); + + // Checks the new values of node comment statistics with comment #2 and + // ensure they haven't changed since the comment has not been moderated. + // The node needs to be reloaded with a node_load_multiple cache reset. + $node = node_load($this->node->nid, NULL, TRUE); + $this->assertEqual($node->last_comment_name, NULL, 'The value of node last_comment_name is still NULL.'); + $this->assertEqual($node->last_comment_uid, $this->web_user2->uid, 'The value of node last_comment_uid is still the comment #1 uid.'); + $this->assertEqual($node->comment_count, 1, 'The value of node comment_count is still 1.'); + + // Prepare for anonymous comment submission (no approval required). + $this->drupalLogin($this->admin_user); + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access comments' => TRUE, + 'post comments' => TRUE, + 'skip comment approval' => TRUE, + )); + $this->drupalLogout(); + + // Post comment #3 as anonymous. + $this->drupalGet('comment/reply/' . $this->node->nid); + $anonymous_comment = $this->postComment($this->node, $this->randomName(), '', array('name' => $this->randomName())); + $comment_loaded = comment_load($anonymous_comment->id); + + // Checks the new values of node comment statistics with comment #3. + // The node needs to be reloaded with a node_load_multiple cache reset. + $node = node_load($this->node->nid, NULL, TRUE); + $this->assertEqual($node->last_comment_name, $comment_loaded->name, 'The value of node last_comment_name is the name of the anonymous user.'); + $this->assertEqual($node->last_comment_uid, 0, 'The value of node last_comment_uid is zero.'); + $this->assertEqual($node->comment_count, 2, 'The value of node comment_count is 2.'); + } + + /** + * Tests comment links. + * + * The output of comment links depends on various environment conditions: + * - Various Comment module configuration settings, user registration + * settings, and user access permissions. + * - Whether the user is authenticated or not, and whether any comments exist. + * + * To account for all possible cases, this test creates permutations of all + * possible conditions and tests the expected appearance of comment links in + * each environment. + */ + function testCommentLinks() { + // Bartik theme alters comment links, so use a different theme. + theme_enable(array('garland')); + variable_set('theme_default', 'garland'); + + // Remove additional user permissions from $this->web_user added by setUp(), + // since this test is limited to anonymous and authenticated roles only. + user_role_delete(key($this->web_user->roles)); + + // Matrix of possible environmental conditions and configuration settings. + // See setEnvironment() for details. + $conditions = array( + 'authenticated' => array(FALSE, TRUE), + 'comment count' => array(FALSE, TRUE), + 'access comments' => array(0, 1), + 'post comments' => array(0, 1), + 'form' => array(COMMENT_FORM_BELOW, COMMENT_FORM_SEPARATE_PAGE), + // USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL is irrelevant for this + // test; there is only a difference between open and closed registration. + 'user_register' => array(USER_REGISTER_VISITORS, USER_REGISTER_ADMINISTRATORS_ONLY), + // @todo Complete test coverage for: + //'comments' => array(COMMENT_NODE_OPEN, COMMENT_NODE_CLOSED, COMMENT_NODE_HIDDEN), + //// COMMENT_ANONYMOUS_MUST_CONTACT is irrelevant for this test. + //'contact ' => array(COMMENT_ANONYMOUS_MAY_CONTACT, COMMENT_ANONYMOUS_MAYNOT_CONTACT), + ); + + $environments = $this->generatePermutations($conditions); + foreach ($environments as $info) { + $this->assertCommentLinks($info); + } + } + + /** + * Re-configures the environment, module settings, and user permissions. + * + * @param $info + * An associative array describing the environment to setup: + * - Environment conditions: + * - authenticated: Boolean whether to test with $this->web_user or + * anonymous. + * - comment count: Boolean whether to test with a new/unread comment on + * $this->node or no comments. + * - Configuration settings: + * - form: COMMENT_FORM_BELOW or COMMENT_FORM_SEPARATE_PAGE. + * - user_register: USER_REGISTER_ADMINISTRATORS_ONLY or + * USER_REGISTER_VISITORS. + * - contact: COMMENT_ANONYMOUS_MAY_CONTACT or + * COMMENT_ANONYMOUS_MAYNOT_CONTACT. + * - comments: COMMENT_NODE_OPEN, COMMENT_NODE_CLOSED, or + * COMMENT_NODE_HIDDEN. + * - User permissions: + * These are granted or revoked for the user, according to the + * 'authenticated' flag above. Pass 0 or 1 as parameter values. See + * user_role_change_permissions(). + * - access comments + * - post comments + * - skip comment approval + * - edit own comments + */ + function setEnvironment(array $info) { + static $current; + + // Apply defaults to initial environment. + if (!isset($current)) { + $current = array( + 'authenticated' => FALSE, + 'comment count' => FALSE, + 'form' => COMMENT_FORM_BELOW, + 'user_register' => USER_REGISTER_VISITORS, + 'contact' => COMMENT_ANONYMOUS_MAY_CONTACT, + 'comments' => COMMENT_NODE_OPEN, + 'access comments' => 0, + 'post comments' => 0, + // Enabled by default, because it's irrelevant for this test. + 'skip comment approval' => 1, + 'edit own comments' => 0, + ); + } + // Complete new environment with current environment. + $info = array_merge($current, $info); + + // Change environment conditions. + if ($current['authenticated'] != $info['authenticated']) { + if ($this->loggedInUser) { + $this->drupalLogout(); + } + else { + $this->drupalLogin($this->web_user); + } + } + if ($current['comment count'] != $info['comment count']) { + if ($info['comment count']) { + // Create a comment via CRUD API functionality, since + // $this->postComment() relies on actual user permissions. + $comment = (object) array( + 'cid' => NULL, + 'nid' => $this->node->nid, + 'node_type' => $this->node->type, + 'pid' => 0, + 'uid' => 0, + 'status' => COMMENT_PUBLISHED, + 'subject' => $this->randomName(), + 'hostname' => ip_address(), + 'language' => LANGUAGE_NONE, + 'comment_body' => array(LANGUAGE_NONE => array($this->randomName())), + ); + comment_save($comment); + $this->comment = $comment; + + // comment_num_new() relies on node_last_viewed(), so ensure that no one + // has seen the node of this comment. + db_delete('history')->condition('nid', $this->node->nid)->execute(); + } + else { + $cids = db_query("SELECT cid FROM {comment}")->fetchCol(); + comment_delete_multiple($cids); + unset($this->comment); + } + } + + // Change comment settings. + variable_set('comment_form_location_' . $this->node->type, $info['form']); + variable_set('comment_anonymous_' . $this->node->type, $info['contact']); + if ($this->node->comment != $info['comments']) { + $this->node->comment = $info['comments']; + node_save($this->node); + } + + // Change user settings. + variable_set('user_register', $info['user_register']); + + // Change user permissions. + $rid = ($this->loggedInUser ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID); + $perms = array_intersect_key($info, array('access comments' => 1, 'post comments' => 1, 'skip comment approval' => 1, 'edit own comments' => 1)); + user_role_change_permissions($rid, $perms); + + // Output verbose debugging information. + // @see DrupalTestCase::error() + $t_form = array( + COMMENT_FORM_BELOW => 'below', + COMMENT_FORM_SEPARATE_PAGE => 'separate page', + ); + $t_contact = array( + COMMENT_ANONYMOUS_MAY_CONTACT => 'optional', + COMMENT_ANONYMOUS_MAYNOT_CONTACT => 'disabled', + COMMENT_ANONYMOUS_MUST_CONTACT => 'required', + ); + $t_comments = array( + COMMENT_NODE_OPEN => 'open', + COMMENT_NODE_CLOSED => 'closed', + COMMENT_NODE_HIDDEN => 'hidden', + ); + $verbose = $info; + $verbose['form'] = $t_form[$info['form']]; + $verbose['contact'] = $t_contact[$info['contact']]; + $verbose['comments'] = $t_comments[$info['comments']]; + $message = t('Changed environment:
    @verbose
    ', array( + '@verbose' => var_export($verbose, TRUE), + )); + $this->assert('debug', $message, 'Debug'); + + // Update current environment. + $current = $info; + + return $info; + } + + /** + * Asserts that comment links appear according to the passed environment setup. + * + * @param $info + * An associative array describing the environment to pass to + * setEnvironment(). + */ + function assertCommentLinks(array $info) { + $info = $this->setEnvironment($info); + + $nid = $this->node->nid; + + foreach (array('', "node/$nid") as $path) { + $this->drupalGet($path); + + // User is allowed to view comments. + if ($info['access comments']) { + if ($path == '') { + // In teaser view, a link containing the comment count is always + // expected. + if ($info['comment count']) { + $this->assertLink(t('1 comment')); + + // For logged in users, a link containing the amount of new/unread + // comments is expected. + // See important note about comment_num_new() below. + if ($this->loggedInUser && isset($this->comment) && !isset($this->comment->seen)) { + $this->assertLink(t('1 new comment')); + $this->comment->seen = TRUE; + } + } + } + } + else { + $this->assertNoLink(t('1 comment')); + $this->assertNoLink(t('1 new comment')); + } + // comment_num_new() is based on node views, so comments are marked as + // read when a node is viewed, regardless of whether we have access to + // comments. + if ($path == "node/$nid" && $this->loggedInUser && isset($this->comment)) { + $this->comment->seen = TRUE; + } + + // User is not allowed to post comments. + if (!$info['post comments']) { + $this->assertNoLink('Add new comment'); + + // Anonymous users should see a note to log in or register in case + // authenticated users are allowed to post comments. + // @see theme_comment_post_forbidden() + if (!$this->loggedInUser) { + if (user_access('post comments', $this->web_user)) { + // The note depends on whether users are actually able to register. + if ($info['user_register']) { + $this->assertText('Log in or register to post comments'); + } + else { + $this->assertText('Log in to post comments'); + } + } + else { + $this->assertNoText('Log in or register to post comments'); + $this->assertNoText('Log in to post comments'); + } + } + } + // User is allowed to post comments. + else { + $this->assertNoText('Log in or register to post comments'); + + // "Add new comment" is always expected, except when there are no + // comments or if the user cannot see them. + if ($path == "node/$nid" && $info['form'] == COMMENT_FORM_BELOW && (!$info['comment count'] || !$info['access comments'])) { + $this->assertNoLink('Add new comment'); + } + else { + $this->assertLink('Add new comment'); + } + + // Also verify that the comment form appears according to the configured + // location. + if ($path == "node/$nid") { + $elements = $this->xpath('//form[@id=:id]', array(':id' => 'comment-form')); + if ($info['form'] == COMMENT_FORM_BELOW) { + $this->assertTrue(count($elements), 'Comment form found below.'); + } + else { + $this->assertFalse(count($elements), 'Comment form not found below.'); + } + } + } + } + } +} + +/** + * Test previewing comments. + */ +class CommentPreviewTest extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment preview', + 'description' => 'Test comment preview.', + 'group' => 'Comment', + ); + } + + /** + * Test comment preview. + */ + function testCommentPreview() { + $langcode = LANGUAGE_NONE; + + // As admin user, configure comment settings. + $this->drupalLogin($this->admin_user); + $this->setCommentPreview(DRUPAL_OPTIONAL); + $this->setCommentForm(TRUE); + $this->setCommentSubject(TRUE); + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Comment paging changed.'); + $this->drupalLogout(); + + // Login as web user and add a signature and a user picture. + $this->drupalLogin($this->web_user); + variable_set('user_signatures', 1); + variable_set('user_pictures', 1); + $test_signature = $this->randomName(); + $edit['signature[value]'] = '' . $test_signature. ''; + $edit['signature[format]'] = 'filtered_html'; + $image = current($this->drupalGetTestFiles('image')); + $edit['files[picture_upload]'] = drupal_realpath($image->uri); + $this->drupalPost('user/' . $this->web_user->uid . '/edit', $edit, t('Save')); + + // As the web user, fill in the comment form and preview the comment. + $edit = array(); + $edit['subject'] = $this->randomName(8); + $edit['comment_body[' . $langcode . '][0][value]'] = $this->randomName(16); + $this->drupalPost('node/' . $this->node->nid, $edit, t('Preview')); + + // Check that the preview is displaying the title and body. + $this->assertTitle(t('Preview comment | Drupal'), 'Page title is "Preview comment".'); + $this->assertText($edit['subject'], 'Subject displayed.'); + $this->assertText($edit['comment_body[' . $langcode . '][0][value]'], 'Comment displayed.'); + + // Check that the title and body fields are displayed with the correct values. + $this->assertFieldByName('subject', $edit['subject'], 'Subject field displayed.'); + $this->assertFieldByName('comment_body[' . $langcode . '][0][value]', $edit['comment_body[' . $langcode . '][0][value]'], 'Comment field displayed.'); + + // Check that the signature is displaying with the correct text format. + $this->assertLink($test_signature); + + // Check that the user picture is displayed. + $this->assertFieldByXPath("//div[contains(@class, 'comment-preview')]//div[contains(@class, 'user-picture')]//img", NULL, 'User picture displayed.'); + } + + /** + * Test comment edit, preview, and save. + */ + function testCommentEditPreviewSave() { + $langcode = LANGUAGE_NONE; + $web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'skip comment approval')); + $this->drupalLogin($this->admin_user); + $this->setCommentPreview(DRUPAL_OPTIONAL); + $this->setCommentForm(TRUE); + $this->setCommentSubject(TRUE); + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Comment paging changed.'); + + $edit = array(); + $edit['subject'] = $this->randomName(8); + $edit['comment_body[' . $langcode . '][0][value]'] = $this->randomName(16); + $edit['name'] = $web_user->name; + $edit['date'] = '2008-03-02 17:23 +0300'; + $raw_date = strtotime($edit['date']); + $expected_text_date = format_date($raw_date); + $expected_form_date = format_date($raw_date, 'custom', 'Y-m-d H:i O'); + $comment = $this->postComment($this->node, $edit['subject'], $edit['comment_body[' . $langcode . '][0][value]'], TRUE); + $this->drupalPost('comment/' . $comment->id . '/edit', $edit, t('Preview')); + + // Check that the preview is displaying the subject, comment, author and date correctly. + $this->assertTitle(t('Preview comment | Drupal'), 'Page title is "Preview comment".'); + $this->assertText($edit['subject'], 'Subject displayed.'); + $this->assertText($edit['comment_body[' . $langcode . '][0][value]'], 'Comment displayed.'); + $this->assertText($edit['name'], 'Author displayed.'); + $this->assertText($expected_text_date, 'Date displayed.'); + + // Check that the subject, comment, author and date fields are displayed with the correct values. + $this->assertFieldByName('subject', $edit['subject'], 'Subject field displayed.'); + $this->assertFieldByName('comment_body[' . $langcode . '][0][value]', $edit['comment_body[' . $langcode . '][0][value]'], 'Comment field displayed.'); + $this->assertFieldByName('name', $edit['name'], 'Author field displayed.'); + $this->assertFieldByName('date', $edit['date'], 'Date field displayed.'); + + // Check that saving a comment produces a success message. + $this->drupalPost('comment/' . $comment->id . '/edit', $edit, t('Save')); + $this->assertText(t('Your comment has been posted.'), 'Comment posted.'); + + // Check that the comment fields are correct after loading the saved comment. + $this->drupalGet('comment/' . $comment->id . '/edit'); + $this->assertFieldByName('subject', $edit['subject'], 'Subject field displayed.'); + $this->assertFieldByName('comment_body[' . $langcode . '][0][value]', $edit['comment_body[' . $langcode . '][0][value]'], 'Comment field displayed.'); + $this->assertFieldByName('name', $edit['name'], 'Author field displayed.'); + $this->assertFieldByName('date', $expected_form_date, 'Date field displayed.'); + + // Submit the form using the displayed values. + $displayed = array(); + $displayed['subject'] = (string) current($this->xpath("//input[@id='edit-subject']/@value")); + $displayed['comment_body[' . $langcode . '][0][value]'] = (string) current($this->xpath("//textarea[@id='edit-comment-body-" . $langcode . "-0-value']")); + $displayed['name'] = (string) current($this->xpath("//input[@id='edit-name']/@value")); + $displayed['date'] = (string) current($this->xpath("//input[@id='edit-date']/@value")); + $this->drupalPost('comment/' . $comment->id . '/edit', $displayed, t('Save')); + + // Check that the saved comment is still correct. + $comment_loaded = comment_load($comment->id); + $this->assertEqual($comment_loaded->subject, $edit['subject'], 'Subject loaded.'); + $this->assertEqual($comment_loaded->comment_body[$langcode][0]['value'], $edit['comment_body[' . $langcode . '][0][value]'], 'Comment body loaded.'); + $this->assertEqual($comment_loaded->name, $edit['name'], 'Name loaded.'); + $this->assertEqual($comment_loaded->created, $raw_date, 'Date loaded.'); + + } + +} + +class CommentAnonymous extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Anonymous comments', + 'description' => 'Test anonymous comments.', + 'group' => 'Comment', + ); + } + + function setUp() { + parent::setUp(); + variable_set('user_register', USER_REGISTER_VISITORS); + } + + /** + * Test anonymous comment functionality. + */ + function testAnonymous() { + $this->drupalLogin($this->admin_user); + // Enabled anonymous user comments. + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access comments' => TRUE, + 'post comments' => TRUE, + 'skip comment approval' => TRUE, + )); + $this->setCommentAnonymous('0'); // Ensure that doesn't require contact info. + $this->drupalLogout(); + + // Post anonymous comment without contact info. + $anonymous_comment1 = $this->postComment($this->node, $this->randomName(), $this->randomName()); + $this->assertTrue($this->commentExists($anonymous_comment1), 'Anonymous comment without contact info found.'); + + // Allow contact info. + $this->drupalLogin($this->admin_user); + $this->setCommentAnonymous('1'); + + // Attempt to edit anonymous comment. + $this->drupalGet('comment/' . $anonymous_comment1->id . '/edit'); + $edited_comment = $this->postComment(NULL, $this->randomName(), $this->randomName()); + $this->assertTrue($this->commentExists($edited_comment, FALSE), 'Modified reply found.'); + $this->drupalLogout(); + + // Post anonymous comment with contact info (optional). + $this->drupalGet('comment/reply/' . $this->node->nid); + $this->assertTrue($this->commentContactInfoAvailable(), 'Contact information available.'); + + $anonymous_comment2 = $this->postComment($this->node, $this->randomName(), $this->randomName()); + $this->assertTrue($this->commentExists($anonymous_comment2), 'Anonymous comment with contact info (optional) found.'); + + // Ensure anonymous users cannot post in the name of registered users. + $langcode = LANGUAGE_NONE; + $edit = array( + 'name' => $this->admin_user->name, + 'mail' => $this->randomName() . '@example.com', + 'subject' => $this->randomName(), + "comment_body[$langcode][0][value]" => $this->randomName(), + ); + $this->drupalPost('comment/reply/' . $this->node->nid, $edit, t('Save')); + $this->assertText(t('The name you used belongs to a registered user.')); + + // Require contact info. + $this->drupalLogin($this->admin_user); + $this->setCommentAnonymous('2'); + $this->drupalLogout(); + + // Try to post comment with contact info (required). + $this->drupalGet('comment/reply/' . $this->node->nid); + $this->assertTrue($this->commentContactInfoAvailable(), 'Contact information available.'); + + $anonymous_comment3 = $this->postComment($this->node, $this->randomName(), $this->randomName(), TRUE); + // Name should have 'Anonymous' for value by default. + $this->assertText(t('E-mail field is required.'), 'E-mail required.'); + $this->assertFalse($this->commentExists($anonymous_comment3), 'Anonymous comment with contact info (required) not found.'); + + // Post comment with contact info (required). + $author_name = $this->randomName(); + $author_mail = $this->randomName() . '@example.com'; + $anonymous_comment3 = $this->postComment($this->node, $this->randomName(), $this->randomName(), array('name' => $author_name, 'mail' => $author_mail)); + $this->assertTrue($this->commentExists($anonymous_comment3), 'Anonymous comment with contact info (required) found.'); + + // Make sure the user data appears correctly when editing the comment. + $this->drupalLogin($this->admin_user); + $this->drupalGet('comment/' . $anonymous_comment3->id . '/edit'); + $this->assertRaw($author_name, "The anonymous user's name is correct when editing the comment."); + $this->assertRaw($author_mail, "The anonymous user's e-mail address is correct when editing the comment."); + + // Unpublish comment. + $this->performCommentOperation($anonymous_comment3, 'unpublish'); + + $this->drupalGet('admin/content/comment/approval'); + $this->assertRaw('comments[' . $anonymous_comment3->id . ']', 'Comment was unpublished.'); + + // Publish comment. + $this->performCommentOperation($anonymous_comment3, 'publish', TRUE); + + $this->drupalGet('admin/content/comment'); + $this->assertRaw('comments[' . $anonymous_comment3->id . ']', 'Comment was published.'); + + // Delete comment. + $this->performCommentOperation($anonymous_comment3, 'delete'); + + $this->drupalGet('admin/content/comment'); + $this->assertNoRaw('comments[' . $anonymous_comment3->id . ']', 'Comment was deleted.'); + $this->drupalLogout(); + + // Reset. + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access comments' => FALSE, + 'post comments' => FALSE, + 'skip comment approval' => FALSE, + )); + + // Attempt to view comments while disallowed. + // NOTE: if authenticated user has permission to post comments, then a + // "Login or register to post comments" type link may be shown. + $this->drupalGet('node/' . $this->node->nid); + $this->assertNoPattern('@]*>Comments@', 'Comments were not displayed.'); + $this->assertNoLink('Add new comment', 'Link to add comment was found.'); + + // Attempt to view node-comment form while disallowed. + $this->drupalGet('comment/reply/' . $this->node->nid); + $this->assertText('You are not authorized to post comments', 'Error attempting to post comment.'); + $this->assertNoFieldByName('subject', '', 'Subject field not found.'); + $this->assertNoFieldByName("comment_body[$langcode][0][value]", '', 'Comment field not found.'); + + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access comments' => TRUE, + 'post comments' => FALSE, + 'skip comment approval' => FALSE, + )); + $this->drupalGet('node/' . $this->node->nid); + $this->assertPattern('@]*>Comments@', 'Comments were displayed.'); + $this->assertLink('Log in', 1, 'Link to log in was found.'); + $this->assertLink('register', 1, 'Link to register was found.'); + + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access comments' => FALSE, + 'post comments' => TRUE, + 'skip comment approval' => TRUE, + )); + $this->drupalGet('node/' . $this->node->nid); + $this->assertNoPattern('@]*>Comments@', 'Comments were not displayed.'); + $this->assertFieldByName('subject', '', 'Subject field found.'); + $this->assertFieldByName("comment_body[$langcode][0][value]", '', 'Comment field found.'); + + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $anonymous_comment3->id); + $this->assertText('You are not authorized to view comments', 'Error attempting to post reply.'); + $this->assertNoText($author_name, 'Comment not displayed.'); + } +} + +/** + * Verify pagination of comments. + */ +class CommentPagerTest extends CommentHelperCase { + + public static function getInfo() { + return array( + 'name' => 'Comment paging settings', + 'description' => 'Test paging of comments and their settings.', + 'group' => 'Comment', + ); + } + + /** + * Confirm comment paging works correctly with flat and threaded comments. + */ + function testCommentPaging() { + $this->drupalLogin($this->admin_user); + + // Set comment variables. + $this->setCommentForm(TRUE); + $this->setCommentSubject(TRUE); + $this->setCommentPreview(DRUPAL_DISABLED); + + // Create a node and three comments. + $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $comments = array(); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_FLAT, 'Comment paging changed.'); + + // Set comments to one per page so that we are able to test paging without + // needing to insert large numbers of comments. + $this->setCommentsPerPage(1); + + // Check the first page of the node, and confirm the correct comments are + // shown. + $this->drupalGet('node/' . $node->nid); + $this->assertRaw(t('next'), 'Paging links found.'); + $this->assertTrue($this->commentExists($comments[0]), 'Comment 1 appears on page 1.'); + $this->assertFalse($this->commentExists($comments[1]), 'Comment 2 does not appear on page 1.'); + $this->assertFalse($this->commentExists($comments[2]), 'Comment 3 does not appear on page 1.'); + + // Check the second page. + $this->drupalGet('node/' . $node->nid, array('query' => array('page' => 1))); + $this->assertTrue($this->commentExists($comments[1]), 'Comment 2 appears on page 2.'); + $this->assertFalse($this->commentExists($comments[0]), 'Comment 1 does not appear on page 2.'); + $this->assertFalse($this->commentExists($comments[2]), 'Comment 3 does not appear on page 2.'); + + // Check the third page. + $this->drupalGet('node/' . $node->nid, array('query' => array('page' => 2))); + $this->assertTrue($this->commentExists($comments[2]), 'Comment 3 appears on page 3.'); + $this->assertFalse($this->commentExists($comments[0]), 'Comment 1 does not appear on page 3.'); + $this->assertFalse($this->commentExists($comments[1]), 'Comment 2 does not appear on page 3.'); + + // Post a reply to the oldest comment and test again. + $replies = array(); + $oldest_comment = reset($comments); + $this->drupalGet('comment/reply/' . $node->nid . '/' . $oldest_comment->id); + $reply = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + + $this->setCommentsPerPage(2); + // We are still in flat view - the replies should not be on the first page, + // even though they are replies to the oldest comment. + $this->drupalGet('node/' . $node->nid, array('query' => array('page' => 0))); + $this->assertFalse($this->commentExists($reply, TRUE), 'In flat mode, reply does not appear on page 1.'); + + // If we switch to threaded mode, the replies on the oldest comment + // should be bumped to the first page and comment 6 should be bumped + // to the second page. + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Switched to threaded mode.'); + $this->drupalGet('node/' . $node->nid, array('query' => array('page' => 0))); + $this->assertTrue($this->commentExists($reply, TRUE), 'In threaded mode, reply appears on page 1.'); + $this->assertFalse($this->commentExists($comments[1]), 'In threaded mode, comment 2 has been bumped off of page 1.'); + + // If (# replies > # comments per page) in threaded expanded view, + // the overage should be bumped. + $reply2 = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + $this->drupalGet('node/' . $node->nid, array('query' => array('page' => 0))); + $this->assertFalse($this->commentExists($reply2, TRUE), 'In threaded mode where # replies > # comments per page, the newest reply does not appear on page 1.'); + + $this->drupalLogout(); + } + + /** + * Test comment ordering and threading. + */ + function testCommentOrderingThreading() { + $this->drupalLogin($this->admin_user); + + // Set comment variables. + $this->setCommentForm(TRUE); + $this->setCommentSubject(TRUE); + $this->setCommentPreview(DRUPAL_DISABLED); + + // Display all the comments on the same page. + $this->setCommentsPerPage(1000); + + // Create a node and three comments. + $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $comments = array(); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + + // Post a reply to the second comment. + $this->drupalGet('comment/reply/' . $node->nid . '/' . $comments[1]->id); + $comments[] = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + + // Post a reply to the first comment. + $this->drupalGet('comment/reply/' . $node->nid . '/' . $comments[0]->id); + $comments[] = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + + // Post a reply to the last comment. + $this->drupalGet('comment/reply/' . $node->nid . '/' . $comments[2]->id); + $comments[] = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + + // Post a reply to the second comment. + $this->drupalGet('comment/reply/' . $node->nid . '/' . $comments[3]->id); + $comments[] = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + + // At this point, the comment tree is: + // - 0 + // - 4 + // - 1 + // - 3 + // - 6 + // - 2 + // - 5 + + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_FLAT, 'Comment paging changed.'); + + $expected_order = array( + 0, + 1, + 2, + 3, + 4, + 5, + 6, + ); + $this->drupalGet('node/' . $node->nid); + $this->assertCommentOrder($comments, $expected_order); + + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Switched to threaded mode.'); + + $expected_order = array( + 0, + 4, + 1, + 3, + 6, + 2, + 5, + ); + $this->drupalGet('node/' . $node->nid); + $this->assertCommentOrder($comments, $expected_order); + } + + /** + * Helper function: assert that the comments are displayed in the correct order. + * + * @param $comments + * And array of comments. + * @param $expected_order + * An array of keys from $comments describing the expected order. + */ + function assertCommentOrder(array $comments, array $expected_order) { + $expected_cids = array(); + + // First, rekey the expected order by cid. + foreach ($expected_order as $key) { + $expected_cids[] = $comments[$key]->id; + } + + $comment_anchors = $this->xpath('//a[starts-with(@id,"comment-")]'); + $result_order = array(); + foreach ($comment_anchors as $anchor) { + $result_order[] = substr($anchor['id'], 8); + } + + return $this->assertIdentical($expected_cids, $result_order, format_string('Comment order: expected @expected, returned @returned.', array('@expected' => implode(',', $expected_cids), '@returned' => implode(',', $result_order)))); + } + + /** + * Test comment_new_page_count(). + */ + function testCommentNewPageIndicator() { + $this->drupalLogin($this->admin_user); + + // Set comment variables. + $this->setCommentForm(TRUE); + $this->setCommentSubject(TRUE); + $this->setCommentPreview(DRUPAL_DISABLED); + + // Set comments to one per page so that we are able to test paging without + // needing to insert large numbers of comments. + $this->setCommentsPerPage(1); + + // Create a node and three comments. + $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $comments = array(); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + + // Post a reply to the second comment. + $this->drupalGet('comment/reply/' . $node->nid . '/' . $comments[1]->id); + $comments[] = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + + // Post a reply to the first comment. + $this->drupalGet('comment/reply/' . $node->nid . '/' . $comments[0]->id); + $comments[] = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + + // Post a reply to the last comment. + $this->drupalGet('comment/reply/' . $node->nid . '/' . $comments[2]->id); + $comments[] = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + + // At this point, the comment tree is: + // - 0 + // - 4 + // - 1 + // - 3 + // - 2 + // - 5 + + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_FLAT, 'Comment paging changed.'); + + $expected_pages = array( + 1 => 5, // Page of comment 5 + 2 => 4, // Page of comment 4 + 3 => 3, // Page of comment 3 + 4 => 2, // Page of comment 2 + 5 => 1, // Page of comment 1 + 6 => 0, // Page of comment 0 + ); + + $node = node_load($node->nid); + foreach ($expected_pages as $new_replies => $expected_page) { + $returned = comment_new_page_count($node->comment_count, $new_replies, $node); + $returned_page = is_array($returned) ? $returned['page'] : 0; + $this->assertIdentical($expected_page, $returned_page, format_string('Flat mode, @new replies: expected page @expected, returned page @returned.', array('@new' => $new_replies, '@expected' => $expected_page, '@returned' => $returned_page))); + } + + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Switched to threaded mode.'); + + $expected_pages = array( + 1 => 5, // Page of comment 5 + 2 => 1, // Page of comment 4 + 3 => 1, // Page of comment 4 + 4 => 1, // Page of comment 4 + 5 => 1, // Page of comment 4 + 6 => 0, // Page of comment 0 + ); + + $node = node_load($node->nid); + foreach ($expected_pages as $new_replies => $expected_page) { + $returned = comment_new_page_count($node->comment_count, $new_replies, $node); + $returned_page = is_array($returned) ? $returned['page'] : 0; + $this->assertEqual($expected_page, $returned_page, format_string('Threaded mode, @new replies: expected page @expected, returned page @returned.', array('@new' => $new_replies, '@expected' => $expected_page, '@returned' => $returned_page))); + } + } +} + +/** + * Tests comments with node access. + * + * See http://drupal.org/node/886752 -- verify there is no PostgreSQL error when + * viewing a node with threaded comments (a comment and a reply), if a node + * access module is in use. + */ +class CommentNodeAccessTest extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment node access', + 'description' => 'Test comment viewing with node access.', + 'group' => 'Comment', + ); + } + + function setUp() { + DrupalWebTestCase::setUp('comment', 'search', 'node_access_test'); + node_access_rebuild(); + + // Create users and test node. + $this->admin_user = $this->drupalCreateUser(array('administer content types', 'administer comments', 'administer blocks')); + $this->web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'create article content', 'edit own comments', 'node test view')); + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->uid)); + } + + /** + * Test that threaded comments can be viewed. + */ + function testThreadedCommentView() { + $langcode = LANGUAGE_NONE; + // Set comments to have subject required and preview disabled. + $this->drupalLogin($this->admin_user); + $this->setCommentPreview(DRUPAL_DISABLED); + $this->setCommentForm(TRUE); + $this->setCommentSubject(TRUE); + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Comment paging changed.'); + $this->drupalLogout(); + + // Post comment. + $this->drupalLogin($this->web_user); + $comment_text = $this->randomName(); + $comment_subject = $this->randomName(); + $comment = $this->postComment($this->node, $comment_text, $comment_subject); + $comment_loaded = comment_load($comment->id); + $this->assertTrue($this->commentExists($comment), 'Comment found.'); + + // Check comment display. + $this->drupalGet('node/' . $this->node->nid . '/' . $comment->id); + $this->assertText($comment_subject, 'Individual comment subject found.'); + $this->assertText($comment_text, 'Individual comment body found.'); + + // Reply to comment, creating second comment. + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $comment->id); + $reply_text = $this->randomName(); + $reply_subject = $this->randomName(); + $reply = $this->postComment(NULL, $reply_text, $reply_subject, TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($reply, TRUE), 'Reply found.'); + + // Go to the node page and verify comment and reply are visible. + $this->drupalGet('node/' . $this->node->nid); + $this->assertText($comment_text); + $this->assertText($comment_subject); + $this->assertText($reply_text); + $this->assertText($reply_subject); + } +} + +class CommentApprovalTest extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment approval', + 'description' => 'Test comment approval functionality.', + 'group' => 'Comment', + ); + } + + /** + * Test comment approval functionality through admin/content/comment. + */ + function testApprovalAdminInterface() { + // Set anonymous comments to require approval. + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access comments' => TRUE, + 'post comments' => TRUE, + 'skip comment approval' => FALSE, + )); + $this->drupalLogin($this->admin_user); + $this->setCommentAnonymous('0'); // Ensure that doesn't require contact info. + + // Test that the comments page loads correctly when there are no comments + $this->drupalGet('admin/content/comment'); + $this->assertText(t('No comments available.')); + + $this->drupalLogout(); + + // Post anonymous comment without contact info. + $subject = $this->randomName(); + $body = $this->randomName(); + $this->postComment($this->node, $body, $subject, TRUE); // Set $contact to true so that it won't check for id and message. + $this->assertText(t('Your comment has been queued for review by site administrators and will be published after approval.'), 'Comment requires approval.'); + + // Get unapproved comment id. + $this->drupalLogin($this->admin_user); + $anonymous_comment4 = $this->getUnapprovedComment($subject); + $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body); + $this->drupalLogout(); + + $this->assertFalse($this->commentExists($anonymous_comment4), 'Anonymous comment was not published.'); + + // Approve comment. + $this->drupalLogin($this->admin_user); + $this->performCommentOperation($anonymous_comment4, 'publish', TRUE); + $this->drupalLogout(); + + $this->drupalGet('node/' . $this->node->nid); + $this->assertTrue($this->commentExists($anonymous_comment4), 'Anonymous comment visible.'); + + // Post 2 anonymous comments without contact info. + $comments[] = $this->postComment($this->node, $this->randomName(), $this->randomName(), TRUE); + $comments[] = $this->postComment($this->node, $this->randomName(), $this->randomName(), TRUE); + + // Publish multiple comments in one operation. + $this->drupalLogin($this->admin_user); + $this->drupalGet('admin/content/comment/approval'); + $this->assertText(t('Unapproved comments (@count)', array('@count' => 2)), 'Two unapproved comments waiting for approval.'); + $edit = array( + "comments[{$comments[0]->id}]" => 1, + "comments[{$comments[1]->id}]" => 1, + ); + $this->drupalPost(NULL, $edit, t('Update')); + $this->assertText(t('Unapproved comments (@count)', array('@count' => 0)), 'All comments were approved.'); + + // Delete multiple comments in one operation. + $edit = array( + 'operation' => 'delete', + "comments[{$comments[0]->id}]" => 1, + "comments[{$comments[1]->id}]" => 1, + "comments[{$anonymous_comment4->id}]" => 1, + ); + $this->drupalPost(NULL, $edit, t('Update')); + $this->assertText(t('Are you sure you want to delete these comments and all their children?'), 'Confirmation required.'); + $this->drupalPost(NULL, $edit, t('Delete comments')); + $this->assertText(t('No comments available.'), 'All comments were deleted.'); + } + + /** + * Test comment approval functionality through node interface. + */ + function testApprovalNodeInterface() { + // Set anonymous comments to require approval. + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access comments' => TRUE, + 'post comments' => TRUE, + 'skip comment approval' => FALSE, + )); + $this->drupalLogin($this->admin_user); + $this->setCommentAnonymous('0'); // Ensure that doesn't require contact info. + $this->drupalLogout(); + + // Post anonymous comment without contact info. + $subject = $this->randomName(); + $body = $this->randomName(); + $this->postComment($this->node, $body, $subject, TRUE); // Set $contact to true so that it won't check for id and message. + $this->assertText(t('Your comment has been queued for review by site administrators and will be published after approval.'), 'Comment requires approval.'); + + // Get unapproved comment id. + $this->drupalLogin($this->admin_user); + $anonymous_comment4 = $this->getUnapprovedComment($subject); + $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body); + $this->drupalLogout(); + + $this->assertFalse($this->commentExists($anonymous_comment4), 'Anonymous comment was not published.'); + + // Approve comment. + $this->drupalLogin($this->admin_user); + $this->drupalGet('comment/1/approve'); + $this->assertResponse(403, 'Forged comment approval was denied.'); + $this->drupalGet('comment/1/approve', array('query' => array('token' => 'forged'))); + $this->assertResponse(403, 'Forged comment approval was denied.'); + $this->drupalGet('node/' . $this->node->nid); + $this->clickLink(t('approve')); + $this->drupalLogout(); + + $this->drupalGet('node/' . $this->node->nid); + $this->assertTrue($this->commentExists($anonymous_comment4), 'Anonymous comment visible.'); + } +} + +/** + * Functional tests for the comment module blocks. + */ +class CommentBlockFunctionalTest extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment blocks', + 'description' => 'Test comment block functionality.', + 'group' => 'Comment', + ); + } + + /** + * Test the recent comments block. + */ + function testRecentCommentBlock() { + $this->drupalLogin($this->admin_user); + + // Set the block to a region to confirm block is available. + $edit = array( + 'blocks[comment_recent][region]' => 'sidebar_first', + ); + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + $this->assertText(t('The block settings have been updated.'), 'Block saved to first sidebar region.'); + + // Set block title and variables. + $block = array( + 'title' => $this->randomName(), + 'comment_block_count' => 2, + ); + $this->drupalPost('admin/structure/block/manage/comment/recent/configure', $block, t('Save block')); + $this->assertText(t('The block configuration has been saved.'), 'Block saved.'); + + // Add some test comments, one without a subject. + $comment1 = $this->postComment($this->node, $this->randomName(), $this->randomName()); + $comment2 = $this->postComment($this->node, $this->randomName(), $this->randomName()); + $comment3 = $this->postComment($this->node, $this->randomName()); + + // Test that a user without the 'access comments' permission cannot see the + // block. + $this->drupalLogout(); + user_role_revoke_permissions(DRUPAL_ANONYMOUS_RID, array('access comments')); + $this->drupalGet(''); + $this->assertNoText($block['title'], 'Block was not found.'); + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access comments')); + + // Test that a user with the 'access comments' permission can see the + // block. + $this->drupalLogin($this->web_user); + $this->drupalGet(''); + $this->assertText($block['title'], 'Block was found.'); + + // Test the only the 2 latest comments are shown and in the proper order. + $this->assertNoText($comment1->subject, 'Comment not found in block.'); + $this->assertText($comment2->subject, 'Comment found in block.'); + $this->assertText($comment3->comment, 'Comment found in block.'); + $this->assertTrue(strpos($this->drupalGetContent(), $comment3->comment) < strpos($this->drupalGetContent(), $comment2->subject), 'Comments were ordered correctly in block.'); + + // Set the number of recent comments to show to 10. + $this->drupalLogout(); + $this->drupalLogin($this->admin_user); + $block = array( + 'comment_block_count' => 10, + ); + $this->drupalPost('admin/structure/block/manage/comment/recent/configure', $block, t('Save block')); + $this->assertText(t('The block configuration has been saved.'), 'Block saved.'); + + // Post an additional comment. + $comment4 = $this->postComment($this->node, $this->randomName(), $this->randomName()); + + // Test that all four comments are shown. + $this->assertText($comment1->subject, 'Comment found in block.'); + $this->assertText($comment2->subject, 'Comment found in block.'); + $this->assertText($comment3->comment, 'Comment found in block.'); + $this->assertText($comment4->subject, 'Comment found in block.'); + + // Test that links to comments work when comments are across pages. + $this->setCommentsPerPage(1); + $this->drupalGet(''); + $this->clickLink($comment1->subject); + $this->assertText($comment1->subject, 'Comment link goes to correct page.'); + $this->drupalGet(''); + $this->clickLink($comment2->subject); + $this->assertText($comment2->subject, 'Comment link goes to correct page.'); + $this->clickLink($comment4->subject); + $this->assertText($comment4->subject, 'Comment link goes to correct page.'); + // Check that when viewing a comment page from a link to the comment, that + // rel="canonical" is added to the head of the document. + $this->assertRaw(' 'Comment RSS', + 'description' => 'Test comments as part of an RSS feed.', + 'group' => 'Comment', + ); + } + + /** + * Test comments as part of an RSS feed. + */ + function testCommentRSS() { + // Find comment in RSS feed. + $this->drupalLogin($this->web_user); + $comment = $this->postComment($this->node, $this->randomName(), $this->randomName()); + $this->drupalGet('rss.xml'); + $raw = '' . url('node/' . $this->node->nid, array('fragment' => 'comments', 'absolute' => TRUE)) . ''; + $this->assertRaw($raw, 'Comments as part of RSS feed.'); + + // Hide comments from RSS feed and check presence. + $this->node->comment = COMMENT_NODE_HIDDEN; + node_save($this->node); + $this->drupalGet('rss.xml'); + $this->assertNoRaw($raw, 'Hidden comments is not a part of RSS feed.'); + } +} + + +/** + * Test to make sure comment content is rebuilt. + */ +class CommentContentRebuild extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment Rebuild', + 'description' => 'Test to make sure the comment content is rebuilt.', + 'group' => 'Comment', + ); + } + + /** + * Test to ensure that the comment's content array is rebuilt for every + * call to comment_view(). + */ + function testCommentRebuild() { + // Update the comment settings so preview isn't required. + $this->drupalLogin($this->admin_user); + $this->setCommentSubject(TRUE); + $this->setCommentPreview(DRUPAL_OPTIONAL); + $this->drupalLogout(); + + // Log in as the web user and add the comment. + $this->drupalLogin($this->web_user); + $subject_text = $this->randomName(); + $comment_text = $this->randomName(); + $comment = $this->postComment($this->node, $comment_text, $subject_text, TRUE); + $comment_loaded = comment_load($comment->id); + $this->assertTrue($this->commentExists($comment), 'Comment found.'); + + // Add the property to the content array and then see if it still exists on build. + $comment_loaded->content['test_property'] = array('#value' => $this->randomString()); + $built_content = comment_view($comment_loaded, $this->node); + + // This means that the content was rebuilt as the added test property no longer exists. + $this->assertFalse(isset($built_content['test_property']), 'Comment content was emptied before being built.'); + } +} + +/** + * Test comment token replacement in strings. + */ +class CommentTokenReplaceTestCase extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment token replacement', + 'description' => 'Generates text using placeholders for dummy content to check comment token replacement.', + 'group' => 'Comment', + ); + } + + /** + * Creates a comment, then tests the tokens generated from it. + */ + function testCommentTokenReplacement() { + global $language; + $url_options = array( + 'absolute' => TRUE, + 'language' => $language, + ); + + $this->drupalLogin($this->admin_user); + + // Set comment variables. + $this->setCommentSubject(TRUE); + + // Create a node and a comment. + $node = $this->drupalCreateNode(array('type' => 'article')); + $parent_comment = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE); + + // Post a reply to the comment. + $this->drupalGet('comment/reply/' . $node->nid . '/' . $parent_comment->id); + $child_comment = $this->postComment(NULL, $this->randomName(), $this->randomName()); + $comment = comment_load($child_comment->id); + $comment->homepage = 'http://example.org/'; + + // Add HTML to ensure that sanitation of some fields tested directly. + $comment->subject = 'Blinking Comment'; + $instance = field_info_instance('comment', 'body', 'comment_body'); + + // Generate and test sanitized tokens. + $tests = array(); + $tests['[comment:cid]'] = $comment->cid; + $tests['[comment:hostname]'] = check_plain($comment->hostname); + $tests['[comment:name]'] = filter_xss($comment->name); + $tests['[comment:mail]'] = check_plain($this->admin_user->mail); + $tests['[comment:homepage]'] = check_url($comment->homepage); + $tests['[comment:title]'] = filter_xss($comment->subject); + $tests['[comment:body]'] = _text_sanitize($instance, LANGUAGE_NONE, $comment->comment_body[LANGUAGE_NONE][0], 'value'); + $tests['[comment:url]'] = url('comment/' . $comment->cid, $url_options + array('fragment' => 'comment-' . $comment->cid)); + $tests['[comment:edit-url]'] = url('comment/' . $comment->cid . '/edit', $url_options); + $tests['[comment:created:since]'] = format_interval(REQUEST_TIME - $comment->created, 2, $language->language); + $tests['[comment:changed:since]'] = format_interval(REQUEST_TIME - $comment->changed, 2, $language->language); + $tests['[comment:parent:cid]'] = $comment->pid; + $tests['[comment:parent:title]'] = check_plain($parent_comment->subject); + $tests['[comment:node:nid]'] = $comment->nid; + $tests['[comment:node:title]'] = check_plain($node->title); + $tests['[comment:author:uid]'] = $comment->uid; + $tests['[comment:author:name]'] = check_plain($this->admin_user->name); + + // Test to make sure that we generated something for each token. + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); + + foreach ($tests as $input => $expected) { + $output = token_replace($input, array('comment' => $comment), array('language' => $language)); + $this->assertEqual($output, $expected, format_string('Sanitized comment token %token replaced.', array('%token' => $input))); + } + + // Generate and test unsanitized tokens. + $tests['[comment:hostname]'] = $comment->hostname; + $tests['[comment:name]'] = $comment->name; + $tests['[comment:mail]'] = $this->admin_user->mail; + $tests['[comment:homepage]'] = $comment->homepage; + $tests['[comment:title]'] = $comment->subject; + $tests['[comment:body]'] = $comment->comment_body[LANGUAGE_NONE][0]['value']; + $tests['[comment:parent:title]'] = $parent_comment->subject; + $tests['[comment:node:title]'] = $node->title; + $tests['[comment:author:name]'] = $this->admin_user->name; + + foreach ($tests as $input => $expected) { + $output = token_replace($input, array('comment' => $comment), array('language' => $language, 'sanitize' => FALSE)); + $this->assertEqual($output, $expected, format_string('Unsanitized comment token %token replaced.', array('%token' => $input))); + } + + // Load node so comment_count gets computed. + $node = node_load($node->nid); + + // Generate comment tokens for the node (it has 2 comments, both new). + $tests = array(); + $tests['[node:comment-count]'] = 2; + $tests['[node:comment-count-new]'] = 2; + + foreach ($tests as $input => $expected) { + $output = token_replace($input, array('node' => $node), array('language' => $language)); + $this->assertEqual($output, $expected, format_string('Node comment token %token replaced.', array('%token' => $input))); + } + } +} + +/** + * Test actions provided by the comment module. + */ +class CommentActionsTestCase extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment actions', + 'description' => 'Test actions provided by the comment module.', + 'group' => 'Comment', + ); + } + + /** + * Test comment publish and unpublish actions. + */ + function testCommentPublishUnpublishActions() { + $this->drupalLogin($this->web_user); + $comment_text = $this->randomName(); + $subject = $this->randomName(); + $comment = $this->postComment($this->node, $comment_text, $subject); + $comment = comment_load($comment->id); + + // Unpublish a comment (direct form: doesn't actually save the comment). + comment_unpublish_action($comment); + $this->assertEqual($comment->status, COMMENT_NOT_PUBLISHED, 'Comment was unpublished'); + $this->assertWatchdogMessage('Unpublished comment %subject.', array('%subject' => $subject), 'Found watchdog message'); + $this->clearWatchdog(); + + // Unpublish a comment (indirect form: modify the comment in the database). + comment_unpublish_action(NULL, array('cid' => $comment->cid)); + $this->assertEqual(comment_load($comment->cid)->status, COMMENT_NOT_PUBLISHED, 'Comment was unpublished'); + $this->assertWatchdogMessage('Unpublished comment %subject.', array('%subject' => $subject), 'Found watchdog message'); + + // Publish a comment (direct form: doesn't actually save the comment). + comment_publish_action($comment); + $this->assertEqual($comment->status, COMMENT_PUBLISHED, 'Comment was published'); + $this->assertWatchdogMessage('Published comment %subject.', array('%subject' => $subject), 'Found watchdog message'); + $this->clearWatchdog(); + + // Publish a comment (indirect form: modify the comment in the database). + comment_publish_action(NULL, array('cid' => $comment->cid)); + $this->assertEqual(comment_load($comment->cid)->status, COMMENT_PUBLISHED, 'Comment was published'); + $this->assertWatchdogMessage('Published comment %subject.', array('%subject' => $subject), 'Found watchdog message'); + $this->clearWatchdog(); + } + + /** + * Tests the unpublish comment by keyword action. + */ + public function testCommentUnpublishByKeyword() { + $this->drupalLogin($this->admin_user); + $callback = 'comment_unpublish_by_keyword_action'; + $hash = drupal_hash_base64($callback); + $comment_text = $keywords = $this->randomName(); + $edit = array( + 'actions_label' => $callback, + 'keywords' => $keywords, + ); + + $this->drupalPost("admin/config/system/actions/configure/$hash", $edit, t('Save')); + + $action = db_query("SELECT aid, type, callback, parameters, label FROM {actions} WHERE callback = :callback", array(':callback' => $callback))->fetchObject(); + + $this->assertTrue($action, 'The action could be loaded.'); + + $comment = $this->postComment($this->node, $comment_text, $this->randomName()); + + // Load the full comment so that status is available. + $comment = comment_load($comment->id); + + $this->assertTrue($comment->status == COMMENT_PUBLISHED, 'The comment status was set to published.'); + + comment_unpublish_by_keyword_action($comment, array('keywords' => array($keywords))); + + // We need to make sure that the comment has been saved with status + // unpublished. + $this->assertEqual(comment_load($comment->cid)->status, COMMENT_NOT_PUBLISHED, 'Comment was unpublished.'); + $this->assertWatchdogMessage('Unpublished comment %subject.', array('%subject' => $comment->subject), 'Found watchdog message.'); + $this->clearWatchdog(); + } + + /** + * Verify that a watchdog message has been entered. + * + * @param $watchdog_message + * The watchdog message. + * @param $variables + * The array of variables passed to watchdog(). + * @param $message + * The assertion message. + */ + function assertWatchdogMessage($watchdog_message, $variables, $message) { + $status = (bool) db_query_range("SELECT 1 FROM {watchdog} WHERE message = :message AND variables = :variables", 0, 1, array(':message' => $watchdog_message, ':variables' => serialize($variables)))->fetchField(); + return $this->assert($status, format_string('@message', array('@message' => $message))); + } + + /** + * Helper function: clear the watchdog. + */ + function clearWatchdog() { + db_truncate('watchdog')->execute(); + } +} + +/** + * Test fields on comments. + */ +class CommentFieldsTest extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment fields', + 'description' => 'Tests fields on comments.', + 'group' => 'Comment', + ); + } + + /** + * Tests that the default 'comment_body' field is correctly added. + */ + function testCommentDefaultFields() { + // Do not make assumptions on default node types created by the test + // installation profile, and create our own. + $this->drupalCreateContentType(array('type' => 'test_node_type')); + + // Check that the 'comment_body' field is present on all comment bundles. + $instances = field_info_instances('comment'); + foreach (node_type_get_types() as $type_name => $info) { + $this->assertTrue(isset($instances['comment_node_' . $type_name]['comment_body']), format_string('The comment_body field is present for comments on type @type', array('@type' => $type_name))); + + // Delete the instance along the way. + field_delete_instance($instances['comment_node_' . $type_name]['comment_body']); + } + + // Check that the 'comment_body' field is deleted. + $field = field_info_field('comment_body'); + $this->assertTrue(empty($field), 'The comment_body field was deleted'); + + // Create a new content type. + $type_name = 'test_node_type_2'; + $this->drupalCreateContentType(array('type' => $type_name)); + + // Check that the 'comment_body' field exists and has an instance on the + // new comment bundle. + $field = field_info_field('comment_body'); + $this->assertTrue($field, 'The comment_body field exists'); + $instances = field_info_instances('comment'); + $this->assertTrue(isset($instances['comment_node_' . $type_name]['comment_body']), format_string('The comment_body field is present for comments on type @type', array('@type' => $type_name))); + } + + /** + * Test that comment module works when enabled after a content module. + */ + function testCommentEnable() { + // Create a user to do module administration. + $this->admin_user = $this->drupalCreateUser(array('access administration pages', 'administer modules')); + $this->drupalLogin($this->admin_user); + + // Disable the comment module. + $edit = array(); + $edit['modules[Core][comment][enable]'] = FALSE; + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->resetAll(); + $this->assertFalse(module_exists('comment'), 'Comment module disabled.'); + + // Enable core content type modules (blog, book, and poll). + $edit = array(); + $edit['modules[Core][blog][enable]'] = 'blog'; + $edit['modules[Core][book][enable]'] = 'book'; + $edit['modules[Core][poll][enable]'] = 'poll'; + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->resetAll(); + + // Now enable the comment module. + $edit = array(); + $edit['modules[Core][comment][enable]'] = 'comment'; + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->resetAll(); + $this->assertTrue(module_exists('comment'), 'Comment module enabled.'); + + // Create nodes of each type. + $blog_node = $this->drupalCreateNode(array('type' => 'blog')); + $book_node = $this->drupalCreateNode(array('type' => 'book')); + $poll_node = $this->drupalCreateNode(array('type' => 'poll', 'active' => 1, 'runtime' => 0, 'choice' => array(array('chtext' => '')))); + + $this->drupalLogout(); + + // Try to post a comment on each node. A failure will be triggered if the + // comment body is missing on one of these forms, due to postComment() + // asserting that the body is actually posted correctly. + $this->web_user = $this->drupalCreateUser(array('access content', 'access comments', 'post comments', 'skip comment approval')); + $this->drupalLogin($this->web_user); + $this->postComment($blog_node, $this->randomName(), $this->randomName()); + $this->postComment($book_node, $this->randomName(), $this->randomName()); + $this->postComment($poll_node, $this->randomName(), $this->randomName()); + } + + /** + * Test that comment module works correctly with plain text format. + */ + function testCommentFormat() { + // Disable text processing for comments. + $this->drupalLogin($this->admin_user); + $edit = array('instance[settings][text_processing]' => 0); + $this->drupalPost('admin/structure/types/manage/article/comment/fields/comment_body', $edit, t('Save settings')); + + // Post a comment without an explicit subject. + $this->drupalLogin($this->web_user); + $edit = array('comment_body[und][0][value]' => $this->randomName(8)); + $this->drupalPost('node/' . $this->node->nid, $edit, t('Save')); + } +} + +/** + * Tests comment threading. + */ +class CommentThreadingTestCase extends CommentHelperCase { + public static function getInfo() { + return array( + 'name' => 'Comment Threading', + 'description' => 'Test to make sure the comment number increments properly.', + 'group' => 'Comment', + ); + } + + /** + * Tests the comment threading. + */ + function testCommentThreading() { + $langcode = LANGUAGE_NONE; + // Set comments to have a subject with preview disabled. + $this->drupalLogin($this->admin_user); + $this->setCommentPreview(DRUPAL_DISABLED); + $this->setCommentForm(TRUE); + $this->setCommentSubject(TRUE); + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Comment paging changed.'); + $this->drupalLogout(); + + // Create a node. + $this->drupalLogin($this->web_user); + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->uid)); + + // Post comment #1. + $this->drupalLogin($this->web_user); + $subject_text = $this->randomName(); + $comment_text = $this->randomName(); + $comment = $this->postComment($this->node, $comment_text, $subject_text, TRUE); + $comment_loaded = comment_load($comment->id); + $this->assertTrue($this->commentExists($comment), 'Comment #1. Comment found.'); + $this->assertEqual($comment_loaded->thread, '01/'); + + // Reply to comment #1 creating comment #2. + $this->drupalLogin($this->web_user); + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $comment->id); + $reply = $this->postComment(NULL, $this->randomName(), '', TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($reply, TRUE), 'Comment #2. Reply found.'); + $this->assertEqual($reply_loaded->thread, '01.00/'); + + // Reply to comment #2 creating comment #3. + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $reply->id); + $reply = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($reply, TRUE), 'Comment #3. Second reply found.'); + $this->assertEqual($reply_loaded->thread, '01.00.00/'); + + // Reply to comment #1 creating comment #4. + $this->drupalLogin($this->web_user); + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $comment->id); + $reply = $this->postComment(NULL, $this->randomName(), '', TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($comment), 'Comment #4. Third reply found.'); + $this->assertEqual($reply_loaded->thread, '01.01/'); + + // Post comment #2 overall comment #5. + $this->drupalLogin($this->web_user); + $subject_text = $this->randomName(); + $comment_text = $this->randomName(); + $comment = $this->postComment($this->node, $comment_text, $subject_text, TRUE); + $comment_loaded = comment_load($comment->id); + $this->assertTrue($this->commentExists($comment), 'Comment #5. Second comment found.'); + $this->assertEqual($comment_loaded->thread, '02/'); + + // Reply to comment #5 creating comment #6. + $this->drupalLogin($this->web_user); + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $comment->id); + $reply = $this->postComment(NULL, $this->randomName(), '', TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($reply, TRUE), 'Comment #6. Reply found.'); + $this->assertEqual($reply_loaded->thread, '02.00/'); + + // Reply to comment #6 creating comment #7. + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $reply->id); + $reply = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($reply, TRUE), 'Comment #7. Second reply found.'); + $this->assertEqual($reply_loaded->thread, '02.00.00/'); + + // Reply to comment #5 creating comment #8. + $this->drupalLogin($this->web_user); + $this->drupalGet('comment/reply/' . $this->node->nid . '/' . $comment->id); + $reply = $this->postComment(NULL, $this->randomName(), '', TRUE); + $reply_loaded = comment_load($reply->id); + $this->assertTrue($this->commentExists($comment), 'Comment #8. Third reply found.'); + $this->assertEqual($reply_loaded->thread, '02.01/'); + } +} + +/** + * Tests that comments behave correctly when the node is changed. + */ +class CommentNodeChangesTestCase extends CommentHelperCase { + + public static function getInfo() { + return array( + 'name' => 'Comment deletion on node changes', + 'description' => 'Tests that comments behave correctly when the node is changed.', + 'group' => 'Comment', + ); + } + + /** + * Tests that comments are deleted with the node. + */ + function testNodeDeletion() { + $this->drupalLogin($this->web_user); + $comment = $this->postComment($this->node, $this->randomName(), $this->randomName()); + $this->assertTrue(comment_load($comment->id), 'The comment could be loaded.'); + node_delete($this->node->nid); + $this->assertFalse(comment_load($comment->id), 'The comment could not be loaded after the node was deleted.'); + } +} diff --git a/comment/comment.tokens.inc b/comment/comment.tokens.inc new file mode 100644 index 0000000..c495ec3 --- /dev/null +++ b/comment/comment.tokens.inc @@ -0,0 +1,243 @@ + t('Comments'), + 'description' => t('Tokens for comments posted on the site.'), + 'needs-data' => 'comment', + ); + + // Comment-related tokens for nodes + $node['comment-count'] = array( + 'name' => t("Comment count"), + 'description' => t("The number of comments posted on a node."), + ); + $node['comment-count-new'] = array( + 'name' => t("New comment count"), + 'description' => t("The number of comments posted on a node since the reader last viewed it."), + ); + + // Core comment tokens + $comment['cid'] = array( + 'name' => t("Comment ID"), + 'description' => t("The unique ID of the comment."), + ); + $comment['hostname'] = array( + 'name' => t("IP Address"), + 'description' => t("The IP address of the computer the comment was posted from."), + ); + $comment['name'] = array( + 'name' => t("Name"), + 'description' => t("The name left by the comment author."), + ); + $comment['mail'] = array( + 'name' => t("Email address"), + 'description' => t("The email address left by the comment author."), + ); + $comment['homepage'] = array( + 'name' => t("Home page"), + 'description' => t("The home page URL left by the comment author."), + ); + $comment['title'] = array( + 'name' => t("Title"), + 'description' => t("The title of the comment."), + ); + $comment['body'] = array( + 'name' => t("Content"), + 'description' => t("The formatted content of the comment itself."), + ); + $comment['url'] = array( + 'name' => t("URL"), + 'description' => t("The URL of the comment."), + ); + $comment['edit-url'] = array( + 'name' => t("Edit URL"), + 'description' => t("The URL of the comment's edit page."), + ); + + // Chained tokens for comments + $comment['created'] = array( + 'name' => t("Date created"), + 'description' => t("The date the comment was posted."), + 'type' => 'date', + ); + $comment['changed'] = array( + 'name' => t("Date changed"), + 'description' => t("The date the comment was most recently updated."), + 'type' => 'date', + ); + $comment['parent'] = array( + 'name' => t("Parent"), + 'description' => t("The comment's parent, if comment threading is active."), + 'type' => 'comment', + ); + $comment['node'] = array( + 'name' => t("Node"), + 'description' => t("The node the comment was posted to."), + 'type' => 'node', + ); + $comment['author'] = array( + 'name' => t("Author"), + 'description' => t("The author of the comment, if they were logged in."), + 'type' => 'user', + ); + + return array( + 'types' => array('comment' => $type), + 'tokens' => array( + 'node' => $node, + 'comment' => $comment, + ), + ); +} + +/** + * Implements hook_tokens(). + */ +function comment_tokens($type, $tokens, array $data = array(), array $options = array()) { + $url_options = array('absolute' => TRUE); + if (isset($options['language'])) { + $url_options['language'] = $options['language']; + $language_code = $options['language']->language; + } + else { + $language_code = NULL; + } + $sanitize = !empty($options['sanitize']); + + $replacements = array(); + + if ($type == 'comment' && !empty($data['comment'])) { + $comment = $data['comment']; + + foreach ($tokens as $name => $original) { + switch ($name) { + // Simple key values on the comment. + case 'cid': + $replacements[$original] = $comment->cid; + break; + + // Poster identity information for comments + case 'hostname': + $replacements[$original] = $sanitize ? check_plain($comment->hostname) : $comment->hostname; + break; + + case 'name': + $name = ($comment->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $comment->name; + $replacements[$original] = $sanitize ? filter_xss($name) : $name; + break; + + case 'mail': + if ($comment->uid != 0) { + $account = user_load($comment->uid); + $mail = $account->mail; + } + else { + $mail = $comment->mail; + } + $replacements[$original] = $sanitize ? check_plain($mail) : $mail; + break; + + case 'homepage': + $replacements[$original] = $sanitize ? check_url($comment->homepage) : $comment->homepage; + break; + + case 'title': + $replacements[$original] = $sanitize ? filter_xss($comment->subject) : $comment->subject; + break; + + case 'body': + if ($items = field_get_items('comment', $comment, 'comment_body', $language_code)) { + $instance = field_info_instance('comment', 'body', 'comment_body'); + $field_langcode = field_language('comment', $comment, 'comment_body', $language_code); + $replacements[$original] = $sanitize ? _text_sanitize($instance, $field_langcode, $items[0], 'value') : $items[0]['value']; + } + break; + + // Comment related URLs. + case 'url': + $url_options['fragment'] = 'comment-' . $comment->cid; + $replacements[$original] = url('comment/' . $comment->cid, $url_options); + break; + + case 'edit-url': + $url_options['fragment'] = NULL; + $replacements[$original] = url('comment/' . $comment->cid . '/edit', $url_options); + break; + + // Default values for the chained tokens handled below. + case 'author': + $replacements[$original] = $sanitize ? filter_xss($comment->name) : $comment->name; + break; + + case 'parent': + if (!empty($comment->pid)) { + $parent = comment_load($comment->pid); + $replacements[$original] = $sanitize ? filter_xss($parent->subject) : $parent->subject; + } + break; + + case 'created': + $replacements[$original] = format_date($comment->created, 'medium', '', NULL, $language_code); + break; + + case 'changed': + $replacements[$original] = format_date($comment->changed, 'medium', '', NULL, $language_code); + break; + + case 'node': + $node = node_load($comment->nid); + $title = $node->title; + $replacements[$original] = $sanitize ? filter_xss($title) : $title; + break; + } + } + + // Chained token relationships. + if ($node_tokens = token_find_with_prefix($tokens, 'node')) { + $node = node_load($comment->nid); + $replacements += token_generate('node', $node_tokens, array('node' => $node), $options); + } + + if ($date_tokens = token_find_with_prefix($tokens, 'created')) { + $replacements += token_generate('date', $date_tokens, array('date' => $comment->created), $options); + } + + if ($date_tokens = token_find_with_prefix($tokens, 'changed')) { + $replacements += token_generate('date', $date_tokens, array('date' => $comment->changed), $options); + } + + if (($parent_tokens = token_find_with_prefix($tokens, 'parent')) && $parent = comment_load($comment->pid)) { + $replacements += token_generate('comment', $parent_tokens, array('comment' => $parent), $options); + } + + if (($author_tokens = token_find_with_prefix($tokens, 'author')) && $account = user_load($comment->uid)) { + $replacements += token_generate('user', $author_tokens, array('user' => $account), $options); + } + } + elseif ($type == 'node' & !empty($data['node'])) { + $node = $data['node']; + + foreach ($tokens as $name => $original) { + switch($name) { + case 'comment-count': + $replacements[$original] = $node->comment_count; + break; + + case 'comment-count-new': + $replacements[$original] = comment_num_new($node->nid); + break; + } + } + } + + return $replacements; +} diff --git a/comment/comment.tpl.php b/comment/comment.tpl.php new file mode 100644 index 0000000..8298473 --- /dev/null +++ b/comment/comment.tpl.php @@ -0,0 +1,92 @@ +created variable. + * - $changed: Formatted date and time for when the comment was last changed. + * Preprocess functions can reformat it by calling format_date() with the + * desired parameters on the $comment->changed variable. + * - $new: New comment marker. + * - $permalink: Comment permalink. + * - $submitted: Submission information created from $author and $created during + * template_preprocess_comment(). + * - $picture: Authors picture. + * - $signature: Authors signature. + * - $status: Comment status. Possible values are: + * comment-unpublished, comment-published or comment-preview. + * - $title: Linked title. + * - $classes: String of classes that can be used to style contextually through + * CSS. It can be manipulated through the variable $classes_array from + * preprocess functions. The default values can be one or more of the following: + * - comment: The current template type, i.e., "theming hook". + * - comment-by-anonymous: Comment by an unregistered user. + * - comment-by-node-author: Comment by the author of the parent node. + * - comment-preview: When previewing a new or edited comment. + * The following applies only to viewers who are registered users: + * - comment-unpublished: An unpublished comment visible only to administrators. + * - comment-by-viewer: Comment by the user currently viewing the page. + * - comment-new: New comment since last the visit. + * - $title_prefix (array): An array containing additional output populated by + * modules, intended to be displayed in front of the main title tag that + * appears in the template. + * - $title_suffix (array): An array containing additional output populated by + * modules, intended to be displayed after the main title tag that appears in + * the template. + * + * These two variables are provided for context: + * - $comment: Full comment object. + * - $node: Node object the comments are attached to. + * + * Other variables: + * - $classes_array: Array of html class attribute values. It is flattened + * into a string within the variable $classes. + * + * @see template_preprocess() + * @see template_preprocess_comment() + * @see template_process() + * @see theme_comment() + * + * @ingroup themeable + */ +?> +
    > + + + + + + + + > + + + + +
    > + + +
    + +
    + +
    + + +
    diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index 15bfe6f..0718086 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -38,17 +38,29 @@ public function run() $users = user_load_multiple($projectUserIds); $projects = node_load_multiple($projectNodeIds); - + $list_all = ""; foreach ($projects as $project) { $user = $users[(int)$project->uid]; $email = $user->mail; $fullname = $user->field_full_name['und'][0]['value']; $project_name = $project->title; $last_rev = $project->revision_timestamp; - $test = 0; - // Using above four vars, send email to myself. + //Used only for testing + $list_all = $list_all."Email: ".$email." | Info: ".$fullname."'s ".$project_name.PHP_EOL; + + //Queue up items - WIP + /* + $queue = new stdClass(); + $queue->uid = $uid; + $queue->mail_type = 'custom_project_notification_email'; + $queue->tokens = serialize($tokens); + $queue->created = REQUEST_TIME; + $queue->sent = null; + drupal_write_record( 'custom_notify_email_queue', $queue); + */ } + send_mail($list_all); } } diff --git a/sites/all/modules/custom/custom.module b/sites/all/modules/custom/custom.module index d704c1e..64404c0 100644 --- a/sites/all/modules/custom/custom.module +++ b/sites/all/modules/custom/custom.module @@ -282,17 +282,17 @@ function _custom_notification_query($neighborhoods, $project_types, $uid) { if (count($neighborhoods) > 0 && count($project_types) > 0) { # both neighborhoods and project types $query_sql = " -select +select -- user u.uid, u.name, u.mail, prefs.field_project_notification_prefs_value as notification_prefs, -- project node - n.nid, n.title, + n.nid, n.title, -- neighborhood - ti_nh.tid as tid_nh, ttd_nh.name as neighborhood, + ti_nh.tid as tid_nh, ttd_nh.name as neighborhood, -- project type ti_pt.tid as tid_pt, ttd_pt.name as project_type -- * -from +from {node} n inner join {users} u @@ -305,25 +305,25 @@ from inner join {taxonomy_index} ti_nh on ti_nh.nid = n.nid - inner join + inner join {taxonomy_term_data} ttd_nh on ti_nh.tid = ttd_nh.tid and ttd_nh.vid = :nh_vid -- project type inner join {taxonomy_index} ti_pt on ti_pt.nid = n.nid - inner join + inner join {taxonomy_term_data} ttd_pt on ti_pt.tid = ttd_pt.tid and ttd_pt.vid = :pt_vid where - prefs.field_project_notification_prefs_value > 0 + prefs.field_project_notification_prefs_value > 0 and u.uid != :uid and n.type = 'project' -and +and n.status = 1 -and +and ( ti_nh.tid in(:nh_tids) or ti_pt.tid in(:pt_tids))"; @@ -338,15 +338,15 @@ or } elseif (count($project_types > 0) ) { # just project types - $query_sql = "select + $query_sql = "select -- user u.uid, u.name, u.mail, prefs.field_project_notification_prefs_value as notification_prefs, -- project node - n.nid, n.title, + n.nid, n.title, -- project type ti_pt.tid as tid_pt, ttd_pt.name as project_type -- * -from +from {node} n inner join {users} u @@ -359,18 +359,18 @@ from inner join {taxonomy_index} ti_pt on ti_pt.nid = n.nid - inner join + inner join {taxonomy_term_data} ttd_pt on ti_pt.tid = ttd_pt.tid and ttd_pt.vid = :pt_vid where - prefs.field_project_notification_prefs_value > 0 + prefs.field_project_notification_prefs_value > 0 and u.uid != :uid and n.type = 'project' -and +and n.status = 1 -and +and ti_pt.tid in(:pt_tids)"; $db = db_query($query_sql, array( ':pt_vid' => $project_type_vid, @@ -448,3 +448,45 @@ function custom_cron() $postContentEmailService = new PostContentEmailService(); $postContentEmailService->run(); } + + +function send_mail($content) +{ + $to = "test@localhost.com"; + $subject = "Test Email"; + $headers = "From postmaster@local"; + $result = mail($to, $subject, $content.PHP_EOL, $headers); + if(result){ + echo("Success!"); + } else { + echo("Failure!"); + } +} + +function custom_drupal_mail($from = 'default_from', $to, $subject, $message) { + $my_module = 'custom'; + $my_mail_token = microtime(); + if ($from == 'default_from') { + // Change this to your own default 'from' email address. + $from = variable_get('system_mail', 'My Email Address '); + } + $message = array( + 'id' => $my_module . '_' . $my_mail_token, + 'to' => $to, + 'subject' => $subject, + 'body' => array($message), + 'headers' => array( + 'From' => $from, + 'Sender' => $from, + 'Return-Path' => $from, + ), + ); + $system = drupal_mail_system($my_module, $my_mail_token); + $message = $system->format($message); + if ($system->mail($message)) { + return TRUE; + } + else { + return FALSE; + } +} From eaa6c87a51636384a8de39613fdd2d90952fa7e5 Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 17 Jul 2017 19:54:13 -0500 Subject: [PATCH 05/11] Preparing to pull --- sites/all/modules/custom/class/PostContentEmailService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index 0718086..dda9871 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -59,7 +59,6 @@ public function run() $queue->sent = null; drupal_write_record( 'custom_notify_email_queue', $queue); */ - } send_mail($list_all); } From 05babba4693c905549c0cbf934363ed919deebc7 Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 7 Aug 2017 18:38:59 -0500 Subject: [PATCH 06/11] Working version - limited to just 10 and the email content is WIP --- .../custom/class/PostContentEmailService.php | 38 +++++++++++++------ sites/all/modules/custom/custom.module | 14 +++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index dda9871..15e1818 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -38,7 +38,7 @@ public function run() $users = user_load_multiple($projectUserIds); $projects = node_load_multiple($projectNodeIds); - $list_all = ""; + foreach ($projects as $project) { $user = $users[(int)$project->uid]; $email = $user->mail; @@ -46,20 +46,36 @@ public function run() $project_name = $project->title; $last_rev = $project->revision_timestamp; - //Used only for testing - $list_all = $list_all."Email: ".$email." | Info: ".$fullname."'s ".$project_name.PHP_EOL; + $token_array = array( + 'user' => $user, + 'email' => $email, + 'fullname' => $fullname, + 'project_name' => $project_name, + 'last_rev' => $last_rev, + ); + + //Queue up items - //Queue up items - WIP - /* $queue = new stdClass(); - $queue->uid = $uid; + $queue->uid = (int)$project->uid; $queue->mail_type = 'custom_project_notification_email'; - $queue->tokens = serialize($tokens); + $queue->tokens = serialize($token_array); $queue->created = REQUEST_TIME; $queue->sent = null; - drupal_write_record( 'custom_notify_email_queue', $queue); - */ - send_mail($list_all); - } + drupal_write_record( 'custom_notify_email_queue', $queue); // Move this out of the day + } + $email_query = "SELECT * FROM {custom_notify_email_queue} WHERE sent IS NULL LIMIT 10"; + $records = db_query($email_query)->fetchAll(); + + foreach($records as $record){ + //send email... + $tokens = unserialize($record->tokens); + $content = "Dear ".$tokens['fullname'].", your project, ".$tokens['project_name'].", was last updated ".date("m/d/Y", $tokens['last_rev']); + send_mail($content); + + //update database + update_enque($record->queue_id); + } + } } diff --git a/sites/all/modules/custom/custom.module b/sites/all/modules/custom/custom.module index 1fc3c2e..2d8c4bf 100644 --- a/sites/all/modules/custom/custom.module +++ b/sites/all/modules/custom/custom.module @@ -456,11 +456,14 @@ function send_mail($content) $subject = "Test Email"; $headers = "From postmaster@local"; $result = mail($to, $subject, $content.PHP_EOL, $headers); + + /* if(result){ echo("Success!"); } else { echo("Failure!"); } + */ } function custom_drupal_mail($from = 'default_from', $to, $subject, $message) { @@ -489,6 +492,17 @@ function custom_drupal_mail($from = 'default_from', $to, $subject, $message) { else { return FALSE; } +} + +function update_enque($id) +{ + db_update('custom_notify_email_queue') // Table name no longer needs {} + ->fields(array( + 'sent' => REQUEST_TIME, + )) + ->condition('queue_id', $id, '=') + ->execute(); +} function custom_enqueue_stale_project_notification() { From f6ee78262183592a32023aed0465f7c31d1dc139 Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 21 Aug 2017 18:42:49 -0500 Subject: [PATCH 07/11] Able to open template - preparing to send email with it --- .../custom/class/PostContentEmailService.php | 32 +++++++++++++---- sites/all/modules/custom/custom.module | 36 ++++++++++--------- .../email_templates/SixMonthProjectEmail.html | 16 +++++++++ 3 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 sites/all/modules/custom/email_templates/SixMonthProjectEmail.html diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index 15e1818..caa94a9 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -40,6 +40,7 @@ public function run() foreach ($projects as $project) { + //_custom_sage_get_field $user = $users[(int)$project->uid]; $email = $user->mail; $fullname = $user->field_full_name['und'][0]['value']; @@ -50,6 +51,7 @@ public function run() 'user' => $user, 'email' => $email, 'fullname' => $fullname, + 'project_nid' => $project->nid, 'project_name' => $project_name, 'last_rev' => $last_rev, ); @@ -59,7 +61,7 @@ public function run() $queue = new stdClass(); $queue->uid = (int)$project->uid; $queue->mail_type = 'custom_project_notification_email'; - $queue->tokens = serialize($token_array); + $queue->tokens = json_encode($token_array); $queue->created = REQUEST_TIME; $queue->sent = null; drupal_write_record( 'custom_notify_email_queue', $queue); // Move this out of the day @@ -70,12 +72,30 @@ public function run() foreach($records as $record){ //send email... - $tokens = unserialize($record->tokens); - $content = "Dear ".$tokens['fullname'].", your project, ".$tokens['project_name'].", was last updated ".date("m/d/Y", $tokens['last_rev']); - send_mail($content); + $tokens = json_decode($record->tokens); + $content = "Hi, ".$tokens->fullname.", your project, ".$tokens->project_name.", was last updated ".date("m/d/Y", $tokens->last_rev); + + $subject = "Custom Drupal Mail"; + $to = "test@localhost.com"; + // $to = $tokens->email + $from = "CommunityKC"; + custom_drupal_mail($from, $to, $subject, $content); + + //enque table + update_node_enque($record->queue_id); + //update ccf_node's rev date to today + //update_node_ccf_node($tokens->project_nid); - //update database - update_enque($record->queue_id); } + + $root_dir = DRUPAL_ROOT; + $template_path = $root_dir."/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html"; + //$template = file_get_contents("C:/xampp/htdocs/CommunityKC/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html"); + $template = file_get_contents($template_path); + $replace = t($template, array('!project_name' => $tokens->project_name)); + } } + + + diff --git a/sites/all/modules/custom/custom.module b/sites/all/modules/custom/custom.module index 2d8c4bf..5483beb 100644 --- a/sites/all/modules/custom/custom.module +++ b/sites/all/modules/custom/custom.module @@ -466,45 +466,49 @@ function send_mail($content) */ } -function custom_drupal_mail($from = 'default_from', $to, $subject, $message) { +function custom_drupal_mail($from, $to, $subject, $message) { $my_module = 'custom'; $my_mail_token = microtime(); - if ($from == 'default_from') { - // Change this to your own default 'from' email address. - $from = variable_get('system_mail', 'My Email Address '); - } + + // Message is going to be overwritten $message = array( 'id' => $my_module . '_' . $my_mail_token, 'to' => $to, 'subject' => $subject, - 'body' => array($message), + 'body' => array($message), // Maybe just $message? 'headers' => array( 'From' => $from, 'Sender' => $from, 'Return-Path' => $from, ), ); + + // It does magic $system = drupal_mail_system($my_module, $my_mail_token); + + // Translate? Does something $message = $system->format($message); - if ($system->mail($message)) { - return TRUE; - } - else { - return FALSE; - } + + // Send message + return $system->mail($message); } -function update_enque($id) +function update_node_enque($que_id) { db_update('custom_notify_email_queue') // Table name no longer needs {} ->fields(array( 'sent' => REQUEST_TIME, )) - ->condition('queue_id', $id, '=') + ->condition('queue_id', $que_id, '=') ->execute(); } -function custom_enqueue_stale_project_notification() +function update_node_ccf_node($nid) { - // ... make args and do code ... + db_update('ccf') // Table name no longer needs {} + ->fields(array( + 'changed' => REQUEST_TIME, + )) + ->condition('nid', $nid, '=') + ->execute(); } diff --git a/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html b/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html new file mode 100644 index 0000000..a65c4ac --- /dev/null +++ b/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html @@ -0,0 +1,16 @@ +

    Greetings from CommunityKC.org, Kansas City's interactive directory of neighborhood projects!

    + +

    We have you listed as a primary contact person for !project_name. It has been 1 year since [Project Name] was published on the site, and it's probably time to look it over to make sure everything is still accurate.

    + +

    Simply click the link below, login and make any necessary changes. We will then review your changes and publish them to the site within a few days.

    + + + +

    Did you forget your password? You'll need that to edit your project. Click here to reset it.

    + +

    Have questions? Having trouble? Send a message to info@communitykc.org and we will try our best to help you out.

    + +

    Thanks for all the hard work you do to make life in KC better.

    + +

    The team at CommmunityKC.org

    +

    Connecting projects, people and resources

    From 84176694c850d1b05a3ce2df0123c30e866594b7 Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 21 Aug 2017 19:46:24 -0500 Subject: [PATCH 08/11] Not really working - need to fix mail system --- .../custom/class/PostContentEmailService.php | 49 ++++++++++++++++--- sites/all/modules/custom/custom.module | 5 ++ .../email_templates/SixMonthProjectEmail.html | 2 +- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index caa94a9..5b5a7fb 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -73,12 +73,19 @@ public function run() foreach($records as $record){ //send email... $tokens = json_decode($record->tokens); - $content = "Hi, ".$tokens->fullname.", your project, ".$tokens->project_name.", was last updated ".date("m/d/Y", $tokens->last_rev); + //$content = "Hi, ".$tokens->fullname.", your project, ".$tokens->project_name.", was last updated ".date("m/d/Y", $tokens->last_rev); $subject = "Custom Drupal Mail"; $to = "test@localhost.com"; // $to = $tokens->email $from = "CommunityKC"; + + $root_dir = DRUPAL_ROOT; + $template_path = $root_dir."/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html"; + + $template = file_get_contents($template_path); + $content = t($template, array('!project_name' => $tokens->project_name)); + custom_drupal_mail($from, $to, $subject, $content); //enque table @@ -88,12 +95,42 @@ public function run() } - $root_dir = DRUPAL_ROOT; - $template_path = $root_dir."/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html"; - //$template = file_get_contents("C:/xampp/htdocs/CommunityKC/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html"); - $template = file_get_contents($template_path); - $replace = t($template, array('!project_name' => $tokens->project_name)); + /* + // Multiple recipients + + // Subject + $subject = 'Birthday Reminders for August'; + + // Message + $message = ' + + + Birthday Reminders for August + + +

    Here are the birthdays upcoming in August!

    + + + + + + + + + + +
    PersonDayMonthYear
    Johny10thAugust1970
    Sally17thAugust1973
    + + + '; + $headers = "From postmaster@local\r\n"; + $headers .= "MIME-Version: 1.0\r\n"; + $headers .= "Content-Type: text/html; charset=ISO-8859-1\r\n"; + // Mail it + $result = mail($to, $subject, $message, $headers); + echo $result; + */ } } diff --git a/sites/all/modules/custom/custom.module b/sites/all/modules/custom/custom.module index 5483beb..ae67276 100644 --- a/sites/all/modules/custom/custom.module +++ b/sites/all/modules/custom/custom.module @@ -470,6 +470,8 @@ function custom_drupal_mail($from, $to, $subject, $message) { $my_module = 'custom'; $my_mail_token = microtime(); + $original = $message; + // Message is going to be overwritten $message = array( 'id' => $my_module . '_' . $my_mail_token, @@ -480,6 +482,8 @@ function custom_drupal_mail($from, $to, $subject, $message) { 'From' => $from, 'Sender' => $from, 'Return-Path' => $from, + 'MIME-Version' => '1.0', + 'Content-type' => 'text/html; charset=iso-8859-1', ), ); @@ -491,6 +495,7 @@ function custom_drupal_mail($from, $to, $subject, $message) { // Send message return $system->mail($message); + // return $system->mail($to, $subject, $original, "From postmaster@local"); } function update_node_enque($que_id) diff --git a/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html b/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html index a65c4ac..8d74617 100644 --- a/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html +++ b/sites/all/modules/custom/email_templates/SixMonthProjectEmail.html @@ -1,6 +1,6 @@

    Greetings from CommunityKC.org, Kansas City's interactive directory of neighborhood projects!

    -

    We have you listed as a primary contact person for !project_name. It has been 1 year since [Project Name] was published on the site, and it's probably time to look it over to make sure everything is still accurate.

    +

    We have you listed as a primary contact person for !project_name. It has been 1 year since !project_name was published on the site, and it's probably time to look it over to make sure everything is still accurate.

    Simply click the link below, login and make any necessary changes. We will then review your changes and publish them to the site within a few days.

    From c6e694b7c96df508c505a26cc65a856263173926 Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 28 Aug 2017 19:35:24 -0500 Subject: [PATCH 09/11] Attempting to implement new mail interface --- .../modules/custom/class/HTMLMailSystem.php | 55 +++++++++++++++++++ .../custom/class/PostContentEmailService.php | 38 +------------ sites/all/modules/custom/custom.module | 4 +- 3 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 sites/all/modules/custom/class/HTMLMailSystem.php diff --git a/sites/all/modules/custom/class/HTMLMailSystem.php b/sites/all/modules/custom/class/HTMLMailSystem.php new file mode 100644 index 0000000..c70dd1b --- /dev/null +++ b/sites/all/modules/custom/class/HTMLMailSystem.php @@ -0,0 +1,55 @@ + $value) { + $mimeheaders[] = $name . ': ' . mime_header_encode($value); + } + $line_endings = variable_get('mail_line_endings', MAIL_LINE_ENDINGS); + return mail( + $message['to'], + mime_header_encode($message['subject']), + // Note: e-mail uses CRLF for line-endings. PHP's API requires LF + // on Unix and CRLF on Windows. Drupal automatically guesses the + // line-ending format appropriate for your system. If you need to + // override this, adjust $conf['mail_line_endings'] in settings.php. + preg_replace('@\r?\n@', $line_endings, $message['body']), + // For headers, PHP's API suggests that we use CRLF normally, + // but some MTAs incorrectly replace LF with CRLF. See #234403. + implode("\n", $mimeheaders) + ); + } +} diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index 5b5a7fb..2bda22c 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -11,6 +11,7 @@ public function __construct() public function run() { + variable_set('mail_system', array('default-system' => 'HTMLMailSystem')); $sql_query = "SELECT n.nid, n.uid @@ -94,43 +95,6 @@ public function run() //update_node_ccf_node($tokens->project_nid); } - - - /* - // Multiple recipients - - // Subject - $subject = 'Birthday Reminders for August'; - - // Message - $message = ' - - - Birthday Reminders for August - - -

    Here are the birthdays upcoming in August!

    - - - - - - - - - - -
    PersonDayMonthYear
    Johny10thAugust1970
    Sally17thAugust1973
    - - - '; - $headers = "From postmaster@local\r\n"; - $headers .= "MIME-Version: 1.0\r\n"; - $headers .= "Content-Type: text/html; charset=ISO-8859-1\r\n"; - // Mail it - $result = mail($to, $subject, $message, $headers); - echo $result; - */ } } diff --git a/sites/all/modules/custom/custom.module b/sites/all/modules/custom/custom.module index ae67276..50aa8fa 100644 --- a/sites/all/modules/custom/custom.module +++ b/sites/all/modules/custom/custom.module @@ -3,6 +3,8 @@ require_once('custom.user.inc'); require_once('custom.helpers.inc'); +require_once('class/HTMLMailSystem.php'); + require_once('class/GeoJsonService.php'); require_once('class/PostContentEmailService.php'); @@ -477,7 +479,7 @@ function custom_drupal_mail($from, $to, $subject, $message) { 'id' => $my_module . '_' . $my_mail_token, 'to' => $to, 'subject' => $subject, - 'body' => array($message), // Maybe just $message? + 'body' => array($message), 'headers' => array( 'From' => $from, 'Sender' => $from, From be693c1e2e7b391137547e419038123ca5253e17 Mon Sep 17 00:00:00 2001 From: Noah-Dev Date: Mon, 11 Sep 2017 18:42:36 -0500 Subject: [PATCH 10/11] Preparing to add new module --- .../custom/class/PostContentEmailService.php | 2 +- sites/all/modules/custom/custom.module | 2 +- sites/all/modules/htmlmail/CHANGELOG.html | 74 ++++ sites/all/modules/htmlmail/CHANGELOG.markdown | 71 ++++ sites/all/modules/htmlmail/CHANGELOG.txt | 107 ++++++ sites/all/modules/htmlmail/LICENSE.txt | 339 ++++++++++++++++++ sites/all/modules/htmlmail/README.html | 183 ++++++++++ sites/all/modules/htmlmail/README.markdown | 216 +++++++++++ sites/all/modules/htmlmail/README.txt | 235 ++++++++++++ .../htmlmail/htmlmail--htmlmail.tpl.php | 32 ++ .../htmlmail/htmlmail--simplenews.tpl.php | 144 ++++++++ .../htmlmail--user--password_reset.tpl.php | 112 ++++++ sites/all/modules/htmlmail/htmlmail.admin.inc | 314 ++++++++++++++++ sites/all/modules/htmlmail/htmlmail.info | 15 + sites/all/modules/htmlmail/htmlmail.install | 112 ++++++ sites/all/modules/htmlmail/htmlmail.mail.inc | 322 +++++++++++++++++ sites/all/modules/htmlmail/htmlmail.markdown | 92 +++++ sites/all/modules/htmlmail/htmlmail.module | 233 ++++++++++++ sites/all/modules/htmlmail/htmlmail.tpl.php | 178 +++++++++ 19 files changed, 2781 insertions(+), 2 deletions(-) create mode 100644 sites/all/modules/htmlmail/CHANGELOG.html create mode 100644 sites/all/modules/htmlmail/CHANGELOG.markdown create mode 100644 sites/all/modules/htmlmail/CHANGELOG.txt create mode 100644 sites/all/modules/htmlmail/LICENSE.txt create mode 100644 sites/all/modules/htmlmail/README.html create mode 100644 sites/all/modules/htmlmail/README.markdown create mode 100644 sites/all/modules/htmlmail/README.txt create mode 100644 sites/all/modules/htmlmail/htmlmail--htmlmail.tpl.php create mode 100644 sites/all/modules/htmlmail/htmlmail--simplenews.tpl.php create mode 100644 sites/all/modules/htmlmail/htmlmail--user--password_reset.tpl.php create mode 100644 sites/all/modules/htmlmail/htmlmail.admin.inc create mode 100644 sites/all/modules/htmlmail/htmlmail.info create mode 100644 sites/all/modules/htmlmail/htmlmail.install create mode 100644 sites/all/modules/htmlmail/htmlmail.mail.inc create mode 100644 sites/all/modules/htmlmail/htmlmail.markdown create mode 100644 sites/all/modules/htmlmail/htmlmail.module create mode 100644 sites/all/modules/htmlmail/htmlmail.tpl.php diff --git a/sites/all/modules/custom/class/PostContentEmailService.php b/sites/all/modules/custom/class/PostContentEmailService.php index 2bda22c..2e9dc8e 100644 --- a/sites/all/modules/custom/class/PostContentEmailService.php +++ b/sites/all/modules/custom/class/PostContentEmailService.php @@ -11,7 +11,7 @@ public function __construct() public function run() { - variable_set('mail_system', array('default-system' => 'HTMLMailSystem')); + variable_set('mail_system', array('html_mail_system' => 'HTMLMailSystem')); $sql_query = "SELECT n.nid, n.uid diff --git a/sites/all/modules/custom/custom.module b/sites/all/modules/custom/custom.module index 50aa8fa..bedb178 100644 --- a/sites/all/modules/custom/custom.module +++ b/sites/all/modules/custom/custom.module @@ -470,7 +470,7 @@ function send_mail($content) function custom_drupal_mail($from, $to, $subject, $message) { $my_module = 'custom'; - $my_mail_token = microtime(); + $my_mail_token = 'html_mail_system'; $original = $message; diff --git a/sites/all/modules/htmlmail/CHANGELOG.html b/sites/all/modules/htmlmail/CHANGELOG.html new file mode 100644 index 0000000..3022427 --- /dev/null +++ b/sites/all/modules/htmlmail/CHANGELOG.html @@ -0,0 +1,74 @@ +

    Change Log Summary

    +

    -- (Note: See the git repository for full version)

    +
    +
    November 4, 2008
    +
    +

    HTML Mail created by Chris Herberte.

    +
    +
    July 19, 2009
    +
    +

    5.x-1.1 released (288 SLOC, 60k) as an improved replacement for drupal_mail().

    +
    +
    February 26, 2010
    +
    +

    6.x-1.3 released (348 SLOC, 108k) and eventually installed by over 5,000 sites.

    +
    +
    January 1, 2011
    +
    +

    Patches to improve theming of the 7.x-1.x-dev version submitted by Bob Vincent (pillarsdotnet).

    +
    +
    March 10, 2011
    +
    +

    Mail System created to allow mail-sending modules to cooperate and co-exist.

    +
    +
    March 11, 2011
    +
    +

    Bob Vincent granted co-maintainer access.

    +
    +
    +

    7.x-1.1 released (589 SLOC, 76k).

    +
    +
    March 16, 2011
    +
    +

    Emogrifier separated into its own module.

    +
    +
    March 18, 2011
    +
    +

    7.x-2.x and 6.x-2.x branches created.

    +
    +
    March 20, 2011
    +
    +

    Echo separated into its own module.

    +
    +
    March 25, 2011
    +
    +

    MIME capability added.

    +
    +
    March 26, 2011
    +
    +

    MIME support delegated to PEAR Mail_mimePart and Mail_mimeDecode classes.

    +
    +
    April 3, 2011
    +
    +

    Mime support separated into its own module.

    +
    +
    April 4, 2011
    +
    +

    7.x-2.x changes backported to 6.x-2.x.

    +
    +
    April 6, 2011
    +
    +

    6.x-2.4 (420 SLOC, 92k) and 7.x-2.4 (414 SLOC, 92k) released.

    +
    +
    +

    Project page updated to remove support for 6.x-1.x and 7.x-1.x branches.

    +
    +
    April 7, 2011
    +
    +

    Dependency on Echo and Mail MIME modules removed.

    +
    +
    April 9, 2011
    +
    +

    Autodetection of template files in both module and selected theme directories.

    +
    +
    diff --git a/sites/all/modules/htmlmail/CHANGELOG.markdown b/sites/all/modules/htmlmail/CHANGELOG.markdown new file mode 100644 index 0000000..f02f315 --- /dev/null +++ b/sites/all/modules/htmlmail/CHANGELOG.markdown @@ -0,0 +1,71 @@ +### Change Log Summary + +-- *(Note: See the [git repository](http://drupalcode.org/project/htmlmail.git/log/refs/heads/7.x-2.x) for full version)* + +November 4, 2008 + +: [HTML Mail created](http://drupalcode.org/project/htmlmail.git/commit/64a69aff375ffe42d311963d748866085281896e) by [Chris Herberte](http://drupal.org/user/1171). + +July 19, 2009 + +: [5.x-1.1](http://drupal.org/node/524718) released *(288 [SLOC](http://www.dwheeler.com/sloccount/), 60k)* as an improved replacement for [`drupal_mail()`](http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_mail/5). + +February 26, 2010 + +: [6.x-1.3](http://drupal.org/node/726398) released *(348 [SLOC](http://www.dwheeler.com/sloccount/), 108k)* and eventually installed by [over 5,000 sites](http://drupal.org/project/usage/726398). + +January 1, 2011 + +: [Patches to improve theming](http://drupal.org/node/1012246) of the [7.x-1.x-dev version](http://drupal.org/node/355250) submitted by [Bob Vincent (pillarsdotnet)](http://drupal.org/user/36148). + +March 10, 2011 + +: [Mail System created](http://drupalcode.org/project/mailsystem.git/commit/5cc8201c5e48b56efecc139c9a51dd49775aebaf) to allow mail-sending modules to cooperate and co-exist. + +March 11, 2011 + +: [Bob Vincent](http://drupal.org/user/36148) granted co-maintainer access. + +: [7.x-1.1](http://drupal.org/node/1088882) released *(589 [SLOC](http://www.dwheeler.com/sloccount/), 76k)*. + +March 16, 2011 + +: [Emogrifier separated](http://drupalcode.org/project/htmlmail.git/commit/a0083eea7b575a702d3aecdd0578378277c7c8d4) into [its own module](http://drupal.org/project/emogrifier). + +March 18, 2011 + +: [7.x-2.x](http://drupalcode.org/project/htmlmail.git/commit/6c0463849493f8b528be8d4099ab0c6fbc976fe2) and [6.x-2.x](http://drupalcode.org/project/htmlmail.git/commit/a4b36c6de5f241ceccd0aeea4599d35de066fa9c) branches created. + +March 20, 2011 + +: [Echo separated](http://drupalcode.org/project/htmlmail.git/commit/f481fc8997a1345e9490e80043c616a5805d6e44) into [its own module](http://drupal.org/project/echo). + +March 25, 2011 + +: [MIME capability added](http://drupalcode.org/project/htmlmail.git/commit/0ebec6e83e688b6d51e35554618727dc3133c970). + +March 26, 2011 + +: [MIME support delegated](http://drupalcode.org/project/htmlmail.git/commit/104e8916c9ba92486a227786b6781cac38e60905) to [PEAR](http://pear.php.net) [Mail_mimePart](http://pear.php.net/manual/en/package.mail.mail-mimepart.mail-mimepart.php) and [Mail_mimeDecode](http://pear.php.net/manual/en/package.mail.mail-mimedecode.php) classes. + +April 3, 2011 + +: [Mime support separated](http://drupalcode.org/project/htmlmail.git/commit/a17e7996d0d119012205cf47195064848e59d937) into [its own module](http://drupal.org/project/mailmime). + +April 4, 2011 + +: [7.x-2.x changes backported to 6.x-2.x](http://drupalcode.org/project/htmlmail.git/commit/f419fb3cf18276cca9f0d3dbc2c80e6e6a6bbda9). + +April 6, 2011 + +: [6.x-2.4](http://drupal.org/node/1118032) *(420 [SLOC](http://www.dwheeler.com/sloccount), 92k)* and [7.x-2.4](http://drupal.org/node/1118034) *(414 [SLOC](http://www.dwheeler.com/sloccount), 92k)* released. + +: [Project page](http://drupal.org/project/htmlmail) updated to remove support for [6.x-1.x](http://drupalcode.org/project/htmlmail.git/shortlog/refs/heads/6.x-1.x) and [7.x-1.x](http://drupalcode.org/project/htmlmail.git/shortlog/refs/heads/7.x-1.x) branches. + +April 7, 2011 + +: Dependency on [Echo](http://drupal.org/project/echo) and [Mail MIME](http://drupal.org/project/mailmime) modules [removed](http://drupalcode.org/project/htmlmail.git/commit/20abfd24ad0006c9312fd8f0a5edcdaed8e5920b). + +April 9, 2011 + +: [Autodetection of template files](http://drupalcode.org/project/htmlmail.git/commit/dd53d4f28cdbe893bb32e31a03ceba34c5240402) in both module and selected theme directories. diff --git a/sites/all/modules/htmlmail/CHANGELOG.txt b/sites/all/modules/htmlmail/CHANGELOG.txt new file mode 100644 index 0000000..3fea1bb --- /dev/null +++ b/sites/all/modules/htmlmail/CHANGELOG.txt @@ -0,0 +1,107 @@ + Change Log Summary + + -- (Note: See the [1]git repository for full version) + + November 4, 2008 + [2]HTML Mail created by [3]Chris Herberte. + + July 19, 2009 + [4]5.x-1.1 released (288 [5]SLOC, 60k) as an improved + replacement for [6]drupal_mail(). + + February 26, 2010 + [7]6.x-1.3 released (348 [8]SLOC, 108k) and eventually installed + by [9]over 5,000 sites. + + January 1, 2011 + [10]Patches to improve theming of the [11]7.x-1.x-dev version + submitted by [12]Bob Vincent (pillarsdotnet). + + March 10, 2011 + [13]Mail System created to allow mail-sending modules to + cooperate and co-exist. + + March 11, 2011 + [14]Bob Vincent granted co-maintainer access. + + [15]7.x-1.1 released (589 [16]SLOC, 76k). + + March 16, 2011 + [17]Emogrifier separated into [18]its own module. + + March 18, 2011 + [19]7.x-2.x and [20]6.x-2.x branches created. + + March 20, 2011 + [21]Echo separated into [22]its own module. + + March 25, 2011 + [23]MIME capability added. + + March 26, 2011 + [24]MIME support delegated to [25]PEAR [26]Mail_mimePart and + [27]Mail_mimeDecode classes. + + April 3, 2011 + [28]Mime support separated into [29]its own module. + + April 4, 2011 + [30]7.x-2.x changes backported to 6.x-2.x. + + April 6, 2011 + [31]6.x-2.4 (420 [32]SLOC, 92k) and [33]7.x-2.4 (414 [34]SLOC, + 92k) released. + + [35]Project page updated to remove support for [36]6.x-1.x and + [37]7.x-1.x branches. + + April 7, 2011 + Dependency on [38]Echo and [39]Mail MIME modules [40]removed. + + April 9, 2011 + [41]Autodetection of template files in both module and selected + theme directories. + +References + + 1. http://drupalcode.org/project/htmlmail.git/log/refs/heads/7.x-2.x + 2. http://drupalcode.org/project/htmlmail.git/commit/64a69aff375ffe42d311963d748866085281896e + 3. http://drupal.org/user/1171 + 4. http://drupal.org/node/524718 + 5. http://www.dwheeler.com/sloccount/ + 6. http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_mail/5 + 7. http://drupal.org/node/726398 + 8. http://www.dwheeler.com/sloccount/ + 9. http://drupal.org/project/usage/726398 + 10. http://drupal.org/node/1012246 + 11. http://drupal.org/node/355250 + 12. http://drupal.org/user/36148 + 13. http://drupalcode.org/project/mailsystem.git/commit/5cc8201c5e48b56efecc139c9a51dd49775aebaf + 14. http://drupal.org/user/36148 + 15. http://drupal.org/node/1088882 + 16. http://www.dwheeler.com/sloccount/ + 17. http://drupalcode.org/project/htmlmail.git/commit/a0083eea7b575a702d3aecdd0578378277c7c8d4 + 18. http://drupal.org/project/emogrifier + 19. http://drupalcode.org/project/htmlmail.git/commit/6c0463849493f8b528be8d4099ab0c6fbc976fe2 + 20. http://drupalcode.org/project/htmlmail.git/commit/a4b36c6de5f241ceccd0aeea4599d35de066fa9c + 21. http://drupalcode.org/project/htmlmail.git/commit/f481fc8997a1345e9490e80043c616a5805d6e44 + 22. http://drupal.org/project/echo + 23. http://drupalcode.org/project/htmlmail.git/commit/0ebec6e83e688b6d51e35554618727dc3133c970 + 24. http://drupalcode.org/project/htmlmail.git/commit/104e8916c9ba92486a227786b6781cac38e60905 + 25. http://pear.php.net/ + 26. http://pear.php.net/manual/en/package.mail.mail-mimepart.mail-mimepart.php + 27. http://pear.php.net/manual/en/package.mail.mail-mimedecode.php + 28. http://drupalcode.org/project/htmlmail.git/commit/a17e7996d0d119012205cf47195064848e59d937 + 29. http://drupal.org/project/mailmime + 30. http://drupalcode.org/project/htmlmail.git/commit/f419fb3cf18276cca9f0d3dbc2c80e6e6a6bbda9 + 31. http://drupal.org/node/1118032 + 32. http://www.dwheeler.com/sloccount + 33. http://drupal.org/node/1118034 + 34. http://www.dwheeler.com/sloccount + 35. http://drupal.org/project/htmlmail + 36. http://drupalcode.org/project/htmlmail.git/shortlog/refs/heads/6.x-1.x + 37. http://drupalcode.org/project/htmlmail.git/shortlog/refs/heads/7.x-1.x + 38. http://drupal.org/project/echo + 39. http://drupal.org/project/mailmime + 40. http://drupalcode.org/project/htmlmail.git/commit/20abfd24ad0006c9312fd8f0a5edcdaed8e5920b + 41. http://drupalcode.org/project/htmlmail.git/commit/dd53d4f28cdbe893bb32e31a03ceba34c5240402 diff --git a/sites/all/modules/htmlmail/LICENSE.txt b/sites/all/modules/htmlmail/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/htmlmail/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/htmlmail/README.html b/sites/all/modules/htmlmail/README.html new file mode 100644 index 0000000..479c8e9 --- /dev/null +++ b/sites/all/modules/htmlmail/README.html @@ -0,0 +1,183 @@ +

    HTML Mail

    +

    Lets you theme your messages the same way you theme the rest of your website.

    +

    Requirement

    + +

    Installation

    +

    The following additional modules, while not required, are highly recommended:

    +
      +
    • +
      +
      Echo
      +
      Wraps your messages in a drupal theme. Now you can "brand" your messages with the same logo, header, fonts, and styles as your website.
      +
      +
    • +
    • +
      +
      Emogrifier
      +
      Converts stylesheets to inline style rules, for consistent display on mobile devices and webmail.
      +
      +
    • +
    • +
      +
      Mail MIME
      +
      Provides a text/plain alternative to text/html emails, and automatically converts image references to inline image attachments.
      +
      +
    • +
    • +
      +
      Pathologic
      +
      Converts urls from relative to absolute, so clickable links in your email messages work as intended.
      +
      +
    • +
    • +
      +
      Transliteration
      +
      +

      Converts non-ASCII characters to their US-ASCII equivalents, such as from Microsoft "smart-quotes" to regular quotes.

      +
      +
      +

      Also available as a patch.

      +
      +
      +
    • +
    +

    Updating from previous versions

    +

    The 7.x-2.x branch shares 94% of its code with the 6.x-2.x branch, but only 15% of its code with the 7.x-1.x branch, and a tiny 8% of its code with the 6.x-1.x branch.

    +

    Let your compatibility expectations be adjusted accordingly.

    +
      +
    • +

      Check the module dependencies, as they have changed. The latest version of HTML Mail depends on the Mail System module (7.x-2.2 or later) and will not work without it.

      +
    • +
    • +

      Run update.php immediately after uploading new code.

      +
    • +
    • +

      The user-interface for adding email header and footer text has been removed. Headers and footers may be added by template files and/or by enabling the Echo module.

      +
    • +
    • +

      Any customized filters should be carefully tested, as some of the template variables have changed. Full documentation is provided both on the module configuration page (Click on the Instructions link) and as comments within the htmlmail.tpl.php file itself.

      +
    • +
    • +

      The following options have been removed from the module settings page. In their place, any combination of over 200 filter modules may be used to create an email-specific text format for post-template filtering.

      + +
    • +
    • +

      Full MIME handling, including automatic generation of a plaintext alternative part and conversion of image references to inline image attachments, is available simply by enabling the Mail MIME module.

      +
    • +
    +

    Configuration

    +

    Visit the Mail System settings page at admin/config/system/mailsystem to select which parts of Drupal will use HTML Mail instead of the default mail system.

    +

    Visit the HTML Mail settings page at admin/config/system/htmlmail to select a theme and post-filter for your messages.

    +

    Theming

    +

    The email message text goes through three transformations before sending:

    +
      +
    1. +

      Template File

      +

      A template file is applied to your message header, subject, and body text. The default template is the included htmlmail.tpl.php file. You may copy this file to your email theme directory (selected below), and use it to customize the contents and formatting of your messages. The comments within that file contain complete documentation on its usage.

      +
    2. +
    3. +

      Theming

      +

      You may choose a theme that will hold your templates from Step 1 above. If the Echo module is installed, this theme will also be used to wrap your templated text in a webpage. You use any one of over 800 themes to style your messages, or create your own for even more power and flexibility.

      +
    4. +
    5. +

      Post-filtering

      +

      You may choose a text format to be used for filtering email messages after theming. This allows you to use any combination of over 200 filter modules to make final changes to your message before sending.

      +

      Here is a recommended configuration:

      +
        +
      • +

        Emogrifier Converts stylesheets to inline style rules for consistent display on mobile devices and webmail.

        +
      • +
      • +

        Transliteration Converts non-ASCII text to US-ASCII equivalents. This helps prevent Microsoft "smart-quotes" from appearing as question-marks in Mozilla Thunderbird.

        +
      • +
      • +

        Pathologic Converts relative URLS to absolute URLS so that clickable links in your message will work as intended.

        +
      • +
      +
    6. +
    +

    Troubleshooting

    +
      +
    • +

      Double-check the Mail System module settings and and make sure you selected HTMLMailSystem for your Site-wide default mail system.

      +
    • +
    • +

      Try selecting the [ ] (Optional) Debug checkbox at the HTML Mail module settings page and re-sending your message.

      +
    • +
    • +

      Clear your cache after changing any .tpl.php files.

      +
    • +
    • +

      If you use a post-filter, make sure your filter settings page looks like this.

      +
    • +
    • +

      Visit the issue queue for support and feature requests.

      +
    • +
    +

    Related Modules

    +
    +
    Echo
    +
    +

    http://drupal.org/project/echo

    +
    +
    Emogrifier
    +
    +

    http://drupal.org/project/emogrifier

    +
    +
    HTML Purifier
    +
    +

    http://drupal.org/project/htmlpurifier

    +
    +
    htmLawed
    +
    +

    http://drupal.org/project/htmlawed

    +
    +
    Mail MIME
    +
    +

    http://drupal.org/project/mailmime

    +
    +
    Mail System
    +
    +

    http://drupal.org/project/mailsystem

    +
    +
    Pathologic
    +
    +

    http://drupal.org/project/pathologic

    +
    +
    Transliteration
    +
    +

    http://drupal.org/project/transliteration

    +
    +
    +

    Documentation

    +
    +
    filter.module
    +
    api.drupal.org/api/drupal/modules--filter--filter.module
    +
    +

    api.drupal.org/api/drupal/modules--filter--filter.module/group/standard_filters/7

    +
    +
    Installing contributed modules
    +
    +

    drupal.org/documentation/install/modules-themes/modules-7

    +
    +
    Theming guide
    +
    +

    drupal.org/documentation/theme

    +
    +
    +

    Original Author

    + +

    Current Maintainer

    + diff --git a/sites/all/modules/htmlmail/README.markdown b/sites/all/modules/htmlmail/README.markdown new file mode 100644 index 0000000..1b1b30d --- /dev/null +++ b/sites/all/modules/htmlmail/README.markdown @@ -0,0 +1,216 @@ +## [HTML Mail](http://drupal.org/project/htmlmail) +Lets you theme your messages the same way you theme the rest of your website. + +### [Requirement](http://www.dict.org/bin/Dict?Form=Dict2&Database=*&Query=requirement) + +* [Mail System 7.x-2.x](http://drupal.org/project/mailsystem) + +### [Installation](http://drupal.org/documentation/install/modules-themes/modules-7) + +The following additional modules, while not required, are highly recommended: + +* [Echo](http://drupal.org/project/echo) + + : Wraps your messages in a drupal theme. Now you can "brand" your + messages with the same logo, header, fonts, and styles as your website. + +* [Emogrifier](http://drupal.org/project/emogrifier) + + : Converts stylesheets to inline style rules, for consistent display on + mobile devices and webmail. + +* [Mail MIME](http://drupal.org/project/mailmime) + + : Provides a text/plain alternative to text/html emails, and automatically + converts image references to inline image attachments. + +* [Pathologic](http://drupal.org/project/pathologic) + + : Converts urls from relative to absolute, so clickable links in your + email messages work as intended. + +* [Transliteration](http://drupal.org/project/filter_transliteration) + + : Converts non-ASCII characters to their US-ASCII equivalents, such + as from Microsoft "smart-quotes" to regular quotes. + + : *Also available as a [patch](http://drupal.org/node/1095278#comment-4219530).* + +### [Updating from previous versions](http://drupal.org/node/250790) + +The [7.x-2.x](http://drupal.org/node/1106064) branch shares 94% of its code +with the [6.x-2.x](http://drupal.org/node/1119548) branch, but only 15% of +its code with the [7.x-1.x](http://drupal.org/node/355250) branch, and a tiny +8% of its code with the [6.x-1.x](http://drupal.org/node/329828) branch. + +Let your compatibility expectations be adjusted accordingly. + +* Check the module dependencies, as they have changed. The latest version of + [HTML Mail](http://drupal.org/project/htmlmail) depends on the + [Mail System](http://drupal.org/project/mailsystem) module (7.x-2.2 or later) + and will not work without it. + +* Run `update.php` *immediately* after uploading new code. + +* The user-interface for adding email header and footer text has been removed. + Headers and footers may be added by template files and/or by enabling the + [Echo](http://drupal.org/project/echo) module. + +* Any customized filters should be carefully tested, as some of the template + variables have changed. Full documentation is provided both on the module + configuration page (Click on the Instructions link) and as comments + within the `htmlmail.tpl.php` file itself. + +* The following options have been removed from the module settings page. In + their place, any combination of + [over 200 filter modules](http://drupal.org/project/modules/?filters=type%3Aproject_project%20tid%3A63%20hash%3A1hbejm%20-bs_project_sandbox%3A1%20bs_project_has_releases%3A1) + may be used to create an email-specific + [text format](http://drupal.org/node/778976) + for post-template filtering. + + * [Line break converter](http://api.drupal.org/api/drupal/modules--filter--filter.module/function/_filter_autop/7) + * [URL Filter](http://api.drupal.org/api/drupal/modules--filter--filter.module/function/_filter_url/7) + * [Relative Path to Absolute URLs](http://drupal.org/project/rel_to_abs) + * [Emogrifier](http://www.pelagodesign.com/sidecar/emogrifier/) + * [Token support](http://drupal.org/project/token) + +* Full MIME handling, including automatic generation of a plaintext + alternative part and conversion of image references to inline image + attachments, is available simply by enabling the + [Mail MIME](http://drupal.org/project/mailmime) module. + +### [Configuration](http://drupal.org/files/images/htmlmail_settings_2.thumbnail.png) + +Visit the [Mail System](http://drupal.org/project/mailsystem) settings page at +admin/config/system/mailsystem +to select which parts of Drupal will use +[HTML Mail](http://drupal.org/project/htmlmail) +instead of the +[default](http://api.drupal.org/api/drupal/modules--system--system.mail.inc/class/DefaultMailSystem/7) +[mail system](http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7). + +Visit the [HTML Mail](http://drupal.org/project/htmlmail) settings page at +admin/config/system/htmlmail +to select a theme and post-filter for your messages. + +### [Theming](http://drupal.org/documentation/theme) + +The email message text goes through three transformations before sending: + +1.

    Template File

    + + A template file is applied to your message header, subject, and body text. + The default template is the included `htmlmail.tpl.php` file. You may copy + this file to your email theme directory (selected below), and + use it to customize the contents and formatting of your messages. The + comments within that file contain complete documentation on its usage. + +2.

    Theming

    + + You may choose a theme that will hold your templates from Step 1 above. If + the [Echo](http://drupal.org/project/echo) module is installed, this theme + will also be used to wrap your templated text in a webpage. You use any one + of [over 800](http://drupal.org/project/themes) themes to style your + messages, or [create your own](http://drupal.org/documentation/theme) for + even more power and flexibility. + +3.

    Post-filtering

    + + You may choose a + [text format](http://drupal.org/node/778976) + to be used for filtering email messages *after* theming. + This allows you to use any combination of + [over 200 filter modules](http://drupal.org/project/modules/?filters=type%3Aproject_project%20tid%3A63%20hash%3A1hbejm%20-bs_project_sandbox%3A1%20bs_project_has_releases%3A1) + to make final changes to your message before sending. + + Here is a recommended configuration: + + * [Emogrifier](http://drupal.org/project/emogrifier) + Converts stylesheets to inline style rules for consistent display on + mobile devices and webmail. + + * [Transliteration](http://drupal.org/project/filter_transliteration) + Converts non-ASCII text to US-ASCII equivalents. This helps prevent + Microsoft "smart-quotes" from appearing as question-marks in + Mozilla Thunderbird. + + * [Pathologic](http://drupal.org/project/pathologic) + Converts relative URLS to absolute URLS so that clickable links in + your message will work as intended. + +### Troubleshooting + +* Check the [online documentation](http://drupal.org/node/1124376), + especially the [screenshots](http://drupal.org/node/1124934). + +* There is a special documentation page for + [Using HTML Mail together with SMTP Authentication Support](http://drupal.org/node/1200142). + +* [Simplenews](http://drupal.org/project/simplenews) users attempting advanced + theming should read [this page](http://drupal.org/node/1260178). + +* Double-check the [Mail System](http://drupal.org/project/mailsystem) + module settings and and make sure you selected + HTMLMailSystem for your + Site-wide default mail system. + +* Try selecting the [ ] *(Optional)* Debug checkbox + at the [HTML Mail](http://drupal.org/project/htmlmail) module + settings page and re-sending your message. + +* Clear your cache after changing any .tpl.php + files. + +* If you use a post-filter, make sure your filter settings page looks like + [this](http://drupal.org/node/1130960). + +* Visit the [issue queue](http://drupal.org/project/issues/htmlmail) + for support and feature requests. + +### Related Modules + +**Echo** +: http://drupal.org/project/echo + +**Emogrifier** +: http://drupal.org/project/emogrifier + +**HTML Purifier** +: http://drupal.org/project/htmlpurifier + +**htmLawed** +: http://drupal.org/project/htmlawed + +**Mail MIME** +: http://drupal.org/project/mailmime + +**Mail System** +: http://drupal.org/project/mailsystem + +**Pathologic** +: http://drupal.org/project/pathologic + +**Transliteration** +: http://drupal.org/project/transliteration + +### [Documentation](http://drupal.org/project/documentation) + +**[HTML Mail](http://drupal.org/node/1124376) + +**[filter.module](http://api.drupal.org/api/drupal/modules--filter--filter.module/6)** +: [api.drupal.org/api/drupal/modules--filter--filter.module](http://api.drupal.org/api/drupal/modules--filter--filter.module/7) +: [api.drupal.org/api/drupal/modules--filter--filter.module/group/standard_filters/7](http://api.drupal.org/api/drupal/modules--filter--filter.module/group/standard_filters/7) + +**[Installing contributed modules](http://drupal.org/documentation/install/modules-themes/modules-7)** +: [drupal.org/documentation/install/modules-themes/modules-7](http://drupal.org/documentation/install/modules-themes/modules-7) + +**[Theming guide](http://drupal.org/documentation/theme)** +: [drupal.org/documentation/theme](http://drupal.org/documentation/theme) + +### Original Author + +* [Chris Herberte](http://drupal.org/user/1171) + +### Current Maintainer + +* [Bob Vincent](http://drupal.org/user/36148) diff --git a/sites/all/modules/htmlmail/README.txt b/sites/all/modules/htmlmail/README.txt new file mode 100644 index 0000000..42758ef --- /dev/null +++ b/sites/all/modules/htmlmail/README.txt @@ -0,0 +1,235 @@ +[1]HTML Mail + + Lets you theme your messages the same way you theme the rest of your + website. + + [2]Requirement + + * [3]Mail System 7.x-2.x + + [4]Installation + + The following additional modules, while not required, are highly + recommended: + * + + [5]Echo + Wraps your messages in a drupal theme. Now you can "brand" + your messages with the same logo, header, fonts, and + styles as your website. + + * + + [6]Emogrifier + Converts stylesheets to inline style rules, for consistent + display on mobile devices and webmail. + + * + + [7]Mail MIME + Provides a text/plain alternative to text/html emails, and + automatically converts image references to inline image + attachments. + + * + + [8]Pathologic + Converts urls from relative to absolute, so clickable + links in your email messages work as intended. + + * + + [9]Transliteration + Converts non-ASCII characters to their US-ASCII + equivalents, such as from Microsoft "smart-quotes" to + regular quotes. + + Also available as a [10]patch. + + [11]Updating from previous versions + + The [12]7.x-2.x branch shares 94% of its code with the [13]6.x-2.x + branch, but only 15% of its code with the [14]7.x-1.x branch, and a + tiny 8% of its code with the [15]6.x-1.x branch. + + Let your compatibility expectations be adjusted accordingly. + * Check the module dependencies, as they have changed. The latest + version of [16]HTML Mail depends on the [17]Mail System module + (7.x-2.2 or later) and will not work without it. + * Run update.php immediately after uploading new code. + * The user-interface for adding email header and footer text has been + removed. Headers and footers may be added by template files and/or + by enabling the [18]Echo module. + * Any customized filters should be carefully tested, as some of the + template variables have changed. Full documentation is provided + both on the module configuration page (Click on the Instructions + link) and as comments within the htmlmail.tpl.php file itself. + * The following options have been removed from the module settings + page. In their place, any combination of [19]over 200 filter + modules may be used to create an email-specific [20]text format for + post-template filtering. + + [21]Line break converter + + [22]URL Filter + + [23]Relative Path to Absolute URLs + + [24]Emogrifier + + [25]Token support + * Full MIME handling, including automatic generation of a plaintext + alternative part and conversion of image references to inline image + attachments, is available simply by enabling the [26]Mail MIME + module. + + [27]Configuration + + Visit the [28]Mail System settings page at + admin/config/system/mailsystem to select which parts of Drupal will use + [29]HTML Mail instead of the [30]default [31]mail system. + + Visit the [32]HTML Mail settings page at admin/config/system/htmlmail + to select a theme and post-filter for your messages. + + [33]Theming + + The email message text goes through three transformations before + sending: + 1. Template File + A template file is applied to your message header, subject, and + body text. The default template is the included htmlmail.tpl.php + file. You may copy this file to your email theme directory + (selected below), and use it to customize the contents and + formatting of your messages. The comments within that file contain + complete documentation on its usage. + 2. Theming + You may choose a theme that will hold your templates from Step 1 + above. If the [34]Echo module is installed, this theme will also be + used to wrap your templated text in a webpage. You use any one of + [35]over 800 themes to style your messages, or [36]create your own + for even more power and flexibility. + 3. Post-filtering + You may choose a [37]text format to be used for filtering email + messages after theming. This allows you to use any combination of + [38]over 200 filter modules to make final changes to your message + before sending. + Here is a recommended configuration: + + [39]Emogrifier Converts stylesheets to inline style rules for + consistent display on mobile devices and webmail. + + [40]Transliteration Converts non-ASCII text to US-ASCII + equivalents. This helps prevent Microsoft "smart-quotes" from + appearing as question-marks in Mozilla Thunderbird. + + [41]Pathologic Converts relative URLS to absolute URLS so that + clickable links in your message will work as intended. + + Troubleshooting + + * Double-check the [42]Mail System module settings and and make sure + you selected HTMLMailSystem for your Site-wide default mail system. + * Try selecting the [ ] (Optional) Debug checkbox at the [43]HTML + Mail module settings page and re-sending your message. + * Clear your cache after changing any .tpl.php files. + * If you use a post-filter, make sure your filter settings page looks + like [44]this. + * Visit the [45]issue queue for support and feature requests. + + Related Modules + + Echo + http://drupal.org/project/echo + + Emogrifier + http://drupal.org/project/emogrifier + + HTML Purifier + http://drupal.org/project/htmlpurifier + + htmLawed + http://drupal.org/project/htmlawed + + Mail MIME + http://drupal.org/project/mailmime + + Mail System + http://drupal.org/project/mailsystem + + Pathologic + http://drupal.org/project/pathologic + + Transliteration + http://drupal.org/project/transliteration + + [46]Documentation + + [47]filter.module + [48]api.drupal.org/api/drupal/modules--filter--filter.module + [49]api.drupal.org/api/drupal/modules--filter--filter.module/gro + up/standard_filters/7 + + [50]Installing contributed modules + [51]drupal.org/documentation/install/modules-themes/modules-7 + + [52]Theming guide + [53]drupal.org/documentation/theme + + Original Author + + * [54]Chris Herberte + + Current Maintainer + + * [55]Bob Vincent + +References + + 1. http://drupal.org/project/htmlmail + 2. http://www.dict.org/bin/Dict?Form=Dict2&Database=*&Query=requirement + 3. http://drupal.org/project/mailsystem + 4. http://drupal.org/documentation/install/modules-themes/modules-7 + 5. http://drupal.org/project/echo + 6. http://drupal.org/project/emogrifier + 7. http://drupal.org/project/mailmime + 8. http://drupal.org/project/pathologic + 9. http://drupal.org/project/filter_transliteration + 10. http://drupal.org/node/1095278#comment-4219530 + 11. http://drupal.org/node/250790 + 12. http://drupal.org/node/1106064 + 13. http://drupal.org/node/1119548 + 14. http://drupal.org/node/355250 + 15. http://drupal.org/node/329828 + 16. http://drupal.org/project/htmlmail + 17. http://drupal.org/project/mailsystem + 18. http://drupal.org/project/echo + 19. http://drupal.org/project/modules/?filters=type%3Aproject_project%20tid%3A63%20hash%3A1hbejm%20-bs_project_sandbox%3A1%20bs_project_has_releases%3A1 + 20. http://drupal.org/node/778976 + 21. http://api.drupal.org/api/drupal/modules--filter--filter.module/function/_filter_autop/7 + 22. http://api.drupal.org/api/drupal/modules--filter--filter.module/function/_filter_url/7 + 23. http://drupal.org/project/rel_to_abs + 24. http://www.pelagodesign.com/sidecar/emogrifier/ + 25. http://drupal.org/project/token + 26. http://drupal.org/project/mailmime + 27. http://drupal.org/files/images/htmlmail_settings_2.thumbnail.png + 28. http://drupal.org/project/mailsystem + 29. http://drupal.org/project/htmlmail + 30. http://api.drupal.org/api/drupal/modules--system--system.mail.inc/class/DefaultMailSystem/7 + 31. http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7 + 32. http://drupal.org/project/htmlmail + 33. http://drupal.org/documentation/theme + 34. http://drupal.org/project/echo + 35. http://drupal.org/project/themes + 36. http://drupal.org/documentation/theme + 37. http://drupal.org/node/778976 + 38. http://drupal.org/project/modules/?filters=type%3Aproject_project%20tid%3A63%20hash%3A1hbejm%20-bs_project_sandbox%3A1%20bs_project_has_releases%3A1 + 39. http://drupal.org/project/emogrifier + 40. http://drupal.org/project/filter_transliteration + 41. http://drupal.org/project/pathologic + 42. http://drupal.org/project/mailsystem + 43. http://drupal.org/project/htmlmail + 44. http://drupal.org/node/1130960 + 45. http://drupal.org/project/issues/htmlmail + 46. http://drupal.org/project/documentation + 47. http://api.drupal.org/api/drupal/modules--filter--filter.module/6 + 48. http://api.drupal.org/api/drupal/modules--filter--filter.module/7 + 49. http://api.drupal.org/api/drupal/modules--filter--filter.module/group/standard_filters/7 + 50. http://drupal.org/documentation/install/modules-themes/modules-7 + 51. http://drupal.org/documentation/install/modules-themes/modules-7 + 52. http://drupal.org/documentation/theme + 53. http://drupal.org/documentation/theme + 54. http://drupal.org/user/1171 + 55. http://drupal.org/user/36148 diff --git a/sites/all/modules/htmlmail/htmlmail--htmlmail.tpl.php b/sites/all/modules/htmlmail/htmlmail--htmlmail.tpl.php new file mode 100644 index 0000000..2d95c67 --- /dev/null +++ b/sites/all/modules/htmlmail/htmlmail--htmlmail.tpl.php @@ -0,0 +1,32 @@ + +

    HTML Mail test message

    +
    + +
    + +
    +
    +

    + To customize this test message: +

    1. + Visit admin/config/system/htmlmail + and select a theme to hold your custom email template files. +

      1. + Visit admin/appearance + to enable your selected theme. +

        1. + Copy the + htmlmail--htmlmail.tpl.php + file to your theme directory + . +

        2. + Edit the copied file. +

    +
    + value) pairs. + * - $from: The configured sender address. + * - $to: The recipient subscriber email address. + * - $subject: The message subject line. + * - $body: The formatted message body. + * - $language: The language object for this message. + * - $params: An array containing the following keys: + * - context: An array containing the following keys: + * - account: The recipient subscriber account object, which contains + * the following useful properties: + * - snid: The simplenews subscriber id, or NULL for test messages. + * - name: The subscriber username, or NULL. + * - activated: The date this subscription became active, or NULL. + * - uid: The subscriber user id, or NULL. + * - mail: The subscriber email address; same as $message['to']. + * - language: The subscriber language code. + * - tids: An array of taxonomy term ids. + * - newsletter_subscription: An array of subscription ids. + * - node: The simplenews newsletter node object, which contains the + * following useful properties: + * - changed: The node last-modified date, as a unix timestamp. + * - created: The node creation date, as a unix timestamp. + * - name: The username of the node publisher. + * - nid: The node id. + * - title: The node title. + * - uid: The user ID of the node publisher. + * - newsletter: The simplenews newsletter object, which contains the + * following useful properties: + * - nid: The node ID of the newsletter node. + * - name: The short name of the newsletter. + * - description: The long name or description of the newsletter. + * - $template_path: The relative path to the template directory. + * - $template_url: The absolute url to the template directory. + * - $theme: The name of the selected Email theme. + * - $theme_path: The relative path to the Email theme directory. + * - $theme_url: The absolute url to the Email theme directory. + */ + $template_name = basename(__FILE__); + $current_path = realpath(NULL); + $current_len = strlen($current_path); + $template_path = realpath(dirname(__FILE__)); + if (!strncmp($template_path, $current_path, $current_len)) { + $template_path = substr($template_path, $current_len + 1); + } + $template_url = url($template_path, array('absolute' => TRUE)); +?> + + + +
    + +
    + +
    +
    +

    + To customize your simplenews messages: +

    1. + Visit admin/config/system/htmlmail + and select a theme to hold your custom email template files. +

    2. + Visit admin/appearance + to enable your selected theme. +

    3. + Edit your
      + +
      file. +

    4. + Copy
      + +
      to
      + +

    5. + For general Simplenews message customization, copy
      + +
      to
      + +

    6. + For message-specific customization, copy
      + +
      to one of the following: +

      • + htmlmail--simplenews--node.tpl.php +

        + Regular newsletter template. +

      • + htmlmail--simplenews--subscribe.tpl.php +

        + New subscriber confirmation message. +

      • + htmlmail--simplenews--test.tpl.php +

        + Test newsletter. +

      • + htmlmail--simplenews--unsubscribe.tpl.php +

        + Unsubscribe confirmation message. +

    7. + Edit the copied file. +

    8. + Send a test message to make sure your customizations worked. +

    9. + If you think your customizations would be of use to others, + please contribute your file as a feature request in the + issue queue. +

    + The simplenews module sets the $params variable. + For this message, +

    +$params = 
    +  

    +
    + value) pairs. + * - $from: The configured sender address. + * - $to: The recipient email address. + * - $subject: The message subject line. + * - $body: The formatted message body. + * - $language: The language object for this message. + * - $params: An array containing the following keys: + * - account: The user object whose password is being requested, which + * contains the following useful properties: + * - uid: The user-id number. + * - name: The user login name. + * - mail: The user email address. Should be the same as $to. + * - theme: The user-chosen theme, or a blank string if unset. + * - signature: The user signature block. + * - signature_format: The text input filter used to format the signature. + * - created: Account creation date, as a unix timestamp. + * - access: Account access date, as a unix timestamp. + * - login: Account login date, as a unix timestamp. + * - status: Integer 0 = disabled; 1 = enabled. + * - timezone: User timezone, or NULL if unset. + * - language: User language, or blank string if unset. + * - picture: Path to user picture, or blank string if unset. + * - init: The email address used to initially register this account. + * - data: User profile data, as a serialized string. + * - roles: Array of roles assigned to this user, as (rid => role_name) + * pairs. + * - $template_path: The relative path to the template directory. + * - $template_url: The absolute url to the template directory. + * - $theme: The name of the selected Email theme. + * - $theme_path: The relative path to the Email theme directory. + * - $theme_url: The absolute url to the Email theme directory. + */ + $template_name = basename(__FILE__); + $current_path = realpath(NULL); + $current_len = strlen($current_path); + $template_path = realpath(dirname(__FILE__)); + if (!strncmp($template_path, $current_path, $current_len)) { + $template_path = substr($template_path, $current_len + 1); + } + $template_url = url($template_path, array('absolute' => TRUE)); +?> +
    + +
    + +
    +
    +

    + To customize your user password reset messages: +

    1. + Visit admin/config/system/htmlmail + and select a theme to hold your custom email template files. +

    2. + Visit admin/build/themes + to enable your selected theme. +

    3. + Edit your
      + +
      file. +

    4. + Copy
      + +
      to
      + +

    5. + For general user-module message customization, copy
      + +
      to
      + +

    6. + Copy
      + +
      to
      + . +

    7. + Edit the copied file. +

    8. + Send a test message to make sure your customizations worked. +

    9. + If you think your customizations would be of use to others, + please contribute your file as a feature request in the + issue queue. +

    + The user module sets the $params variable. + For this message, +

    +$params = 
    +  

    +
    + t('Unfiltered')); + foreach (filter_formats() as $id => $filter) { + $formats[$id] = $filter->name; + } + $form['template'] = array( + '#type' => 'fieldset', + '#title' => t('Step 1'), + '#collapsible' => FALSE, + ); + $form['template']['htmlmail_template'] = array( + '#type' => 'fieldset', + '#prefix' => '' . t('Template file') . ':
    ' + . t('A template file is applied to your message header, subject, and body text. You may copy the !template file to your default theme directory and use it to customize your messages.', + array( + '!uri' => url('http://drupalcode.org/project/htmlmail.git/blob_plain/refs/heads/7.x-2.x:/htmlmail.tpl.php'), + '!template' => 'htmlmail.tpl.php' + ) + ), + '#title' => t('Instructions'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['template']['htmlmail_template']['instructions'] = array( + '#type' => 'item', + '#suffix' => t('!Instructions +

    When formatting an email message with a given $module and $key, HTML Mail will use the first template file it finds from the following list:

    +
      +
    1. htmlmail--$module--$key.tpl.php
    2. +
    3. htmlmail--$module.tpl.php
    4. +
    5. htmlmail.tpl.php
    6. +
    +

    For each filename, HTML Mail looks first in the chosen Email theme directory, then in its own module directory, before proceeding to the next filename.

    +

    For example, if example_module sends mail with:

    +
    +drupal_mail("example_module", "outgoing_message" ...)
    +
    +
    +

    the possible template file names would be:

    +
      +
    1. htmlmail--example_module--outgoing_message.tpl.php
    2. +
    3. htmlmail--example_module.tpl.php
    4. +
    5. htmlmail.tpl.php
    6. +
    +

    Template files are cached, so remember to clear the cache by visiting admin/config/development/performance after changing any .tpl.php files.

    +

    The following variables available in this template:

    +
    +
    $body
    +
    +

    The message body text.

    +
    +
    $module
    +
    +

    The first argument to drupal_mail(), which is, by convention, the machine-readable name of the sending module.

    +
    +
    $key
    +
    +

    The second argument to drupal_mail(), which should give some indication of why this email is being sent.

    +
    +
    $message_id
    +
    +

    The email message id, which should be equal to "{$module}_{$key}".

    +
    +
    $headers
    +
    +

    An array of email (name => value) pairs.

    +
    +
    $from
    +
    +

    The configured sender address.

    +
    +
    $to
    +
    +

    The recipient email address.

    +
    +
    $subject
    +
    +

    The message subject line.

    +
    +
    $body
    +
    +

    The formatted message body.

    +
    +
    $language
    +
    +

    The language object for this message.

    +
    +
    $params
    +
    +

    Any module-specific parameters.

    +
    +
    $template_name
    +
    +

    The basename of the active template.

    +
    +
    $template_path
    +
    +

    The relative path to the template directory.

    +
    +
    $template_url
    +
    +

    The absolute URL to the template directory.

    +
    +
    $theme
    +
    +

    The name of the Email theme used to hold template files. If the Echo module is enabled this theme will also be used to transform the message body into a fully-themed webpage.

    +
    +
    $theme_path
    +
    +

    The relative path to the selected Email theme directory.

    +
    +
    $theme_url
    +
    +

    The absolute URL to the selected Email theme directory.

    +
    +
    $debug
    +
    +

    TRUE to add some useful debugging info to the bottom of the message.

    +
    +
    +

    Other modules may also add or modify theme variables by implementing a MODULENAME_preprocess_htmlmail(&$variables) hook function.

    ', + array('!Instructions' => '') + ), + ); + $form['template']['htmlmail_debug'] = array( + '#type' => 'checkbox', + '#prefix' => '
    ', + '#title' => '' . t('(Optional)') . ' ' . t('Debug'), + '#default_value' => variable_get('htmlmail_debug', '0'), + '#description' => t('Add debugging info (Set $debug to TRUE).'), + ); + $form['theme'] = array( + '#type' => 'fieldset', + '#title' => t('Step 2'), + '#collapsible' => FALSE, + ); + $form['theme']['htmlmail_theme'] = array( + '#type' => 'select', + '#title' => t('Email theme'), + '#default_value' => variable_get('htmlmail_theme', ''), + '#options' => htmlmail_get_allowed_themes(), + '#suffix' => '

    ' + . t('Choose the theme that will hold your customized templates from Step 1 above.') + . '

    ' + . (module_exists('echo') ? + t('The templated text will be styled by your chosen theme. This lets you use any one of over 800 themes to style your messages. Creating an email-specific sub-theme lets you use the full power of the drupal theme system to format your messages.', + array( + '!themes' => 'http://drupal.org/project/themes', + '!theme_system' => 'http://drupal.org/documentation/theme', + ) + ) : + t('If you install and enable the Echo module, the theme you select will also be used to style your messages as if they were pages on your website.', + array( + '!echo' => 'http://drupal.org/project/echo' + ) + ) + ) + . '

    ' + . (module_exists('mailmime') ? + t('Since you have the Mail MIME module installed, your images will be automatically converted to inline attachments, and a plain-text alternative will be available to recipients who prefer it.', + array('!mailmime' => 'http://drupal.org/project/mailmime') + ) : + t('If you install the Mail MIME module, images in your emails will be automatically converted to inline attachments, and a plain-text alternative will be made available. This prevents your recipients from seeing broken image links and scary security warnings when they don\'t have the sender\'s address in their email addressbook. Mail MIME also allows HTML Mail to handle MIME-formatted messages sent by other modules such as Send by-email.', + array( + '!mailmime' => 'http://drupal.org/project/mailmime', + '!print' => 'http://drupal.org/project/print', + ) + ) + ) + . '

    ', + ); + $form['filter'] = array( + '#type' => 'fieldset', + '#title' => t('Step 3'), + '#collapsible' => FALSE, + ); + $form['filter']['htmlmail_postfilter'] = array( + '#type' => 'select', + '#title' => t('Post-filtering'), + '#default_value' => variable_get('htmlmail_postfilter', ''), + '#options' => $formats, + '#suffix' => '

    ' + . t('You may choose a text format to be used for filtering email messages after theming. This allows you to use any combination of over 200 filter modules to make final changes to your message before sending.', + array( + '!formats' => url('admin/config/content/formats'), + '!filters' => url('http://drupal.org/project/modules/?filters=type%3Aproject_project%20tid%3A63%20hash%3A1hbejm%20-bs_project_sandbox%3A1%20bs_project_has_releases%3A1'), + ) + ) + . '

    ' + . t('Here is a recommended configuration:') + . '

    • ' + . t('Emogrifier', + array('!emogrifier' => url('http://drupal.org/project/emogrifier')) + ) + . '
      ' + . t('Converts stylesheets to inline style rules for consistent display on mobile devices and webmail.') + . '
    • ' + . t('Transliteration', + array('!transliteration' => url('http;//drupal.org/project/filter_transliteration')) + ) + . '
      ' + . t('Converts non-ASCII text to US-ASCII equivalents. This helps prevent Microsoft smart-quotes from appearing as question-marks in Mozilla Thunderbird.' + ) + . '
    • ' + . t('Pathologic', + array('!pathologic' => url('http://drupal.org/project/pathologic')) + ) + . '
      ' + . t('Converts relative URLS to absolute URLS so that clickable links in your message will work as intended.') + . '
    ' + ); + return system_settings_form($form); +} + +/** + * Builds a form for sending a test message. + */ +function htmlmail_test_form($form_values = NULL) { + $defaults = variable_get( + 'htmlmail_test', + array( + 'to' => variable_get('site_mail', 'user@example.com'), + 'subject' => 'test', + 'body' => array( + 'value' => 'test', + ), + ) + ); + $defaults['body']['format'] = filter_fallback_format(); + $form['to'] = array( + '#type' => 'textfield', + '#title' => t('To'), + '#default_value' => $defaults['to'], + '#maxlength' => 128, + '#required' => TRUE, + ); + $form['subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#default_value' => $defaults['subject'], + '#maxlength' => 128, + '#required' => TRUE, + ); + $form['body'] = array( + '#type' => 'text_format', + '#title' => t('Body'), + '#rows' => 20, + '#default_value' => $defaults['body']['value'], + '#format' => $defaults['body']['format'], + '#required' => TRUE, + ); + $mailsystem = mailsystem_get(); + if (empty($mailsystem['htmlmail'])) { + $mailsystem['htmlmail'] = 'HTMLMailSystem'; + } + $form['class'] = array( + '#type' => 'select', + '#title' => t('Test mail sending class'), + '#default_value' => $mailsystem['htmlmail'], + '#options' => array_combine(mailsystem_get_classes(), mailsystem_get_classes()), + '#description' => 'Select the MailSystemInterface implementation to be tested.', + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Send test message'), + ); + return $form; +} + +/** + * Sends the test messsage and saves the contents for re-use. + */ +function htmlmail_test_form_submit($form, &$form_state) { + // Get the form values. + $defaults = array( + 'to' => $form_state['values']['to'], + 'subject' => $form_state['values']['subject'], + 'body' => $form_state['values']['body'], + ); + // Set the defaults for reuse. + variable_set('htmlmail_test', $defaults); + // Set the mail sending class. + mailsystem_set(array('htmlmail' => $form_state['values']['class'])); + // Send the email. + $params = array( + 'subject' => $form_state['values']['subject'], + 'body' => check_markup( + $form_state['values']['body']['value'], + $form_state['values']['body']['format'] + ), + ); + if ( + drupal_mail( + 'htmlmail', + 'test', + $form_state['values']['to'], + language_default(), + $params + ) + ) { + drupal_set_message(t('HTML Mail test message sent.')); + } +} diff --git a/sites/all/modules/htmlmail/htmlmail.info b/sites/all/modules/htmlmail/htmlmail.info new file mode 100644 index 0000000..8964a21 --- /dev/null +++ b/sites/all/modules/htmlmail/htmlmail.info @@ -0,0 +1,15 @@ +package = Mail +name = HTML Mail +description = Enables HTML in system emails. +php = 5.0 +files[] = htmlmail.mail.inc +dependencies[] = mailsystem +core = 7.x +configure = admin/config/system/htmlmail + +; Information added by drupal.org packaging script on 2012-04-05 +version = "7.x-2.65" +core = "7.x" +project = "htmlmail" +datestamp = "1333660850" + diff --git a/sites/all/modules/htmlmail/htmlmail.install b/sites/all/modules/htmlmail/htmlmail.install new file mode 100644 index 0000000..d3f4a9c --- /dev/null +++ b/sites/all/modules/htmlmail/htmlmail.install @@ -0,0 +1,112 @@ + 'HTML Mail', + '!htmlmail' => 'http://drupal.org/project/htmlmail', + '%mailsystem' => 'Mail System', + '!mailsystem' => 'http://drupal.org/project/mailsystem', + ); + $result['htmlmail_mailsystem'] = array( + 'title' => t('%mailsystem module', $args), + 'value' => t('7.x-1.x'), + 'description' => t( + '%htmlmail new requires %mailsystem 7.x-2.6 or later. Please download and install a recent version of %mailsystem, then re-enable the %htmlmail module.', $args + ), + 'severity' => REQUIREMENT_ERROR, + ); + return $result; +} + +/** + * Implements hook_update_N(). + * + * Removes variables that are no longer used. + */ +function htmlmail_update_7200() { + variable_del('htmlmail_header'); + variable_del('htmlmail_footer'); + variable_del('htmlmail_css'); +} + +/** + * Implements hook_update_N(). + * + * Rename HTMLMailMailSystem to HTMLMailSystem. + */ +function htmlmail_update_7201() { + module_load_include('module', 'mailsystem'); + foreach (mailsystem_get() as $name => $value) { + if ($value == 'HTMLMailMailSystem') { + mailsystem_set(array($name => 'HTMLMailSystem')); + } + } +} + +/** + * Implements hook_update_N(). + * + * Increase module weight so dependent modules get loaded first. + */ +function htmlmail_update_7202() { + db_query("UPDATE {system} SET weight = 10 WHERE type = 'module' AND name = 'htmlmail'"); +} + +function htmlmail_update_7203() { + if ($requirements = htmlmail_requirements('runtime')) { + $requirement = array_shift($requirements); + throw new DrupalUpdateException($requirement['description']); + } +} + +/** + * Implements hook_enable(). + */ +function htmlmail_enable() { + module_load_include('module', 'mailsystem'); + mailsystem_set(array('htmlmail' => 'HTMLMailSystem')); +} + +/** + * Implements hook_disable(). + */ +function htmlmail_disable() { + // Check is necessary because a 7.x-1.x to 7.x-2.x upgrade + // may not have mailsystem installed. + if (function_exists('mailsystem_clear')) { + mailsystem_clear(array('htmlmail' => 'HTMLMailSystem')); + } +} + +/** + * Implements hook_install(). + */ +function htmlmail_install() { + htmlmail_update_7202(); +} + +/** + * Implements hook_uninstall(). + */ +function htmlmail_uninstall() { + db_query("DELETE FROM {variable} WHERE name LIKE 'htmlmail_%'"); + cache_clear_all('variables', 'cache'); +} diff --git a/sites/all/modules/htmlmail/htmlmail.mail.inc b/sites/all/modules/htmlmail/htmlmail.mail.inc new file mode 100644 index 0000000..d882c5c --- /dev/null +++ b/sites/all/modules/htmlmail/htmlmail.mail.inc @@ -0,0 +1,322 @@ + value) email headers. + * - body: The text/plain or text/html message part. + * + * @return + * The formatted $message, ready for sending. + */ + public function format(array $message) { + $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS); + // @todo Remove this when issue #209672 gets resolved. + $default_from = variable_get('site_mail', ini_get('sendmail_from')); + if ( !empty($message['headers']['From']) + && $message['headers']['From'] == $default_from + && valid_email_address($default_from) + ) { + $message['headers']['From'] = '"' + . str_replace('"', '', variable_get('site_name', 'Drupal')) + . '" <' . $default_from . '>'; + } + // Collapse the message body array. + if (module_exists('mailmime')) { + $body = $this->formatMailMIME($message); + $plain = $message['MailMIME']->getTXTBody(); + } + else { + if (is_array($message['body'])) { + $message['body'] = implode("
    $eol
    $eol", $message['body']); + } + $body = theme('htmlmail', $message); + if ($message['body'] && !$body) { + watchdog( + 'htmlmail', + 'The %theme function did not return any text. Please check your template file for errors.', + array('%theme' => "theme('htmlmail', \$message)"), + WATCHDOG_WARNING + ); + $body = $message['body']; + } + // @todo Change to drupal_html_to_text when issue #299138 gets resolved. + $plain = mailsystem_html_to_text($body); + if ($body && !$plain) { + watchdog( + 'htmlmail', + 'The %convert function did not return any text. Please report this error to the %mailsystem issue queue.', + array('%convert' => 'mailsystem_html_to_text()', '%mailsystem' => 'Mail system'), + WATCHDOG_WARNING, + 'http://drupal.org/node/add/project-issue/mailsystem' + ); + } + } + // Check to see whether recipient allows non-plaintext. + if ($body && htmlmail_is_allowed($message['to'])) { + // Optionally apply the selected web theme. + if (module_exists('echo') && $theme = htmlmail_get_selected_theme($message)) { + $themed_body = echo_themed_page($message['subject'], $body, $theme); + if ($themed_body) { + $body = $themed_body; + } + else { + watchdog( + 'htmlmail', + 'The %echo function did not return any text. Please check the page template of your %theme theme for errors.', + array('%echo' => 'echo_themed_page()', '%theme' => $theme), + WATCHDOG_WARNING + ); + } + } + // Optionally apply the selected output filter. + if ($filter = variable_get('htmlmail_postfilter')) { + $filtered_body = check_markup($body, $filter); + if ($filtered_body) { + $body = $filtered_body; + } + else { + watchdog( + 'htmlmail', + 'The %check function did not return any text. Please check your %filter output filter for errors.', + array('%check' => 'check_markup()', '%filter' => $filter), + WATCHDOG_WARNING + ); + } + } + // Store the fully-themed HTML body. + if (isset($message['MailMIME'])) { + $mime = &$message['MailMIME']; + $mime->setHTMLBody($body); + list($message['headers'], $message['body']) = $mime->toEmail($message['headers']); + if (!$message['body']) { + watchdog( + 'htmlmail', + 'The %toemail function did not return any text. Please report this errot to the %mailmime issue queue.', + array('%toemail' => 'MailMIME::toEmail()', '%mailmime' => 'Mail MIME'), + WATCHDOG_WARNING, + 'http://drupal.org/node/add/project-issue/mailmime' + ); + } + } + else { + $message['headers']['Content-Type'] = 'text/html; charset=utf-8'; + $message['body'] = $body; + } + } + else { + if (isset($message['MailMIME'])) { + $mime = &$message['MailMIME']; + $mime->setHTMLBody(''); + $mime->setContentType('text/plain', array('charset' => 'utf-8')); + list($message['headers'], $message['body']) = $mime->toEmail($message['headers']); + if (!$message['body']) { + watchdog( + 'htmlmail', + 'The %toemail function did not return any text. Please report this errot to the %mailmime issue queue.', + array('%toemail' => 'MailMIME::toEmail()', '%mailmime' => 'Mail MIME'), + WATCHDOG_WARNING, + 'http://drupal.org/node/add/project-issue/mailmime' + ); + } + } + else { + $message['body'] = $plain; + $message['headers']['Content-Type'] = 'text/plain; charset=utf-8'; + } + } + return $message; + } + + /** + * Use the MailMime class to format the message body. + * + * @see http://drupal.org/project/mailmime + */ + public function formatMailMIME(array &$message) { + $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS); + $message['body'] = MailMIME::concat($message['body']); + // Build a full email message string. + $email = MailMIME::encodeEmail($message['headers'], $message['body']); + // Parse it into MIME parts. + if (!($mime = MailMIME::parse($email))) { + watchdog( + 'HTMLMailSystem', + 'Could not parse email message.', + array(), + WATCHDOG_ERROR + ); + return $message; + } + // Work on a copy so that the original $message['body'] remains unchanged. + $email = $message; + if ( !($email['body'] = $mime->getHTMLBody()) + && !($email['body'] = $mime->getTXTBody()) + ) { + $email['body'] = ''; + } + else { + // Wrap formatted plaintext in
     tags.
    +      if ( $email['body'] === strip_tags($email['body']) // No html tags.
    +        && preg_match('/.' . $eol . './', $email['body']) // At least one embedded newline.
    +      ) {
    +        $email['body'] = '
    ' . $email['body'] . '
    '; + } + } + // Theme with htmlmail.tpl.php. + $body = theme('htmlmail', $email); + $mime->setHTMLBody($body); + // @todo Change to drupal_html_to_text when issue #299138 gets resolved. + $mime->setTXTBody(mailsystem_html_to_text($body)); + $message['MailMIME'] = &$mime; + return $body; + } + + /** + * Send an email message. + * + * @param $message + * An associative array containing at least: + * - headers: An associative array of (name => value) email headers. + * - body: The text/plain or text/html message body. + * - MailMIME: The message, parsed into a MailMIME object. + */ + public function mail(array $message) { + $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS); + // Ensure that subject is non-null. + $message += array('subject' => t('(No subject)')); + // Check for empty recipient. + if (empty($message['to'])) { + if (empty($message['headers']['To'])) { + watchdog( + 'HTMLMailSystem', + 'Cannot send email about %subject without a recipient.', + array('subject' => $message['subject']), + WATCHDOG_ERROR + ); + return FALSE; + } + $message['to'] = $message['headers']['To']; + } + if (class_exists('MailMIME')) { + $mime = new MailMIME(); + $to = $mime->encodeHeader('to', $message['to']); + $subject = $mime->encodeHeader('subject', $message['subject']); + $txt_headers = $mime->txtHeaders($message['headers']); + } + else { + $to = mime_header_encode($message['to']); + $subject = mime_header_encode($message['subject']); + $txt_headers = $this->txtHeaders($message['headers']); + } + $body = preg_replace('#(\r\n|\r|\n)#s', $eol, $message['body']); + // Check for empty body. + if (empty($body)) { + watchdog( + 'HTMLMailSystem', + 'Refusing to send a blank email to %recipient about %subject.', + array('%recipient' => $message['to'], '%subject' => $message['subject']), + WATCHDOG_WARNING + ); + return FALSE; + } + if (variable_get('htmlmail_debug', 0)) { + $params = array( + $to, + $subject, + drupal_substr($body, 0, min(80, strpos("\n", $body))) . '...', + $txt_headers + ); + } + if (isset($message['headers']['Return-Path'])) { + // A return-path was set. + if (isset($_SERVER['WINDIR']) || strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE) { + // On Windows, PHP will use the value of sendmail_from for the + // Return-Path header. + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $message['headers']['Return-Path']); + $result = @mail($to, $subject, $body, $txt_headers); + ini_set('sendmail_from', $old_from); + } + elseif (ini_get('safe_mode')) { + // If safe mode is in effect, passing the fifth parameter to @mail + // will cause it to return FALSE and generate a PHP warning, even + // if the parameter is NULL. + $result = @mail($to, $subject, $body, $txt_headers); + } + else { + // On most non-Windows systems, the "-f" option to the sendmail command + // is used to set the Return-Path. + $extra = '-f' . $message['headers']['Return-Path']; + $result = @mail($to, $subject, $body, $txt_headers, $extra); + if (variable_get('htmlmail_debug', 0)) { + $params[] = $extra; + } + } + } + else { + // No return-path was set. + $result = @mail($to, $subject, $body, $txt_headers); + } + if (!$result && variable_get('htmlmail_debug', 0)) { + $call = '@mail(' . implode(', ', $params) . ')'; + foreach ($params as $i => $value) { + $params[$i] = var_export($value, 1); + } + if (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) { + $trace = print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 1); + } + else { + $trace = debug_backtrace(0); + for ($i = count($trace) - 1; $i >= 0; $i--) { + unset($trace[$i]['args']); + } + $trace = print_r($trace); + } + watchdog('htmlmail', 'Mail sending failed because:
    @call

    returned FALSE.
    @trace
    ', array('@call' => $call, '@trace' => $trace)); + } + return $result; + } + + /** + * Converts an array of email headers to a text string. + * + * @param $headers + * An associative array of ('HeaderName' => 'header value') pairs. + * + * @return + * The concatenated headers as a single string. + */ + public function txtHeaders(array $headers) { + $output = array(); + foreach ($headers as $name => $value) { + if (is_array($value)) { + foreach ($value as $val) { + $output[] = "$name: $val"; + } + } + else { + $output[] = "$name: $value"; + } + } + return implode("\n", $output); + } +} diff --git a/sites/all/modules/htmlmail/htmlmail.markdown b/sites/all/modules/htmlmail/htmlmail.markdown new file mode 100644 index 0000000..bcc6fa2 --- /dev/null +++ b/sites/all/modules/htmlmail/htmlmail.markdown @@ -0,0 +1,92 @@ +When formatting an email message with a given `$module` and `$key`, +[HTML Mail](http://drupal.org/project/htmlmail) +will use the first template file it finds from the following list: + +1. `htmlmail--$module--$key.tpl.php` +2. `htmlmail--$module.tpl.php` +3. `htmlmail.tpl.php` + +For each filename, +[HTML Mail](http://drupal.org/project/htmlmail) +looks first in the chosen *Email theme* directory, then in its own +module directory, before proceeding to the next filename. + +For example, if `example_module` sends mail with: + + drupal_mail("example_module", "outgoing_message" ...) + +the possible template file names would be: + +1. `htmlmail--example_module--outgoing_message.tpl.php` +2. `htmlmail--example_module.tpl.php` +3. `htmlmail.tpl.php` + +Template files are cached, so remember to clear the cache by visiting +admin/config/development/performance +after changing any `.tpl.php` files. + +The following variables available in this template: + +**`$body`** +: The message body text. + +**`$module`** +: The first argument to + [`drupal_mail()`](http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail/7), + which is, by convention, the machine-readable name of the sending module. + +**`$key`** +: The second argument to + [`drupal_mail()`](http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail/7), + which should give some indication of why this email is being sent. + +**`$message_id`** +: The email message id, which should be equal to `"{$module}_{$key}"`. + +**`$headers`** +: An array of email `(name => value)` pairs. + +**`$from`** +: The configured sender address. + +**`$to`** +: The recipient email address. + +**`$subject`** +: The message subject line. + +**`$body`** +: The formatted message body. + +**`$language`** +: The language object for this message. + +**`$params`** +: Any module-specific parameters. + +**`$template_name`** +: The basename of the active template. + +**`$template_path`** +: The relative path to the template directory. + +**`$template_url`** +: The absolute URL to the template directory. + +**`$theme`** +: The name of the *Email theme* used to hold template files. If the + [Echo](http://drupal.org/project/echo) module is enabled this theme will + also be used to transform the message body into a fully-themed webpage. + +**`$theme_path`** +: The relative path to the selected *Email theme* directory. + +**`$theme_url`** +: The absolute URL to the selected *Email theme* directory. + +**`$debug`** +: `TRUE` to add some useful debugging info to the bottom of the message. + +Other modules may also add or modify theme variables by implementing a +`MODULENAME_preprocess_htmlmail(&$variables)` +[hook function](http://api.drupal.org/api/drupal/modules--system--theme.api.php/function/hook_preprocess_HOOK/7). diff --git a/sites/all/modules/htmlmail/htmlmail.module b/sites/all/modules/htmlmail/htmlmail.module new file mode 100644 index 0000000..e8bbbde --- /dev/null +++ b/sites/all/modules/htmlmail/htmlmail.module @@ -0,0 +1,233 @@ + url('http://drupal.org/project/htmlmail'), + '%htmlmail' => 'HTML Mail', + ); + + return array( + 'choose htmlmail_plaintext' => array( + 'title' => t('Choose to receive plaintext emails via %htmlmail', $args), + 'description' => t( + 'Granting this permission allows users to choose whether to receive all their emails in plaintext, rather than the default format provided by the %htmlmail module.', $args + ), + ), + ); +} + +/** + * Implements hook_help(). + */ +function htmlmail_help($path, $arg) { + switch ($path) { + case 'admin/config/system/htmlmail': + return '

    ' . t('Theming') . '

    ' . t('The email message goes through three transformations before sending:') . '

    '; + case 'admin/help#htmlmail': + return '

    ' + . t('HTML Mail lets you theme your messages the same way you theme the rest of your website.', + array('!htmlmail' => 'http://drupal.org/project/htmlmail') + ) . '

    '; + default: + return ''; + } +} + +/** + * Implements hook_menu(). + */ +function htmlmail_menu() { + $items['admin/config/system/htmlmail'] = array( + 'title' => 'HTML Mail', + 'description' => 'Configure HTML Mail system-wide settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('htmlmail_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'htmlmail.admin.inc', + ); + $items['admin/config/system/htmlmail/settings'] = array( + 'title' => 'Settings', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => '-2' + ); + $items['admin/config/system/htmlmail/test'] = array( + 'title' => 'Send Test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('htmlmail_test_form'), + 'access arguments' => array('access administration pages'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'htmlmail.admin.inc', + ); + return $items; +} + +/** + * Implements hook_theme(). + * + * Auto-detects htmlmail template files in the selected theme and in the + * htmlmail module directory. + */ +function htmlmail_theme() { + $items = array(); + $module_path = drupal_get_path('module', 'htmlmail'); + $pattern = '/^htmlmail.*\.tpl\.php$/'; + $files = file_scan_directory($module_path, $pattern, array('key' => 'name')); + if ($theme = htmlmail_get_selected_theme()) { + $theme_path = drupal_get_path('theme', $theme); + $files = array_merge($files, + file_scan_directory($theme_path, $pattern, array('key' => 'name')) + ); + } + else { + $theme_path = $module_path; + } + ksort($files); + foreach ($files as $file) { + $path = dirname($file->uri); + $template = substr($file->name, 0, -4); + $suggestion = str_replace('--', '__', $template); + $items[$suggestion] = array( + 'variables' => array('message' => array()), + 'template' => $template, + 'path' => $path, + 'theme path' => $theme_path, + ); + } + return $items; +} + +/** + * Process variables to format email messages. + * + * @see htmlmail.tpl.php + */ +function template_preprocess_htmlmail(array &$variables) { + $variables['debug'] = variable_get('htmlmail_debug', '0'); + $variables['theme'] = htmlmail_get_selected_theme($variables); + $variables['module_path'] = drupal_get_path('module', 'htmlmail'); + if (empty($variables['theme'])) { + $variables['theme'] = 'no theme'; + $variables['theme_path'] = $variables['module_path']; + } + else { + $variables['theme_path'] = drupal_get_path('theme', $variables['theme']); + } + $variables['theme_url'] = url( + $variables['theme_path'], array('absolute' => TRUE) + ); + $variables['message_id'] = $variables['module'] . '_' . $variables['key']; + $suggestion = 'htmlmail__' . $variables['module']; + $variables['theme_hook_suggestions'][] = $suggestion; + $suggestion .= '__' . $variables['key']; + $variables['theme_hook_suggestions'][] = $suggestion; +} + +/** + * Implements hook_mail(). + */ +function htmlmail_mail($key, &$message, $params) { + $message['module'] = 'htmlmail'; + $message['key'] = $key; + $message['subject'] = $params['subject']; + $message['body'] = explode( + MAIL_LINE_ENDINGS . MAIL_LINE_ENDINGS, + $params['body'] + ); + return $message; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function htmlmail_form_user_profile_form_alter(&$form, &$form_state) { + if ($form['#user_category'] != 'account') { + return; + } + if (!(user_access('choose htmlmail_plaintext') || user_access('administer users'))) { + return; + } + $account = $form['#user']; + $mail = $form['account']['mail']; + $form['account']['mail'] = array( + 'mail' => $mail, + 'htmlmail_plaintext' => array( + '#type' => 'checkbox', + '#title' => t('Plaintext-only emails'), + '#default_value' => empty($account->data['htmlmail_plaintext']) ? 0 : 1, + '#description' => t('The %htmlmail module can send emails with fonts, styles, and other HTML formatting. If you prefer to receive all your emails in unformatted plain text, select this option.', + array('%htmlmail' => 'HTML Mail') + ), + ), + ); +} + +/** + * Implements hook_user_presave(). + */ +function htmlmail_user_presave(&$edit, $account, $category) { + if (user_access('choose htmlmail_plaintext') || user_access('administer users')) { + $edit['data']['htmlmail_plaintext'] = empty($edit['htmlmail_plaintext']) ? 0 : 1; + unset($edit['htmlmail_plaintext']); + } +} + +/** + * Returns an associative array of allowed themes. The keys are the + * machine-readable names and the values are the .info file names. + * Based on code from the og_theme module. + */ +function &htmlmail_get_allowed_themes() { + $allowed = &drupal_static(__FUNCTION__); + if (!isset($allowed)) { + $allowed = array('' => t('No theme')); + $themes = list_themes(); + module_load_include('inc', 'system', 'system.admin'); + uasort($themes, 'system_sort_modules_by_info_name'); + foreach ($themes as $key => $value) { + if ($value->status) { + $allowed[$key] = check_plain($value->info['name']); + } + } + } + return $allowed; +} + +/** + * Returns the selected theme to use for outgoing emails. + */ +function htmlmail_get_selected_theme(&$message = array()) { + $selected = isset($message['theme']) + ? $message['theme'] : variable_get('htmlmail_theme', ''); + if ($selected) { + // Make sure the selected theme is allowed. + $themes = &htmlmail_get_allowed_themes(); + if (empty($themes[$selected])) { + $selected = ''; + } + } + return $selected; +} + +/** + * Checks whether a given recipient email prefers plaintext-only messages. + * + * @param $email + * The recipient email address. + * + * @return + * FALSE if the recipient prefers plaintext-only messages; otherwise TRUE. + */ +function htmlmail_is_allowed($email) { + return !($recipient = user_load_by_mail($email)) + || empty($recipient->data['htmlmail_plaintext']); +} diff --git a/sites/all/modules/htmlmail/htmlmail.tpl.php b/sites/all/modules/htmlmail/htmlmail.tpl.php new file mode 100644 index 0000000..1e30c01 --- /dev/null +++ b/sites/all/modules/htmlmail/htmlmail.tpl.php @@ -0,0 +1,178 @@ + value) pairs. + * + * $from + * The configured sender address. + * + * $to + * The recipient email address. + * + * $subject + * The message subject line. + * + * $body + * The formatted message body. + * + * $language + * The language object for this message. + * + * $params + * Any module-specific parameters. + * + * $template_name + * The basename of the active template. + * + * $template_path + * The relative path to the template directory. + * + * $template_url + * The absolute URL to the template directory. + * + * $theme + * The name of the Email theme used to hold template files. If the + * [5]Echo module is enabled this theme will also be used to + * transform the message body into a fully-themed webpage. + * + * $theme_path + * The relative path to the selected Email theme directory. + * + * $theme_url + * The absolute URL to the selected Email theme directory. + * + * $debug + * TRUE to add some useful debugging info to the bottom of the + * message. + * + * Other modules may also add or modify theme variables by implementing a + * MODULENAME_preprocess_htmlmail(&$variables) [6]hook function. + * + * References + * + * 1. http://drupal.org/project/htmlmail + * 2. http://drupal.org/project/htmlmail + * 3. http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail/7 + * 4. http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail/7 + * 5. http://drupal.org/project/echo + * 6. http://api.drupal.org/api/drupal/modules--system--theme.api.php/function/hook_preprocess_HOOK/7 + * + * =========================================================== End instructions. + */ + $template_name = basename(__FILE__); + $current_path = realpath(NULL); + $current_len = strlen($current_path); + $template_path = realpath(dirname(__FILE__)); + if (!strncmp($template_path, $current_path, $current_len)) { + $template_path = substr($template_path, $current_len + 1); + } + $template_url = url($template_path, array('absolute' => TRUE)); +?> +
    + +
    + +
    +
    +

    + To customize this message: +

    1. + Visit admin/config/system/htmlmail + and select a theme to hold your custom email template files. +

    2. + Visit admin/appearance + to enable your selected + theme. +

    3. + Edit your
      + +
      file. +

    4. + Copy
      + +
      to
      + +

    5. + For module-specific customization, copy
      + +
      to
      + +

    6. + For message-specific customization, copy
      + +
      to
      + +

    7. + Edit the copied file. +

    8. + Send a test message to make sure your customizations worked. +

    9. + If you think your customizations would be of use to others, + please contribute your file as a feature request in the + issue queue. +

    + The module sets the $params + variable. For this message, +

    +$params = 
    +  

    +
    + Date: Mon, 2 Oct 2017 18:45:07 -0500 Subject: [PATCH 11/11] Part of this should be working on the mail interface. I was also trying to implement a module to do mailing for me; did not get past installation --- sites/all/modules/mailsystem/LICENSE.txt | 339 ++++++++ sites/all/modules/mailsystem/README.html | 113 +++ sites/all/modules/mailsystem/README.markdown | 118 +++ sites/all/modules/mailsystem/README.txt | 134 ++++ sites/all/modules/mailsystem/html_to_text.inc | 742 ++++++++++++++++++ .../modules/mailsystem/mailsystem.admin.inc | 202 +++++ sites/all/modules/mailsystem/mailsystem.info | 14 + .../all/modules/mailsystem/mailsystem.module | 353 +++++++++ .../modules/mailsystem/mailsystem.theme.inc | 82 ++ sites/all/modules/mimemail/CHANGELOG.txt | 232 ++++++ sites/all/modules/mimemail/LICENSE.txt | 339 ++++++++ sites/all/modules/mimemail/README.txt | 136 ++++ .../mimemail/includes/mimemail.admin.inc | 154 ++++ .../mimemail/includes/mimemail.incoming.inc | 211 +++++ .../mimemail/includes/mimemail.mail.inc | 63 ++ sites/all/modules/mimemail/mimemail.inc | 575 ++++++++++++++ sites/all/modules/mimemail/mimemail.info | 22 + sites/all/modules/mimemail/mimemail.install | 113 +++ sites/all/modules/mimemail/mimemail.module | 400 ++++++++++ sites/all/modules/mimemail/mimemail.rules.inc | 391 +++++++++ .../mimemail_action/mimemail_action.info | 14 + .../mimemail_action/mimemail_action.module | 198 +++++ .../mimemail_compress/mimemail_compress.inc | 286 +++++++ .../mimemail_compress/mimemail_compress.info | 14 + .../mimemail_compress.install | 31 + .../mimemail_compress.module | 21 + .../mimemail_example/mimemail_example.info | 12 + .../mimemail_example/mimemail_example.install | 20 + .../mimemail_example/mimemail_example.module | 170 ++++ .../all/modules/mimemail/tests/mimemail.test | 99 +++ .../mimemail/tests/mimemail_compress.test | 31 + .../mimemail/tests/mimemail_rules.test | 225 ++++++ .../mimemail/theme/mimemail-message.tpl.php | 40 + .../modules/mimemail/theme/mimemail.theme.inc | 102 +++ .../module_missing_message_fixer/LICENSE.txt | 339 ++++++++ .../module_missing_message_fixer/README.txt | 53 ++ .../module_missing_message_fixer.admin.inc | 117 +++ .../module_missing_message_fixer.drush.inc | 171 ++++ .../module_missing_message_fixer.info | 13 + .../module_missing_message_fixer.module | 36 + sites/all/themes/exp/css/mail.css | 0 .../template/mail/mimemail-message.tpl.php | 40 + 42 files changed, 6765 insertions(+) create mode 100644 sites/all/modules/mailsystem/LICENSE.txt create mode 100644 sites/all/modules/mailsystem/README.html create mode 100644 sites/all/modules/mailsystem/README.markdown create mode 100644 sites/all/modules/mailsystem/README.txt create mode 100644 sites/all/modules/mailsystem/html_to_text.inc create mode 100644 sites/all/modules/mailsystem/mailsystem.admin.inc create mode 100644 sites/all/modules/mailsystem/mailsystem.info create mode 100644 sites/all/modules/mailsystem/mailsystem.module create mode 100644 sites/all/modules/mailsystem/mailsystem.theme.inc create mode 100644 sites/all/modules/mimemail/CHANGELOG.txt create mode 100644 sites/all/modules/mimemail/LICENSE.txt create mode 100644 sites/all/modules/mimemail/README.txt create mode 100644 sites/all/modules/mimemail/includes/mimemail.admin.inc create mode 100644 sites/all/modules/mimemail/includes/mimemail.incoming.inc create mode 100644 sites/all/modules/mimemail/includes/mimemail.mail.inc create mode 100644 sites/all/modules/mimemail/mimemail.inc create mode 100644 sites/all/modules/mimemail/mimemail.info create mode 100644 sites/all/modules/mimemail/mimemail.install create mode 100644 sites/all/modules/mimemail/mimemail.module create mode 100644 sites/all/modules/mimemail/mimemail.rules.inc create mode 100644 sites/all/modules/mimemail/modules/mimemail_action/mimemail_action.info create mode 100644 sites/all/modules/mimemail/modules/mimemail_action/mimemail_action.module create mode 100644 sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.inc create mode 100644 sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.info create mode 100644 sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.install create mode 100644 sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.module create mode 100644 sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.info create mode 100644 sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.install create mode 100644 sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.module create mode 100644 sites/all/modules/mimemail/tests/mimemail.test create mode 100644 sites/all/modules/mimemail/tests/mimemail_compress.test create mode 100644 sites/all/modules/mimemail/tests/mimemail_rules.test create mode 100644 sites/all/modules/mimemail/theme/mimemail-message.tpl.php create mode 100644 sites/all/modules/mimemail/theme/mimemail.theme.inc create mode 100644 sites/all/modules/module_missing_message_fixer/LICENSE.txt create mode 100644 sites/all/modules/module_missing_message_fixer/README.txt create mode 100644 sites/all/modules/module_missing_message_fixer/includes/module_missing_message_fixer.admin.inc create mode 100644 sites/all/modules/module_missing_message_fixer/includes/module_missing_message_fixer.drush.inc create mode 100644 sites/all/modules/module_missing_message_fixer/module_missing_message_fixer.info create mode 100644 sites/all/modules/module_missing_message_fixer/module_missing_message_fixer.module create mode 100644 sites/all/themes/exp/css/mail.css create mode 100644 sites/all/themes/exp/template/mail/mimemail-message.tpl.php diff --git a/sites/all/modules/mailsystem/LICENSE.txt b/sites/all/modules/mailsystem/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/mailsystem/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/mailsystem/README.html b/sites/all/modules/mailsystem/README.html new file mode 100644 index 0000000..6b77862 --- /dev/null +++ b/sites/all/modules/mailsystem/README.html @@ -0,0 +1,113 @@ +

    Mail System

    +

    Provides an Administrative UI and Developers API for safely updating the mail_system configuration variable.

    +

    Administrative UI

    +

    The administrative interface is at admin/config/system/mailsystem. A screenshot is available.

    +

    Used by:

    + +

    Developers API

    +

    A module example with a MailSystemInterface implementation called ExampleMailSystem should add the following in its example.install file:

    +
    +/**
    + * Implements hook_enable().
    + */
    +function example_enable() {
    +  mailsystem_set(array('example' => 'ExampleMailSystem'));
    +}
    +/**
    + * Implements hook_disable().
    + */
    +function example_disable() {
    +  mailsystem_clear(array('example' => 'ExampleMailSystem'));
    +}
    +
    +
    +

    The above settings allow mail sent by example to use ExampleMailSystem. To make ExampleMailSystem the site-wide default for sending mail:

    +
    +mailsystem_set(array(mailsystem_default_id() => 'ExampleMailSystem'));
    +
    +
    +

    To restore the default mail system:

    +
    +mailsystem_set(array(mailsystem_default_id() => mailsystem_default_value()));
    +
    +
    +

    Or simply:

    +
    +mailsystem_set(mailsystem_defaults());
    +
    +
    +

    If module example relies on dependency foo and its FooMailSystem class, then the example.install code should like like this:

    +
    +/**
    + * Implements hook_enable().
    + */
    +function example_enable() {
    +  mailsystem_set(array('example' => 'FooMailSystem'));
    +}
    +/**
    + * Implements hook_disable().
    + */
    +function example_disable() {
    +  mailsystem_clear(array('example' => ''));
    +}
    +
    +
    +

    If module example only wants to use FooMailSystem when sending emails with a key of examail, then the example.install code should look like this:

    +
    +/**
    + * Implements hook_enable().
    + */
    +function example_enable() {
    +  mailsystem_set(array('example_examail' => 'FooMailSystem'));
    +}
    +/**
    + * Implements hook_disable().
    + */
    +function example_disable() {
    +  mailsystem_clear(array('example_examail' => ''));
    +}
    +
    +
    +

    (New in 2.x branch)

    +

    To change the site-wide defaults to use the FooMailSystem for formatting messages and the BarMailSystem for sending them:

    +
    +mailsystem_set(
    +  array(
    +    mailsystem_default_id() => array(
    +      'format' => 'FooMailSystem',
    +      'mail' => 'BarMailSystem',
    +    ),
    +  )
    +);
    +
    +
    +

    To change the site-wide defaults to use the FooMailSystem for sending messages, while continuing to use the current system for formatting them:

    +
    +mailsystem_set(
    +  array(
    +    mailsystem_default_id() => array(
    +      'mail' => 'FooMailsystem',
    +    ),
    +  )
    +);
    +
    +
    +

    References

    +
    +
    drupal_mail_system() API documentation:
    +
    +

    api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7

    +
    +
    MailSystemInterface API documentation:
    +
    +

    api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7

    +
    +
    Creating HTML formatted mails in Drupal 7:
    +
    +

    drupal.org/node/900794

    +
    +
    diff --git a/sites/all/modules/mailsystem/README.markdown b/sites/all/modules/mailsystem/README.markdown new file mode 100644 index 0000000..6d549f9 --- /dev/null +++ b/sites/all/modules/mailsystem/README.markdown @@ -0,0 +1,118 @@ +## [Mail System](http://drupal.org/project/mailsystem) + +Provides an Administrative UI and Developers API for safely updating the +[mail_system](http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7) +configuration variable. + +### Administrative UI + +The administrative interface is at `admin/config/system/mailsystem`. +A [screenshot](http://drupal.org/node/1134044) is available. + +### Used by: + +* [HTML Mail](http://drupal.org/project/htmlmail) +* [Mime Mail 7.x-1.x-dev](http://drupal.org/project/mimemail) +* [Postmark 7.x-1.x](http://drupal.org/project/postmark) + +### Developers API + +A module `example` with a +[`MailSystemInterface`](http://api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7) +implementation called `ExampleMailSystem` should add the following in its +`example.install` file: + + /** + * Implements hook_enable(). + */ + function example_enable() { + mailsystem_set(array('example' =\> 'ExampleMailSystem')); + } + + /** + * Implements hook_disable(). + */ + function example_disable() { + mailsystem_clear(array('example' =\> 'ExampleMailSystem')); + } + +The above settings allow mail sent by `example` to use `ExampleMailSystem`. To make +`ExampleMailSystem` the site-wide default for sending mail: + + mailsystem_set(array(mailsystem_default_id() =\> 'ExampleMailSystem')); + +To restore the default mail system: + + mailsystem_set(array(mailsystem_default_id() =\> mailsystem_default_value())); + +Or simply: + + mailsystem_set(mailsystem_defaults()); + +If module `example` relies on dependency `foo` and its `FooMailSystem` class, then +the `example.install` code should like like this: + + /** + * Implements hook_enable(). + */ + function example_enable() { + mailsystem_set(array('example' =\> 'FooMailSystem')); + } + + /** + * Implements hook_disable(). + */ + function example_disable() { + mailsystem_clear(array('example' =\> '')); + } + +If module `example` only wants to use `FooMailSystem` when sending emails with a key +of `examail`, then the `example.install` code should look like this: + + /** + * Implements hook_enable(). + */ + function example_enable() { + mailsystem_set(array('example_examail' =\> 'FooMailSystem')); + } + + /** + * Implements hook_disable(). + */ + function example_disable() { + mailsystem_clear(array('example_examail' =\> '')); + } + +#### *(New in 2.x branch)* + +To change the site-wide defaults to use the `FooMailSystem` for formatting messages and the `BarMailSystem` for sending them: + + mailsystem_set( + array( + mailsystem_default_id() => array( + 'format' => 'FooMailSystem', + 'mail' => 'BarMailSystem', + ), + ) + ); + +To change the site-wide defaults to use the `FooMailSystem` for sending messages, while continuing to use the current system for formatting them: + + mailsystem_set( + array( + mailsystem_default_id() => array( + 'mail' => 'FooMailsystem', + ), + ) + ); + +### References + +**[`drupal_mail_system()` API documentation](http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7)**: +: [api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7](http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7) + +**[`MailSystemInterface` API documentation](http://api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7)**: +: [api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7](http://api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7) + +**[Creating HTML formatted mails in Drupal 7](http://drupal.org/node/900794)**: +: [drupal.org/node/900794](http://drupal.org/node/900794) diff --git a/sites/all/modules/mailsystem/README.txt b/sites/all/modules/mailsystem/README.txt new file mode 100644 index 0000000..ca302af --- /dev/null +++ b/sites/all/modules/mailsystem/README.txt @@ -0,0 +1,134 @@ +[1]Mail System + + Provides an Administrative UI and Developers API for safely updating + the [2]mail_system configuration variable. + + Administrative UI + + The administrative interface is at admin/config/system/mailsystem. A + [3]screenshot is available. + + Used by: + + * [4]HTML Mail + * [5]Mime Mail 7.x-1.x-dev + * [6]Postmark 7.x-1.x + + Developers API + + A module example with a [7]MailSystemInterface implementation called + ExampleMailSystem should add the following in its example.install file: +/** + * Implements hook_enable(). + */ +function example_enable() { + mailsystem_set(array('example' => 'ExampleMailSystem')); +} +/** + * Implements hook_disable(). + */ +function example_disable() { + mailsystem_clear(array('example' => 'ExampleMailSystem')); +} + + + The above settings allow mail sent by example to use ExampleMailSystem. + To make ExampleMailSystem the site-wide default for sending mail: +mailsystem_set(array(mailsystem_default_id() => 'ExampleMailSystem')); + + + To restore the default mail system: +mailsystem_set(array(mailsystem_default_id() => mailsystem_default_value())); + + + Or simply: +mailsystem_set(mailsystem_defaults()); + + + If module example relies on dependency foo and its FooMailSystem class, + then the example.install code should like like this: +/** + * Implements hook_enable(). + */ +function example_enable() { + mailsystem_set(array('example' => 'FooMailSystem')); +} +/** + * Implements hook_disable(). + */ +function example_disable() { + mailsystem_clear(array('example' => '')); +} + + + If module example only wants to use FooMailSystem when sending emails + with a key of examail, then the example.install code should look like + this: +/** + * Implements hook_enable(). + */ +function example_enable() { + mailsystem_set(array('example_examail' => 'FooMailSystem')); +} +/** + * Implements hook_disable(). + */ +function example_disable() { + mailsystem_clear(array('example_examail' => '')); +} + + + (New in 2.x branch) + + To change the site-wide defaults to use the FooMailSystem for + formatting messages and the BarMailSystem for sending them: +mailsystem_set( + array( + mailsystem_default_id() => array( + 'format' => 'FooMailSystem', + 'mail' => 'BarMailSystem', + ), + ) +); + + + To change the site-wide defaults to use the FooMailSystem for sending + messages, while continuing to use the current system for formatting + them: +mailsystem_set( + array( + mailsystem_default_id() => array( + 'mail' => 'FooMailsystem', + ), + ) +); + + + References + + [8]drupal_mail_system() API documentation: + [9]api.drupal.org/api/drupal/includes--mail.inc/function/drupal_ + mail_system/7 + + [10]MailSystemInterface API documentation: + [11]api.drupal.org/api/drupal/includes--mail.inc/interface/MailS + ystemInterface/7 + + [12]Creating HTML formatted mails in Drupal 7: + [13]drupal.org/node/900794 + +References + + 1. http://drupal.org/project/mailsystem + 2. http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7 + 3. http://drupal.org/node/1134044 + 4. http://drupal.org/project/htmlmail + 5. http://drupal.org/project/mimemail + 6. http://drupal.org/project/postmark + 7. http://api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7 + 8. http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7 + 9. http://api.drupal.org/api/drupal/includes--mail.inc/function/drupal_mail_system/7 + 10. http://api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7 + 11. http://api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7 + 12. http://drupal.org/node/900794 + 13. http://drupal.org/node/900794 diff --git a/sites/all/modules/mailsystem/html_to_text.inc b/sites/all/modules/mailsystem/html_to_text.inc new file mode 100644 index 0000000..0857622 --- /dev/null +++ b/sites/all/modules/mailsystem/html_to_text.inc @@ -0,0 +1,742 @@ +' characters are + * repeated on subsequent wrapped lines. Others are replaced by spaces. + * - max: The maximum length at which to wrap each line. Defaults to 80. + * - stuff: Whether to space-stuff special lines. Defaults to TRUE. + * - hard: Whether to enforce the maximum line length even if no convenient + * space character is available. Defaults to FALSE. + * - pad: A string to use for padding short lines to 'max' characters. If + * more than one character, only the last will be repeated. + * - break: The line break sequence to insert. The default is one of the + * following: + * - "\r\n": Windows, when $text does not contain a space character. + * - "\n": Non-Windows, when $text does not contain a space character. + * - " \r\n": On Windows, when $text contains at least one space. + * - " \n": Non-Windows, when $text contains at least one space. + * + * @see drupal_mail() + */ +function mailsystem_wrap_mail($text, array $options = array()) { + static $defaults; + if (!isset($defaults)) { + $defaults = array( + 'indent' => '', + 'pad' => '', + 'pad_repeat' => '', + 'max' => 80, + 'stuff' => TRUE, + 'hard' => FALSE, + 'eol' => variable_get('mail_line_endings', MAIL_LINE_ENDINGS), + ); + } + $options += $defaults; + if (!isset($options['break'])) { + // Allow soft-wrap spaces only when $text contains at least one space. + $options['break'] = (strpos($text, ' ') === FALSE ? '' : ' ') . $defaults['eol']; + } + $options['wrap'] = $options['max'] - drupal_strlen($options['indent']); + if ($options['pad']) { + $options['pad_repeat'] = drupal_substr($options['pad'], -1, 1); + } + // The 'clean' indent is applied to all lines after the first one. + $options['clean'] = _mailsystem_html_to_text_clean($options['indent']); + // Wrap lines according to RFC 3676. + $lines = explode($defaults['eol'], $text); + array_walk($lines, '_mailsystem_wrap_mail_line', $options); + // Expand the lines array on newly-inserted line breaks. + $lines = explode($defaults['eol'], implode($defaults['eol'], $lines)); + // Apply indentation, space-stuffing, and padding. + array_walk($lines, '_mailsystem_indent_mail_line', $options); + return implode($defaults['eol'], $lines); +} + +/** + * Transform an HTML string into plain text, preserving the structure of the + * markup. Useful for preparing the body of a node to be sent by e-mail. + * + * The output will be suitable for use as 'format=flowed; delsp=yes' text + * (RFC 3676) and can be passed directly to drupal_mail() for sending. + * + * We deliberately use variable_get('mail_line_endings', MAIL_LINE_ENDINGS) + * rather than "\r\n". + * + * This function provides suitable alternatives for the following tags: + * + *

    + *


    1.  
      + *  
      DOM Node into plain text. Attributes such as rowspan, + * colspan, padding, border, etc. are ignored. + * + * @param DOMNode $node + * The DOMNode corresponding to the
      tag and its contents. + * @param $allowed_tags + * The list of allowed tags passed to _mailsystem_html_to_text(). + * @param array &$notes + * A writeable array of footnote reference numbers, keyed by their + * respective hyperlink destination urls. + * @param $table_width + * The desired maximum table width, after word-wrapping each table cell. + * + * @return + * A plain text representation of the table. + * + * @see _mailsystem_html_to_text() + */ +function _mailsystem_html_to_text_table(DOMNode $node, $allowed_tags = NULL, array &$notes = array(), $table_width = 80) { + $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS); + $header = array(); + $footer = array(); + $body = array(); + $text = $eol; + $current = $node; + while (TRUE) { + if (isset($current->tagName)) { + switch ($current->tagName) { + case 'caption': // The table caption is added first. + $text = _mailsystem_html_to_text($current, $allowed_tags, $notes, $table_width); + break; + + case 'tr': + switch ($current->parentNode->tagName) { + case 'thead': + $header[] = $current; + break; + + case 'tfoot': + $footer[] = $current; + break; + + default: // Either 'tbody' or 'table' + $body[] = $current; + break; + } + break; + + default: + if ($current->hasChildNodes()) { + $current = $current->firstChild; + continue 2; + } + } + } + do { + if ($current->nextSibling) { + $current = $current->nextSibling; + continue 2; + } + $current = $current->parentNode; + } while ($current && !$current->isSameNode($node)); + break; + } + // Merge the thead, tbody, and tfoot sections together. + if ($rows = array_merge($header, $body, $footer)) { + $num_rows = count($rows); + // First just count the number of columns. + $num_cols = 0; + foreach ($rows as $row) { + $row_cols = 0; + foreach ($row->childNodes as $cell) { + if (isset($cell->tagName) && in_array($cell->tagName, array('td', 'th'))) { + $row_cols++; + } + } + $num_cols = max($num_cols, $row_cols); + } + // If any columns were found, calculate each column height and width. + if ($num_cols) { + // Set up a binary search for best wrap width for each column. + $max = max($table_width - $num_cols - 1, 1); + $max_wraps = array_fill(0, $num_cols, $max); + $try = max(intval(($table_width - 1) / $num_cols - 1), 1); + $try_wraps = array_fill(0, $num_cols, $try); + $min_wraps = array_fill(0, $num_cols, 1); + // Start searching... + $change = FALSE; + do { + $change = FALSE; + $widths = array_fill(0, $num_cols, 0); + $heights = array_fill(0, $num_rows, 0); + $table = array_fill(0, $num_rows, array_fill(0, $num_cols, '')); + $breaks = array_fill(0, $num_cols, FALSE); + foreach ($rows as $i => $row) { + $j = 0; + foreach ($row->childNodes as $cell) { + if (!isset($cell->tagName) || !in_array($cell->tagName, array('td', 'th'))) { + // Skip text nodes. + continue; + } + // Render the cell contents. + $cell = _mailsystem_html_to_text($cell, $allowed_tags, $notes, $try_wraps[$j]); + // Trim leading line-breaks and trailing whitespace. + // chr(160) is the non-breaking space character. + $cell = rtrim(ltrim($cell, $eol), ' ' . $eol . chr(160)); + $table[$i][$j] = $cell; + if ($cell > '') { + // Split the cell into lines. + $lines = explode($eol, $cell); + // The row height is the maximum number of lines among all the + // cells in that row. + $heights[$i] = max($heights[$i], count($lines)); + foreach ($lines as $line) { + $this_width = drupal_strlen($line); + // The column width is the maximum line width among all the + // lines in that column. + if ($this_width > $widths[$j]) { + $widths[$j] = $this_width; + // If the longest line in a column contains at least one + // space character, then the table can be made narrower. + $breaks[$j] = strpos(' ', $line) !== FALSE; + } + } + } + $j++; + } + } + // Calculate the total table width; + $this_width = array_sum($widths) + $num_cols + 1; + if ($this_width > $table_width) { + // Wider than desired. + if (!in_array(TRUE, $breaks)) { + // If there are no more break points, then the table is already as + // narrow as it can get, so we're done. + break; + } + foreach ($try_wraps as $i => $wrap) { + $max_wraps[$i] = min($max_wraps[$i], $wrap); + if ($breaks[$i]) { + $new_wrap = intval(($min_wraps[$i] + $max_wraps[$i]) / 2); + $new_wrap = min($new_wrap, $widths[$i] - 1); + $new_wrap = max($new_wrap, $min_wraps[$i]); + } + else { + // There's no point in trying to make the column narrower than + // the widest un-wrappable line in the column. + $min_wraps[$i] = $widths[$i]; + $new_wrap = $widths[$i]; + } + if ($try_wraps[$i] > $new_wrap) { + $try_wraps[$i] = $new_wrap; + $change = TRUE; + } + } + } + elseif ($this_width < $table_width) { + // Narrower than desired. + foreach ($try_wraps as $i => $wrap) { + if ($min_wraps[$i] < $wrap) { + $min_wraps[$i] = $wrap; + } + $new_wrap = intval(($min_wraps[$i] + $max_wraps[$i]) / 2); + $new_wrap = max($new_wrap, $widths[$i] + 1); + $new_wrap = min($new_wrap, $max_wraps[$i]); + if ($try_wraps[$i] < $new_wrap) { + $try_wraps[$i] = $new_wrap; + $change = TRUE; + } + } + } + } while ($change); + // Pad each cell to column width and line height. + for ($i = 0; $i < $num_rows; $i++) { + if ($heights[$i]) { + for ($j = 0; $j < $num_cols; $j++) { + $cell = $table[$i][$j]; + // Pad each cell to the maximum number of lines in that row. + $lines = array_pad(explode($eol, $cell), $heights[$i], ''); + foreach ($lines as $k => $line) { + // Pad each line to the maximum width in that column. + $repeat = $widths[$j] - drupal_strlen($line); + if ($repeat > 0) { + // chr(160) is the non-breaking space character. + $lines[$k] .= str_repeat(chr(160), $repeat); + } + } + $table[$i][$j] = $lines; + } + } + } + // Generate the row separator line. + $separator = '+'; + for($i = 0; $i < $num_cols; $i++) { + $separator .= str_repeat('-', $widths[$i]) . '+'; + } + $separator .= $eol; + for ($i = 0; $i < $num_rows; $i++) { + $text .= $separator; + if (!$heights[$i]) { + continue; + } + $row = $table[$i]; + // For each row, iterate first by lines within the row. + for ($k = 0; $k < $heights[$i]; $k++) { + // Add a vertical-bar at the beginning of each row line. + $row_line = '|'; + $trimmed = ''; + // Within each row line, iterate by cells within that line. + for ($j = 0; $j < $num_cols; $j++) { + // Add a vertical bar at the end of each cell line. + $row_line .= $row[$j][$k] . '|'; + // chr(160) is the non-breaking space character. + $trimmed .= trim($row[$j][$k], ' ' . $eol . chr(160)); + } + if ($trimmed > '') { + // Only print rows that are non-empty. + $text .= $row_line . $eol; + } + } + } + // Final output ends with a row separator. + $text .= $separator; + } + } + // Make sure formatted table content doesn't line-wrap. + // chr(160) is the non-breaking space character. + return str_replace(' ', chr(160), $text); +} + +/** + * Helper function for array_walk in drupal_wrap_mail(). + * + * Inserts $values['break'] sequences to break up $line into parts of no more + * than $values['wrap'] characters. Only breaks at space characters, unless + * $values['hard'] is TRUE. + */ +function _mailsystem_wrap_mail_line(&$line, $key, $values) { + $line = wordwrap($line, $values['wrap'], $values['break'], $values['hard']); +} + +/** + * Helper function for array_walk in drupal_wrap_mail(). + * + * If $values['pad'] is non-empty, $values['indent'] will be added at the start + * of each line, and $values['pad'] at the end, repeating the last character of + * $values['pad'] until the line length equals $values['max']. + * + * If $values['pad'] is empty, $values['indent'] will be added at the start of + * the first line, and $values['clean'] at the start of subsequent lines. + * + * If $values['stuff'] is true, then an extra space character will be added at + * the start of any line beginning with a space, a '>', or the word 'From'. + * + * @see http://www.ietf.org/rfc/rfc3676.txt + */ +function _mailsystem_indent_mail_line(&$line, $key, $values) { + if ($line == '') { + return; + } + if ($values['pad']) { + $line = $values['indent'] . $line; + $count = $values['max'] - drupal_strlen($line) - drupal_strlen($values['pad']); + if ($count >= 0) { + $line .= $values['pad'] . str_repeat($values['pad_repeat'], $count); + } + } + else { + $line = $values[$key === 0 ? 'indent' : 'clean'] . $line; + } + if ($values['stuff']) { + // chr(160) is the non-breaking space character. + $line = preg_replace('/^(' . chr(160) . '| |>|From)/', ' $1', $line); + } +} + +/** + * Helper function for drupal_wrap_mail() and drupal_html_to_text(). + * + * Replace all non-quotation markers from a given piece of indentation with + * non-breaking space characters. + */ +function _mailsystem_html_to_text_clean($indent) { + // chr(160) is the non-breaking space character. + return preg_replace('/[^>]/', chr(160), $indent); +} diff --git a/sites/all/modules/mailsystem/mailsystem.admin.inc b/sites/all/modules/mailsystem/mailsystem.admin.inc new file mode 100644 index 0000000..2dca212 --- /dev/null +++ b/sites/all/modules/mailsystem/mailsystem.admin.inc @@ -0,0 +1,202 @@ + url('http://api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7'), + '@interface' => 'MailSystemInterface', + '!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'), + '@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); + $form['mailsystem'] = array( + '#type' => 'fieldset', + '#title' => t('Mail System Settings'), + '#description' => t( + 'Drupal provides a default @interface class called @default_class. Modules may provide additional classes. Each @interface class may be associated with one or more identifiers, composed of a %module and an optional %key. Each email being sent also has a %module and a %key. To decide which class to use, Drupal uses the following search order:
      1. The class associated with the %module and %key, if any.
      2. The class associated with the %module, if any.
      3. The site-wide default @interface class.
      ', $args + ), + '#collapsible' => FALSE, + '#tree' => TRUE, + ); + $form['mailsystem'][mailsystem_default_id()] = array( + '#type' => 'select', + '#title' => t( + 'Site-wide default @interface class', $args + ), + '#options' => $mailsystem_classes, + '#default_value' => $mail_system[mailsystem_default_id()], + ); + $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'; + $form['mailsystem'][$id] = array( + '#type' => 'select', + '#title' => $title, + '#options' => $mailsystem_classes, + '#default_value' => $class, + ); + } + // 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( + '#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 @interface 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 @format method', $args + ), + '#options' => $mailsystem_classes, + ); + $form['class']['mail'] = array( + '#type' => 'select', + '#title' => t( + 'Class to use for the @mail method', $args + ), + '#options' => $mailsystem_classes, + ); + $form['identifier'] = array( + '#type' => 'fieldset', + '#title' => t('New Setting'), + '#description' => t('Add a new %module and %key to the settings list.', + array( + '%module' => 'module', + '%key' => 'key', + ) + ), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + ); + array_unshift($descriptions, t('-- Select --')); + $form['identifier']['module'] = array( + '#type' => 'select', + '#title' => t('Module'), + '#options' => $descriptions, + ); + $form['identifier']['key'] = array( + '#type' => 'textfield', + '#title' => t('Key'), + '#size' => 80, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save settings'), + ); + return $form; +} + +/** + * Processes mailsystem_admin_settings form. + */ +function mailsystem_admin_settings_submit($form, &$form_state) { + variable_set('mailsystem_theme', $form_state['values']['mailsystem']['mailsystem_theme']); + // Rebuild the theme registry to make changes needed by theme rendering. + drupal_theme_rebuild(); + unset($form_state['values']['mailsystem']['mailsystem_theme']); + + $default_id = mailsystem_default_id(); + $mail_system = array( + $default_id => ( + empty($form_state['values'][$default_id]) + ? mailsystem_default_value() + : $form_state['values'][$default_id] + ) + ); + foreach (element_children($form_state['values']['mailsystem']) as $module) { + $class = $form_state['values']['mailsystem'][$module]; + if (!empty($class) && $class != $default_id) { + $mail_system[$module] = $class; + } + } + unset($form_state['values']['mailsystem']); + if ($form_state['values']['class']['format'] === mailsystem_default_id()) { + unset($form_state['values']['class']['format']); + } + if ($form_state['values']['class']['mail'] === mailsystem_default_id()) { + unset($form_state['values']['class']['mail']); + } + if ($form_state['values']['class']) { + $new_class = mailsystem_create_class($form_state['values']['class']); + } + else { + $new_class = $mail_system[mailsystem_default_id()]; + } + unset($form_state['values']['class']); + if ($id = $form_state['values']['identifier']['module']) { + if (!empty($form_state['values']['identifier']['key'])) { + $id .= '_' . $form_state['values']['identifier']['key']; + } + $mail_system[$id] = $new_class; + } + unset($form_state['values']['identifier']); + variable_set('mail_system', $mail_system); + drupal_set_message(t('The configuration options have been saved.')); +} diff --git a/sites/all/modules/mailsystem/mailsystem.info b/sites/all/modules/mailsystem/mailsystem.info new file mode 100644 index 0000000..7d4956f --- /dev/null +++ b/sites/all/modules/mailsystem/mailsystem.info @@ -0,0 +1,14 @@ +package = Mail +name = Mail System +description = Provides a user interface for per-module and site-wide mail_system selection. +php = 5.0 +core = 7.x +configure = admin/config/system/mailsystem +dependencies[] = filter + +; Information added by drupal.org packaging script on 2012-04-10 +version = "7.x-2.34" +core = "7.x" +project = "mailsystem" +datestamp = "1334082653" + diff --git a/sites/all/modules/mailsystem/mailsystem.module b/sites/all/modules/mailsystem/mailsystem.module new file mode 100644 index 0000000..e414280 --- /dev/null +++ b/sites/all/modules/mailsystem/mailsystem.module @@ -0,0 +1,353 @@ + array( + 'title' => t('Administer Mail System'), + 'description' => t( + 'Select the default, per-module, and per-mailing @interface to use for formatting and sending email messages.', + array( + '!interface' => url('http://api.drupal.org/api/drupal/includes--mail.inc/interface/MailSystemInterface/7'), + '@interface' => 'MailSystemInterface', + ) + ), + ), + ); +} + +/** + * Implements hook_menu(). + */ +function mailsystem_menu() { + $items['admin/config/system/mailsystem'] = array( + 'title' => 'Mail System', + 'description' => 'Configure per-module Mail System settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('mailsystem_admin_settings'), + 'access arguments' => array('administer mailsystem'), + 'file' => 'mailsystem.admin.inc', + ); + return $items; +} + +/** + * Returns the id for the default mail_system setting. + */ +function mailsystem_default_id() { + // @todo: Is there a way to get this from core? + return 'default-system'; +} + +/** + * Returns the value for the default mail_system setting. + */ +function mailsystem_default_value() { + // @todo: Is there a way to get this from core? + return 'DefaultMailSystem'; +} + +/** + * Returns the default settings for the mail_system variable. + */ +function mailsystem_defaults() { + return array(mailsystem_default_id() => mailsystem_default_value()); +} + +/** + * Returns the current mail_system settings. + * + * @return The contents of the mail_system variable merged with its defaults. + */ +function mailsystem_get() { + return array_merge( + mailsystem_defaults(), + variable_get('mail_system', mailsystem_defaults()) + ); +} + +/** + * 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 = ' $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 %class written to %file.', + array('%class' => $class_name, '%file' => $class_file) + ) + ); + } + return $class_name; +} + +/** + * Helps other modules safely set their own key within mail_system. This + * function should be called from hook_enable() implementations. + * + * @param $setting An associative array ($id => $value) where: + * - $id is the machine-readable module name optionally followed by '_' + * and a key. + * - $value is one of + * - (string) The name of a class that implements MailSystemInterface. + * - (array) 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. + * + * @see drupal_mail(), mailsystem_default_methods() + */ +function mailsystem_set(array $setting) { + $mail_system = mailsystem_get(); + foreach ($setting as $key => $class) { + if (is_array($class)) { + unset($setting[$key]); + if ($new_class = mailsystem_create_class($class)) { + $setting[$key] = $new_class; + } + } + } + variable_set('mail_system', array_merge(mailsystem_get(), $setting)); +} + +/** + * Helps other modules safely remove their settings from mail_system. This + * function should be called from the other module's hook_disable() function. + * + * @param $setting An associative array ($module => $classname) describing + * a module and associated MailSystemInterface class that are being disabled. + * - $module is the machine-readable module name. + * - $classname is a class that implements MailSystemInterface. + * + * If $classname is empty, only the $module entry is removed. + * + * @param $class + * The name of the class to be removed, if any. + */ +function mailsystem_clear(array $setting) { + variable_set( + 'mail_system', + array_merge( + mailsystem_defaults(), + array_diff_key(array_diff(mailsystem_get(), $setting), $setting) + ) + ); +} + +/** + * Returns a list of classes which implement MailSystemInterface. + */ +function &mailsystem_get_classes() { + $mailsystem_classes = &drupal_static(__FUNCTION__); + if (!isset($mailsystem_classes)) { + $mailsystem_classes = array(); + // @todo Is there a better way to find all mail-related classes? + $declared_classes = get_declared_classes(); + $all_classes = array_combine( + $declared_classes, + array_fill(0, count($declared_classes), 0) + ); + $mail_classes = db_select('registry', 'registry') + ->distinct() + ->fields('registry', array('name', 'filename')) + ->where("type=:type AND ( filename like :filename OR name like :name )", + // Making the HUGE assumption that all classes which implement + // MailSystemInterface have filenames containing '.mail.' or + // classnames ending in 'MailSystem'. + array( + ':type' => 'class', + ':name' => '%MailSystem', + ':filename' => '%.mail.%', + ) + ) + ->execute() + ->fetchAllKeyed(); + foreach ($mail_classes as $classname => $classfile) { + if ( file_exists($classfile) + && drupal_autoload_class($classname) + ) { + $all_classes[$classname] = 1; + } + } + foreach ($all_classes as $classname => $autoload) { + if ( ($autoload || preg_match('/MailSystem/', $classname)) + && ($object = new $classname) + && ($object instanceof MailSystemInterface) + ) { + $mailsystem_classes[$classname] = $classname; + } + elseif ($autoload) { + // Clear classes that are no longer available. + db_delete('registry') + ->condition('name', $classname) + ->execute(); + } + } + foreach (array_unique(mailsystem_get()) as $classname) { + if (class_exists($classname)) { + $mailsystem_classes[$classname] = $classname; + } + else { + mailsystem_clear(array(mailsystem_default_id() => $classname)); + } + } + ksort($mailsystem_classes); + } + return $mailsystem_classes; +} + +/** +* Implements hook_theme_registry_alter(). +*/ +function mailsystem_theme_registry_alter(&$theme_registry) { + module_load_include('inc', 'mailsystem', 'mailsystem.theme'); + return mailsystem_theme_theme_registry_alter($theme_registry); +} + +/** +* Retrieves the key of the theme used to render the emails. +* +* @todo Add some kind of hook to let other modules alter this behavior. +*/ +function mailsystem_get_mail_theme() { + global $theme_key; + $theme = variable_get('mailsystem_theme', 'current'); + switch ($theme) { + case 'default': + $theme = variable_get('theme_default', NULL); + break; + case 'current': + $theme = $theme_key; + break; + case 'domain': + // Fetch the theme for the current domain. + if (module_exists('domain_theme')) { + // Assign the selected theme, based on the active domain. + global $_domain; + $domain_theme = domain_theme_lookup($_domain['domain_id']); + // The above returns -1 on failure. + $theme = ($domain_theme != -1) ? $domain_theme['theme'] : $theme_key; + } + break; + } + return $theme; +} diff --git a/sites/all/modules/mailsystem/mailsystem.theme.inc b/sites/all/modules/mailsystem/mailsystem.theme.inc new file mode 100644 index 0000000..8a22b91 --- /dev/null +++ b/sites/all/modules/mailsystem/mailsystem.theme.inc @@ -0,0 +1,82 @@ +base_themes)) { + foreach (array_keys($theme->base_themes) as $base) { + $base_theme[$base] = clone $themes[$base]; + } + } + if (isset($theme->base_theme) && !isset($base_theme[$theme->base_theme])) { + $base_theme[$theme->base_theme] = clone $themes[$theme->base_theme]; + } + if (isset($theme->engine)) { + $theme_engine = $theme->engine; + } + + // Include template files to let _theme_load_registry add preprocess + // functions. + include_once(drupal_get_path('theme', $theme->name) . '/template.php'); + foreach ($base_theme as $base) { + include_once(drupal_get_path('theme', $base->name) . '/template.php'); + } + + // Get the theme_registry cache. + $cache = _theme_load_registry($theme, $base_theme, $theme_engine); + + // Change the registry for hooks with a 'mail theme' element. + foreach ($theme_registry as $name => $hook) { + if (!empty($hook['mail theme'])) { + if (isset($cache[$name])) { + $cache[$name]['includes'][] = drupal_get_path('theme', $theme->name) . '/template.php'; + foreach ($base_theme as $base) { + $cache[$name]['includes'][] = drupal_get_path('theme', $base->name) . '/template.php'; + } + // Change the current registry for the new record. + $theme_registry[$name] = $cache[$name]; + } + + // Look for template suggestions. + foreach ($cache as $cache_name => $cache_hook) { + if (strpos($cache_name, $name . '__') !== FALSE) { + $cache_hook['includes'][] = drupal_get_path('theme', $theme->name) . '/template.php'; + foreach ($base_theme as $base) { + $cache_hook['includes'][] = drupal_get_path('theme', $base->name) . '/template.php'; + } + // Change the current registry for the new record. + $theme_registry[$cache_name] = $cache_hook; + } + } + } + } + } + } + } + $recursion_prevention = FALSE; +} diff --git a/sites/all/modules/mimemail/CHANGELOG.txt b/sites/all/modules/mimemail/CHANGELOG.txt new file mode 100644 index 0000000..a7e42b9 --- /dev/null +++ b/sites/all/modules/mimemail/CHANGELOG.txt @@ -0,0 +1,232 @@ +Mime Mail 7.x-1.x, xxxx-xx-xx +----------------------- + +Mime Mail 7.x-1.0, 2017-05-14 +----------------------- +- #2743229 by Bird-Kid, AdamPS: CSS doesn't get attached in PHP7 +- #2858390 by Munavijayalakshmi, Jigar.addweb: Fix coding standard issues +- #2594743 by skipyT: Theme function called for plain messages also +- #2374673 by smokris: Prevent processing of already embedded images +- #1532352 by sgabe, Matt B: Add permission to view user specific settings +- #2783815 by argiepiano: Add 'Reply-to' field to "Send HTML mail to all users of a role" action +- #2796993 by Cauliflower: Return send status in Rules actions +- #2796965 by Cauliflower: Allow NULL values in Rules actions +- #1568680 by jamsilver: Use $message for themeing +- #2721799 by igorik, sgabe: Minor typo +- #2146513 by torotil, Anybody: Scan theme for other *css* file types +- #2678818 by hoebekewim, das-peter: Mime Mail Compress has a deprecated constructor +- #2553815 by nitrocad, anthonys: Imported font generates empty attachment +- #2562181 by rrfegade: Remove unused varibles +- #2562177 by rrfegade: Spelling errors + +Mime Mail 7.x-1.0-beta4, 2015-08-02 +----------------------- +- #2413495 by sgabe, siggi_kid: Public images not embedded when file default scheme is private +- #2312747 by Lukas von Blarer: Remove 'itok' token from image URL +- #2366659 by david_garcia: Attachments created by content generate warnings +- #2404719 by alexp999: Missing space in RFC headers breaks DKIM +- #1908318 by jvieille, zionduc, bisonbleu | anrkaid: Sender is double encoded. +- #2359771 by PascalAnimateur: Support for OG membership tokens. +- #2218037 by sgabe | pinkonomy: Fixed integrity constraint violation. +- #2219609 by mitrpaka, sgabe: Convert float properties just for images. +- #2057703 by New Zeal, sgabe: Warning: is_file() expects a valid path. +- #2202127 by k.skarlatos: Added List-Unsubscribe header field for bulk mails. +- #2297135 by tobiasb: Language prefix with absolute-path reference. +- #2297091 by cameron1729: TLDs in Return-Path trimmed to 4 characters. +- #2237109 by Dane Powell: Indicate text format for body in Rules. + +Mime Mail 7.x-1.0-beta3, 2014-03-05 +----------------------- +- Public files test incorrect if similar path is not below root. + +Mime Mail 7.x-1.0-beta2, 2014-02-26 +----------------------- +- Stronger authentication for incoming messages. +- #2031143 by sgabe, das-peter: Support @import by using drupal_load_stylesheet(). +- #2087913 by sgabe | alancsilver: Allow spaces in attachment filenames. +- #1852694 by clemens.tolboom | Pixelstyle: Convert float to align for images. +- #2145659 by sgabe, fatherguddha | raincloud: Images with 'itok' token are not embedded. +- #2119613 by sgabe, david_garcia_garcia: Further improve boundary collision avoidance. +- #2185909 by cyrus_bt5, ekidman: Extra space in long header fields. +- #2152705 by gargsuchi: Images with 'itok' token not showing up. +- #2129149 by pokap | satvision83: Undefined offset in mimemail_headers. + +Mime Mail 7.x-1.0-beta1, 2013-10-07 +----------------------- +- #1702868 by sgabe, kid_icarus: Remove tokens if no replacement value can be generated. +- #1719570 by sgabe, oadaeh | greggles: Fix for SA-CONTRIB-2012-124. +- #2020875 by das-peter, Propaganistas: Provide option to set language in Rules actions. +- #2045699 by sgabe | Punk_UnDeaD: Boundaries are not unique on Windows. +- #1798324 by sgabe, kienan | ShaneOnABike: Return-Path is incorrectly using sender name. +- #1790098 by sgabe | edb, Shellingfox: Custom 'from' address comes out as 'Array'. +- #1979776 by sgabe | cswolf: Hash mark link gets replaced with site URL. +- #1963412 by sgabe | djg_tram: Content-Disposition should default to 'inline'. +- #1469828 by sgabe | shadowhitman: Allow to use simple address format only for recipient. +- #1947018 by sgabe | bendev: Allow sending plain text messages with Rules. +- #962226 by sgabe | rchuber: Allow custom mailkeys for system and Rules action messages. +- #1873348 by sgabe | tutumlum: Cannot use tokens in subject and body of HTML email action. +- #1439918 by sgabe | Lukas von Blarer: 'Link images only' is not working if the file exists as-is. +- #1538004 by sgabe | djg_tram: Change template naming logic to use module as well. +- #1719256 by lirantal, sgabe: Handle different files with the same file name. +- #1922530 by berliner: Callto links in mail body wrongly replaced. +- #1911558 by Simon Georges, JulienD: Remove useless files[] directive from .info files. +- #1877928 by sgabe | parasite: Replacing underscore in key is not needed. +- #1898140 by MiroslavBanov: Engine variable set to NULL on settings page. +- #1780412 by sgabe, kid_icarus: Option to exclude blocked users from a role. +- #1814922 by marcusx: Rule sanitizes the $body if populated by a parameter. +- #1813348 by sgabe | jdhildeb: Sendmail invoked with empty Return-Path. +- #1469022 by sgabe, das-peter | MI: Add 'Reply-to' field to Rules and system actions. +- #1773698 by jherencia: Alternatives for mimemail-message.tpl.php do not work. +- #1585546 by kotnik, bojanz: Rules actions must be in root module directory. + +Mime Mail 7.x-1.0-alpha2, 2012-08-22 +----------------------- +- #1722188 by sgabe | christian death: Split has been deprecated. +- #1643750 by sgabe | MRV: Remove class attributes from the compressed message. +- #321026 by sgabe, LUTi | attiks: HTML messages are broken by line breaks. +- #1605230 by sgabe | mrbubbs: Extra space in subject caused by wordwrap. +- #1662682 by sgabe, itamar: Value may be left unset in requirements check. +- #1504782 by rjkuyvenhoven: Update support for Fusion based themes. +- #1597896 by sgabe | joewickert: Plus symbol encoded as space in URLs. +- #1515660 by sgabe | philsward: Missing upgrade path for Rules actions. +- #81707 by sgabe | FredCK, Peters196: Auto-detect appropriate line endings. +- #1475664 by sgabe | pumpkinkid: Getting 'Array to string conversion' error. +- #1301876 by sean_fremouw: Regex in mimemail_headers() strips allowed characters. +- #1432502 by El Bandito: Quotations are not needed to specify an attachment. +- #1349728 by jherencia: Possibility to configure the theme that will render the email. +- #1391680 by marcdecaluwe: Headers not correctly set. +- #1283620 by Cyberwolf: Expose email settings user field to field API. +- #1372660 by eueco: Set the proper line ending when calling chunk_split(). +- #1388786 by tostinni: mimemail_html_body() fails to retrieve file's URI. + +Mime Mail 7.x-1.0-alpha1, 2011-12-18 +----------------------- +- #1372088 by marcus.n3rd.26: Use uri to load mail.css when sending mail. +- #1305824 by sgabe: Leave MIME type and use only path to specify an attachment. +- #1370422 by awagner: Missing delimiter in file_scan_directory(). +- #1275734 by gnindl: Scan recursively for mail.css. +- #1304332 by sgabe: Token replacement and PHP evaluation in Rules action messages. +- #1305830 by sgabe | ibes: Set default filename and mimetype to enforce auto detection. +- #1289584 by sgabe | oguerreiro: Check if 'styles' is set. +- #1288546 by sgabe | carn1x: Unknown Rules actions. +- #1066438 by quicksketch, sgabe, guillaumev, oadaeh: Initial support of attachments. +- #1258302 by ralf.strobel: Replace 'arguments' with 'variables' in hook_theme(). +- #1190144 by Cyberwolf, sgabe: Trim less-than and grater-than chars from Return-Path. +- #1140538 by sgabe: Site style sheet isn't included. +- #1232266 by InternetDevels.Com: Engine select form element has wrong array key. + +Mime Mail 6.x-1.0, 2011-11-19 +----------------------- +- #1232264 by InternetDevels.Com: Check for not just NULL but empty From address. +- #1201154 by guillaumev: Check if attachments is an array and isn't empty. +- #1203234 by sgabe | Offlein: Store input format setting for Rules and actions. +- #1227242 by sgabe: Remove unnecessary reference signs. +- #1076520 by joelstein: Absolute site stylesheets not included. +- #1258062 by oadaeh: Don't allow an empty e-mail address with the default engine. +- #1270686 by gmania: Don't add Content-Disposition to multipart/alternative parts. +- #1260302 by sgabe | prokop1000: Replace encoding detection with UTF-8. +- #1270656 by sgabe: From header can be an array which causes errors. +- #1301868 by sean_fremouw: Headers overwritten. +- #1308628 by sgabe, chriscohen: List function throws notice. +- #1301924 by sgabe, ibes: Use array for body in Rules and system actions. +- #417462 by plach, Lukas von Blarer, sgabe: Language prefix is not taken into account. +- #1181170 by sgabe, Cyberwolf, ibes | windm: Add permission to set user specific settings. +- #1309248 by sgabe, gmania: Generate not existing ImageCache images before embedding. +- #1304134 by sdague: Add preference to link images. +- #1275080 by gmania: Remove the depricated Errors-To header. + +Mime Mail 6.x-1.0-beta2, 2011-06-22 +---------------------- +- #1181486 by sgabe: HTML Message not saving in Rules Action form. +- #1164870 by itserich: Recipient is not an array anymore. +- #1186690 by samhassell: Can't send multiple attachments. + +Mime Mail 6.x-1.0-beta1, 2011-06-04 +---------------------- +- #911612 by geneticdrift: Hidden attachments in some email clients. +- #1090286 by sgabe: Prepare action messages with drupal_mail() to allow alteration. +- #1137358 by sgabe: Tokens don't work in the body of Rules action messages +- #1150224 by sgabe: Run filters on the message body of Rules and system actions. +- #1090286 by sgabe: Remove process function, fix sending message to a role action. +- #1116930 by Pol, sgabe: No text alternative if the CSS is too large. +- #808518 by sgabe: Return only the result from drupal_mail_wrapper(). +- #808518 by claudiu.cristea, sgabe: Split mail preparation from sending. +- #1108324 by sgabe: Add input filter to HTML message for system and Rules actions. +- #1114536 by rjbrown99: Pass recipient to the template. +- #971272 by sgabe: Allow to specify sender's name for Rules action messages. +- #1167576 by Pol: Accept plaintext and text parameters for system messages. +- #338460 by hopla: Doens't look for mail.css in Zen sub-themes. +- #261028 by sgabe, gnosis, mfb, mrfelton, LUTi: SMTP Return-Path Setting. +- #1175378 by sgabe, samalone: Include module CSS files in email. + +Mime Mail 6.x-1.0-alpha8, 2011-03-24 +---------------------- +- #374615 by joelstein: Set starter default value for plain text user reference. +- #1076222 by papasse, Aron Novak: Check the module path on settings submission. +- #920904 by fmjrey: Fusion local.css not taken into account. +- #443964 by sgabe, pillarsdotnet: Skip style sheets with print media. +- #932962 by clydefrog, arvana, sgabe: Allow attachments to be added by contents. +- #907716 by isaac.niebeling: Allow non-web-accessible files as attachments. +- #758922 by eft, sgabe: Use simple address format for PHP mail() on Windows. + +Mime Mail 6.x-1.0-alpha7, 2011-01-31 +---------------------- +- #950456 by stella, sgabe: Check if body encoding can be, and is it detected +- #364198 by mfb, sgabe | HS: CSS code in email +- #835734 by sgabe | sylvaticus: In some cases CSS optimization causes WSOD +- #438058 by AlexisWilke, DanChadwick: Remove line feeds in subject +- #979748 by Romka: Missing include in mimemail_mailengine() +- #700996 by smk-ka: Custom inline style properties overwritten +- #960374 by kim-day: Don't set BCC and CC headers if they are empty +- #961536 by clydefrog: Check if sender is empty, not just null +- #852698 by sgabe | interestingaftermath: Specify sender's name +- #685574 by sgabe, Wim Leers | Michelle: Optional site's css embedding +- #758754 by sgabe | mennonot: Add 'Send HTML e-mail' action +- #501722 by jpetso, fago, criz, sgabe, aantonop: HTML mail actions for Rules +- #729658 by sgabe, Agileware: Allow better integration with Domain Access module +- #960726 by sgabe, clydefrog: Send plaintext message if the HTML body is empty + +Mime Mail 6.x-1.0-alpha6, 2010-09-13 +---------------------- +- #629038 by Robbert: Attachments dont respect ‘list’ setting +- #882960 by sgabe, glynster: CSS Mail Style Sheet Overrides +- #319229 by javierreartes, tobiasb, sgabe, crifi: Set $attachments in drupal_wrap_mail() +- #903536 by sgabe: Use variable_del() to remove smtp_library() +- #456242 by sgabe, kenorb: Use proper operators in if statements with strpos() +- #882528 by sgabe | Carsten: Template suggestions based on mailkey +- #752838 by sgabe | dsms: Pass $subject to the template +- #319384 by sgabe | mariuss: Add $mailkey to body tag as CSS class +- #796510 by sgabe | smk-ka: Update CSS Compressor +- #614782 by sgabe, Sutharsan: Update README.txt + +Mime Mail 6.x-1.0-alpha5, 2010-08-12 +---------------------- +- #850674 by sgabe, AlexisWilke: Prepare function name testing '_prepare'... +- #448996 by mfb, hanoii, Sylvain Lecoy: Wrong implementation of hook_mail_alter() +- #319229 by sgabe, jm.federico, joostvdl, donquixote, fehin, sunfire-design, mariuss: src='Array' if path to image is broken +- #517306 by sgabe, rdosser: Mime Mail Compress mangles absolute URLs in CSS properties +- #597448 by sgabe, rmjiv: Unsafe regex pattern in mimemail_extract_files() +- #535466 by andreiashu, sgabe: WSOD when using Mime Mail Compress without DOM extension +- #513138 by sgabe, peterx: Undefined variables in mimemail.inc +- #304476 by sgabe, Thomas_Zahreddin, aaron: PHP Error when Stylesheets don't exist +- #710116 by sgabe, neoglez: Wrong implementation/namespace conflict of mimemail_prepare() + +Mime Mail 6.x-1.0-alpha4, 2010-07-10 +---------------------- +- #642800 by scronide: Enforce requirement of PHP 5.x for Mime Mail Compress +- #740856 by sgabe, Vicbus: Check if the file part is set in the body +- #567594 by hanoii: $mailkey is not properly set in drupal_mail_wrapper() +- #768794 by sgabe, danyg: Check if the name is empty when the address is an object +- #700996 by sgabe, -Mania-: Custom inline style properties overwritten when using CSS Compressor +- #729334 by plach: Flawed CSS to XPath conversion for class selectors in Mime Mail CSS Compressor +- #456260 by sgabe, kenorb, kscheirer, mitchmac: WSOD: smtp_library variable is not removed when Mime Mail has been disabled +- #698794 by sgabe, mobilis: Attachment Content-Type-fix +- #629038 by jackinloadup, sgabe: Attachments don't respect list setting + +Mime Mail 6.x-1.0-alpha3, 2010-06-16 +---------------------- +- #358439 by folkertdv: Images are only in the first message +- #448670 by sgabe, gregarios, moritzz: Spaces and Line Breaks are removed from CSS definitions +- #372710 by LUTi, sgabe, perarnet: HTML emails are text-only in Hotmail +- #583920 by Sutharsan, sgabe: Can't override mimemail.tpl.php +- #127876 by sgabe, Sutharsan, jerdavis: Plain text with/without attachment diff --git a/sites/all/modules/mimemail/LICENSE.txt b/sites/all/modules/mimemail/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/mimemail/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/mimemail/README.txt b/sites/all/modules/mimemail/README.txt new file mode 100644 index 0000000..804e73d --- /dev/null +++ b/sites/all/modules/mimemail/README.txt @@ -0,0 +1,136 @@ + +-- SUMMARY -- + + This is a Mime Mail component module (for use by other modules). + * It permits users to receieve HTML email and can be used by other modules. The mail + functionality accepts an HTML message body, mime-endcodes it and sends it. + * If the HTML has embedded graphics, these graphics are MIME-encoded and included + as a message attachment. + * Adopts your site's style by automatically including your theme's stylesheet files in a + themeable HTML message format + * If the recipient's preference is available and they prefer plaintext, the HTML will be + converted to plain text and sent as-is. Otherwise, the email will be sent in themeable + HTML with a plaintext alternative. + + For a full description of the module, visit the project page: + http://drupal.org/project/mimemail + + To submit bug reports and feature suggestions, or to track changes: + http://drupal.org/project/issues/mimemail + + +-- REQUIREMENTS -- + + Mail System module - http://drupal.org/project/mailsystem + + +-- INSTALLATION -- + + Hopefully, you know the drill by now :) + 1. Download the module and extract the files. + 2. Upload the entire mimemail folder into your Drupal sites/all/modules/ + or sites/my.site.folder/modules/ directory if you are running a multi-site + installation of Drupal and you want this module to be specific to a + particular site in your installation. + 3. Enable the Mime Mail module by navigating to: + Administration > Modules + 4. Adjust settings by navigating to: + Administration > Configuration > Mime Mail + + +-- USAGE -- + + This module may be required by other modules, but in favor of the recently + added system actions and Rules integration, it can be useful by itself too. + + Once installed, any module can send MIME-encoded messages by specifing + MimeMailSystem as the responsible mail system for a particular message + or all mail sent by one module. + + This can be done through the web by visiting admin/config/system/mailsystem + or in a program as follows: + + mailsystem_set(array( + '{$module}_{$key}' => 'MimeMailSystem', // Just messages with $key sent by $module. + '{$module}' => 'MimeMailSystem', // All messages sent by $module. + )); + + You can use the following optional parameters to build the e-mail: + 'plain': + Boolean, whether to send messages in plaintext-only (optional, default is FALSE). + 'plaintext': + Plaintext portion of a multipart e-mail (optional). + 'attachments': + Array of arrays with the path or content, name and MIME type of the file (optional). + 'headers': + A keyed array with headers (optional). + + You can set these in $params either before calling drupal_mail() or in hook_mail() + and of course hook_mail_alter(). + + Normally, Mime Mail uses email addresses in the form of "name" , + but PHP running on Windows servers requires extra SMTP handling to use this format. + If you are running your site on a Windows server and don't have an SMTP solution such + as the SMTP module installed, you may need to set the 'Use the simple format of + user@example.com for all email addresses' option on the configuration settings page. + + This module creates a user preference for receiving plaintext-only messages. + This preference will be honored by all messages if the format is not explicitly set + and the user has access to edit this preference (allowed by default). + + Email messages are formatted using the mimemail-message.tpl.php template. + This includes a CSS style sheet and uses an HTML version of the text. + The included CSS is either: + the mail.css file found anywhere in your theme folder or + the combined CSS style sheets of your theme if enabled. + + Since some email clients (namely Outlook 2007 and GMail) is tend to only regard + inline CSS, you can use the Compressor to convert CSS styles into inline style + attributes. It transmogrifies the HTML source by parsing the CSS and inserting the + CSS definitions into tags within the HTML based on the CSS selectors. To use the + Compressor, just enable it. + + To create a custom mail template copy the mimemail-message.tpl.php file from + the mimemail/theme directory into your default theme's folder. Both general and + by-mailkey theming can be performed: + mimemail-message--[module]--[key].tpl.php (for messages with a specific module and key) + mimemail-message--[module].tpl.php (for messages with a specific module) + mimemail-message--[key].tpl.php (for messages with a specific key) + mimemail-message.tpl.php (for all messages) + + Messages can be rendered using different themes. You can choose the following + settings to render the e-mail: + 'current': Theme currently used by the user who runs drupal_mail(). + 'default': Default theme, obtained via variable theme_default. + 'domain': Theme obtained via Domain Theme module. + or any other active theme. + + Images with absolute URL will be available as remote content. To embed images + into emails you have to use a relative URL or an internal path. Due to security + concerns, only files residing in the public file system (e.g sites/default/files) + can be used by default. + + For example: + instead of http://www.mysite.com/sites/default/files/mypicture.jpg + use /home/www/public_html/drupal/sites/default/files/mypicture.jpg + or /sites/default/files/mypicture.jpg + or public://mypicture.jpg + + The 'send arbitrary files' permission allows you to attach or embed files located + outside Drupal's public files directory. Note that this has security implications: + arbitrary means even your settings.php! Give to trusted roles only! + + +-- CREDITS -- + + MAINTAINER: Allie Micka < allie at pajunas dot com > + + * Allie Micka + Mime enhancements and HTML mail code + + * Gerhard Killesreiter + Original mail and mime code + + * Robert Castelo + HTML to Text and other functionality + diff --git a/sites/all/modules/mimemail/includes/mimemail.admin.inc b/sites/all/modules/mimemail/includes/mimemail.admin.inc new file mode 100644 index 0000000..7155e79 --- /dev/null +++ b/sites/all/modules/mimemail/includes/mimemail.admin.inc @@ -0,0 +1,154 @@ + 'textfield', + '#title' => t('Sender name'), + '#default_value' => variable_get('mimemail_name', variable_get('site_name', 'Drupal')), + '#size' => 60, + '#maxlength' => 128, + '#description' => t('The name that all site emails will be from when using default engine.'), + ); + $form['mimemail']['mimemail_mail'] = array( + '#type' => 'textfield', + '#title' => t('Sender e-mail address'), + '#default_value' => variable_get('mimemail_mail', variable_get('site_mail', ini_get('sendmail_from'))), + '#size' => 60, + '#maxlength' => 128, + '#description' => t('The email address that all site e-mails will be from when using default engine.'), + ); + $form['mimemail']['mimemail_simple_address'] = array( + '#type' => 'checkbox', + '#title' => t('Use simple address format'), + '#default_value' => variable_get('mimemail_simple_address', FALSE), + '#description' => t('Use the simple format of user@example.com for all recipient email addresses.'), + ); + $form['mimemail']['mimemail_sitestyle'] = array( + '#type' => 'checkbox', + '#title' => t('Include site style sheets'), + '#default_value' => variable_get('mimemail_sitestyle', TRUE), + '#description' => t('Gather all style sheets when no mail.css found in the default theme directory.'), + '#disabled' => $disable_sitestyle, + ); + $form['mimemail']['mimemail_textonly'] = array( + '#type' => 'checkbox', + '#title' => t('Send plain text email only'), + '#default_value' => variable_get('mimemail_textonly', FALSE), + '#description' => t('This option disables the use of email messages with graphics and styles. All messages will be converted to plain text.'), + ); + $form['mimemail']['mimemail_linkonly'] = array( + '#type' => 'checkbox', + '#title' => t('Link images only'), + '#default_value' => variable_get('mimemail_linkonly', 0), + '#description' => t('This option disables the embedding of images. All image will be available as external content. This can make email messages much smaller.'), + ); + if (module_exists('mimemail_compress')) { + $form['mimemail']['mimemail_preserve_class'] = array( + '#type' => 'checkbox', + '#title' => t('Preserve class attributes'), + '#default_value' => variable_get('mimemail_preserve_class', 0), + '#description' => t('This option disables the removing of class attributes from the message source. Useful for debugging the style of the message.'), + ); + } + + // Get a list of all formats. + $formats = filter_formats(); + foreach ($formats as $format) { + $format_options[$format->format] = $format->name; + } + $form['mimemail']['mimemail_format'] = array( + '#type' => 'select', + '#title' => t('E-mail format'), + '#options' => $format_options, + '#default_value' => variable_get('mimemail_format', filter_fallback_format()), + '#access' => count($formats) > 1, + '#attributes' => array('class' => array('filter-list')), + '#description' => t('The filter set that will be applied to the message body. + If you are using Mime Mail as default mail system, make sure to enable + "Convert line breaks into HTML" and "Convert URLs into links" with a long + enough maximum length for e.g. password reset URLs!'), + ); + + $form['mimemail']['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['mimemail']['advanced']['mimemail_incoming'] = array( + '#type' => 'checkbox', + '#title' => t('Process incoming messages posted to this site'), + '#default_value' => variable_get('mimemail_incoming', FALSE), + '#description' => t('This is an advanced setting that should not be enabled unless you know what you are doing.'), + ); + $form['mimemail']['advanced']['mimemail_key'] = array( + '#type' => 'textfield', + '#title' => t('Message validation string'), + '#default_value' => variable_get('mimemail_key', drupal_random_key()), + '#required' => TRUE, + '#description' => t('This string will be used to validate incoming messages. It can be anything, but must be used on both sides of the transfer.'), + ); + + // Get the available mail engines. + $engines = mimemail_get_engines(); + foreach ($engines as $module => $engine) { + $engine_options[$module] = $engine['name'] . ': ' . $engine['description']; + } + // Hide the settings if only 1 engine is available. + if (count($engines) == 1) { + reset($engines); + variable_set('mimemail_engine', key($engines)); + $form['mimemail']['mimemail_engine'] = array( + '#type' => 'hidden', + '#value' => variable_get('mimemail_engine', 'mimemail'), + ); + } + else { + $form['mimemail']['mimemail_engine'] = array( + '#type' => 'select', + '#title' => t('E-mail engine'), + '#default_value' => variable_get('mimemail_engine', 'mimemail'), + '#options' => $engine_options, + '#description' => t('Choose an engine for sending mails from your site.'), + ); + } + + if (variable_get('mimemail_engine', 'mail')) { + $settings = module_invoke(variable_get('mimemail_engine', 'mimemail'), 'mailengine', 'settings'); + if ($settings) { + $form['mimemail']['engine_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Engine specific settings'), + ); + foreach ($settings as $name => $value) { + $form['mimemail']['engine_settings'][$name] = $value; + } + } + } + else { + drupal_set_message(t('Please choose a mail engine.'), 'error'); + } + + return system_settings_form($form); +} diff --git a/sites/all/modules/mimemail/includes/mimemail.incoming.inc b/sites/all/modules/mimemail/includes/mimemail.incoming.inc new file mode 100644 index 0000000..43f98eb --- /dev/null +++ b/sites/all/modules/mimemail/includes/mimemail.incoming.inc @@ -0,0 +1,211 @@ + 1) { + foreach ($sub_parts as $sub_part_body) { + $sub_part = mimemail_parse_headers($sub_part_body); + if ($sub_part['content-type'] == 'text/plain') { + $mail['text'] = mimemail_parse_content($sub_part); + } + if ($sub_part['content-type'] == 'text/html') { + $mail['html'] = mimemail_parse_content($sub_part); + } + else { + $mail['attachments'][] = mimemail_parse_attachment($sub_part); + } + } + } + + if (($part['content-type'] == 'text/plain') && !isset($mail['text'])) { + $mail['text'] = mimemail_parse_content($part); + } + elseif (($part['content-type'] == 'text/html') && !isset($mail['html'])) { + $mail['html'] = mimemail_parse_content($part); + } + else { + $mail['attachments'][] = mimemail_parse_attachment($part); + } + } + + // Make sure our text and html parts are accounted for. + if (isset($mail['html']) && !isset($mail['text'])) { + $mail['text'] = preg_replace('||mis', '', $mail['html']); + $mail['text'] = drupal_html_to_text($mail['text']); + } + elseif (isset($mail['text']) && !isset($mail['html'])) { + $mail['html'] = check_markup($mail['text'], variable_get('mimemail_format', filter_fallback_format())); + } + + // Last ditch attempt - use the body as-is. + if (!isset($mail['text'])) { + $mail['text'] = mimemail_parse_content($mail); + $mail['html'] = check_markup($mail['text'], variable_get('mimemail_format', filter_fallback_format())); + } + + return $mail; +} + +/** + * Split a multi-part message using MIME boundaries. + */ +function mimemail_parse_boundary($part) { + $m = array(); + if (preg_match('/.*boundary="?([^";]+)"?.*/', $part['headers']['Content-Type'], $m)) { + $boundary = "\n--" . $m[1]; + $body = str_replace("$boundary--", '', $part['body']); + return array_slice(explode($boundary, $body), 1); + } + return array($part['body']); +} + +/** + * Split a message (or message part) into its headers and body section. + */ +function mimemail_parse_headers($message) { + // Split out body and headers. + if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $message, $match)) { + list($hdr, $body) = array($match[1], $match[2]); + } + + // Un-fold the headers. + $hdr = preg_replace(array("/\r/", "/\n(\t| )+/"), array('', ' '), $hdr); + + $headers = array(); + foreach (explode("\n", trim($hdr)) as $row) { + $split = strpos($row, ':'); + $name = trim(drupal_substr($row, 0, $split)); + $val = trim(drupal_substr($row, $split+1)); + $headers[$name] = $val; + } + + $type = (preg_replace('/\s*([^;]+).*/', '\1', $headers['Content-Type'])); + + return array('headers' => $headers, 'body' => $body, 'content-type' => $type); +} + +/** + * Return a decoded MIME part in UTF-8. + */ +function mimemail_parse_content($part) { + $content = $part['body']; + + // Decode this part. + if ($encoding = drupal_strtolower($part['headers']['Content-Transfer-Encoding'])) { + switch ($encoding) { + case 'base64': + $content = base64_decode($content); + break; + case 'quoted-printable': + $content = quoted_printable_decode($content); + break; + // 7bit is the RFC default. + case '7bit': + break; + } + } + + // Try to convert character set to UTF-8. + if (preg_match('/.*charset="?([^";]+)"?.*/', $part['headers']['Content-Type'], $m)) { + $content = drupal_convert_to_utf8($content, $m[1]); + } + + return $content; +} + +/** + * Convert a MIME part into a file array. + */ +function mimemail_parse_attachment($part) { + $m = array(); + if (preg_match('/.*filename="?([^";])"?.*/', $part['headers']['Content-Disposition'], $m)) { + $name = $m[1]; + } + elseif (preg_match('/.*name="?([^";])"?.*/', $part['headers']['Content-Type'], $m)) { + $name = $m[1]; + } + + return array( + 'filename' => $name, + 'filemime' => $part['content-type'], + 'content' => mimemail_parse_content($part), + ); +} diff --git a/sites/all/modules/mimemail/includes/mimemail.mail.inc b/sites/all/modules/mimemail/includes/mimemail.mail.inc new file mode 100644 index 0000000..28bebfc --- /dev/null +++ b/sites/all/modules/mimemail/includes/mimemail.mail.inc @@ -0,0 +1,63 @@ + + */ + +/** + * Attempts to RFC822-compliant headers for the mail message or its MIME parts. + * + * @todo Could use some enhancement and stress testing. + * + * @param array $headers + * An array of headers. + * + * @return string + * A string containing the headers. + */ +function mimemail_rfc_headers($headers) { + $header = ''; + $crlf = variable_get('mimemail_crlf', MAIL_LINE_ENDINGS); + foreach ($headers as $key => $value) { + $key = trim($key); + // Collapse spaces and get rid of newline characters. + $value = preg_replace('/(\s+|\n|\r|^\s|\s$)/', ' ', $value); + // Fold headers if they're too long. + // A CRLF may be inserted before any WSP. + // @see http://tools.ietf.org/html/rfc2822#section-2.2.3 + if (drupal_strlen($value) > 60) { + // If there's a semicolon, use that to separate. + if (count($array = preg_split('/;\s*/', $value)) > 1) { + $value = trim(join(";$crlf ", $array)); + } + else { + $value = wordwrap($value, 50, "$crlf ", FALSE); + } + } + $header .= $key . ": " . $value . $crlf; + } + return trim($header); +} + +/** + * Gives useful defaults for standard email headers. + * + * @param array $headers + * Message headers. + * @param string $from + * The address of the sender. + * + * @return array + * Overwrited headers. + */ +function mimemail_headers($headers, $from = NULL) { + $default_from = variable_get('site_mail', ini_get('sendmail_from')); + + // Overwrite standard headers. + if ($from) { + if (!isset($headers['From']) || $headers['From'] == $default_from) { + $headers['From'] = $from; + } + if (!isset($headers['Sender']) || $headers['Sender'] == $default_from) { + $headers['Sender'] = $from; + } + // This may not work. The MTA may rewrite the Return-Path. + if (!isset($headers['Return-Path']) || $headers['Return-Path'] == $default_from) { + // According to IANA the current longest TLD is 23 characters. + if (preg_match('/[a-z\d\-\.\+_]+@(?:[a-z\d\-]+\.)+[a-z\d]{2,23}/i', $from, $matches)) { + $headers['Return-Path'] = "<$matches[0]>"; + } + } + } + + // Convert From header if it is an array. + if (is_array($headers['From'])) { + $headers['From'] = mimemail_address($headers['From']); + } + + // Run all headers through mime_header_encode() to convert non-ascii + // characters to an rfc compliant string, similar to drupal_mail(). + foreach ($headers as $key => $value) { + // According to RFC 2047 addresses MUST NOT be encoded. + if ($key !== 'From' && $key !== 'Sender') { + $headers[$key] = mime_header_encode($value); + } + } + + return $headers; +} + +/** + * Extracts links to local images from HTML documents. + * + * @param string $html + * A string containing the HTML source of the message. + * + * @return array + * An array containing the document body and the extracted files like the following. + * array( + * array( + * 'name' => document name + * 'content' => html text, local image urls replaced by Content-IDs, + * 'Content-Type' => 'text/html; charset=utf-8') + * array( + * 'name' => file name, + * 'file' => reference to local file, + * 'Content-ID' => generated Content-ID, + * 'Content-Type' => derived using mime_content_type if available, educated guess otherwise + * ) + * ) + */ +function mimemail_extract_files($html) { + $pattern = '/(]+href=[\'"]?|]+codebase=[\'"]?|@import (?:url\()?[\'"]?|[\s]src=[\'"]?)([^\'>")]+)([\'"]?)/mis'; + $content = preg_replace_callback($pattern, '_mimemail_replace_files', $html); + + $encoding = '8Bit'; + $body = explode("\n", $content); + foreach ($body as $line) { + if (drupal_strlen($line) > 998) { + $encoding = 'base64'; + break; + } + } + if ($encoding == 'base64') { + $content = rtrim(chunk_split(base64_encode($content))); + } + + $document = array(array( + 'Content-Type' => "text/html; charset=utf-8", + 'Content-Transfer-Encoding' => $encoding, + 'content' => $content, + )); + + $files = _mimemail_file(); + + return array_merge($document, $files); +} + +/** + * Callback function for preg_replace_callback(). + */ +function _mimemail_replace_files($matches) { + return stripslashes($matches[1]) . _mimemail_file($matches[2]) . stripslashes($matches[3]); +} + +/** + * Helper function to extract local files. + * + * @param string $url + * (optional) The URI or the absolute URL to the file. + * @param string $content + * (optional) The actual file content. + * @param string $name + * (optional) The file name. + * @param string $type + * (optional) The file type. + * @param string $disposition + * (optional) The content disposition. Defaults to inline. + * + * @return + * The Content-ID and/or an array of the files on success or the URL on failure. + */ +function _mimemail_file($url = NULL, $content = NULL, $name = '', $type = '', $disposition = 'inline') { + static $files = array(); + static $ids = array(); + + if ($url) { + $image = preg_match('!\.(png|gif|jpg|jpeg)$!i', $url); + $linkonly = variable_get('mimemail_linkonly', 0); + // The file exists on the server as-is. Allows for non-web-accessible files. + if (@is_file($url) && $image && !$linkonly) { + $file = $url; + } + else { + $url = _mimemail_url($url, 'TRUE'); + // The $url is absolute, we're done here. + $scheme = file_uri_scheme($url); + if ($scheme == 'http' || $scheme == 'https' || preg_match('!mailto:!', $url) || preg_match('!^data:!', $url)) { + return $url; + } + // The $url is a non-local URI that needs to be converted to a URL. + else { + $file = (drupal_realpath($url)) ? drupal_realpath($url) : file_create_url($url); + } + } + } + // We have the actual content. + elseif ($content) { + $file = $content; + } + + if (isset($file)) { + $is_file = @is_file($file); + + if ($is_file) { + $access = user_access('send arbitrary files'); + $in_public_path = strpos(@drupal_realpath($file), drupal_realpath('public://')) === 0; + if (!$in_public_path && !$access) { + return $url; + } + } + + if (!$name) { + $name = $is_file ? basename($file) : 'attachment.dat'; + } + if (!$type) { + $type = $is_file ? file_get_mimetype($file) : file_get_mimetype($name); + } + + $id = md5($file) . '@' . $_SERVER['HTTP_HOST']; + + // Prevent duplicate items. + if (isset($ids[$id])) { + return 'cid:' . $ids[$id]; + } + + $new_file = array( + 'name' => $name, + 'file' => $file, + 'Content-ID' => $id, + 'Content-Disposition' => $disposition, + 'Content-Type' => $type, + ); + + $files[] = $new_file; + $ids[$id] = $id; + + return 'cid:' . $id; + } + // The $file does not exist and no $content, return the $url if possible. + elseif ($url) { + return $url; + } + + $ret = $files; + $files = array(); + $ids = array(); + + return $ret; +} + +/** + * Build a multipart body. + * + * @param array $parts + * An associative array containing the parts to be included: + * - name: A string containing the name of the attachment. + * - content: A string containing textual content. + * - file: A string containing file content. + * - Content-Type: A string containing the content type of either file or content. Mandatory + * for content, optional for file. If not present, it will be derived from file the file if + * mime_content_type is available. If not, application/octet-stream is used. + * - Content-Disposition: (optional) A string containing the disposition. Defaults to inline. + * - Content-Transfer-Encoding: (optional) Base64 is assumed for files, 8bit for other content. + * - Content-ID: (optional) for in-mail references to attachements. + * Name is mandatory, one of content and file is required, they are mutually exclusive. + * @param string $content_type + * (optional) A string containing the content-type for the combined message. Defaults to + * multipart/mixed. + * + * @return array + * An associative array containing the following elements: + * - body: A string containing the MIME-encoded multipart body of a mail. + * - headers: An array that includes some headers for the mail to be sent. + */ +function mimemail_multipart_body($parts, $content_type = 'multipart/mixed; charset=utf-8', $sub_part = FALSE) { + // Control variable to avoid boundary collision. + static $part_num = 0; + + $boundary = sha1(uniqid($_SERVER['REQUEST_TIME'], TRUE)) . $part_num++; + $body = ''; + $headers = array( + 'Content-Type' => "$content_type; boundary=\"$boundary\"", + ); + if (!$sub_part) { + $headers['MIME-Version'] = '1.0'; + $body = "This is a multi-part message in MIME format.\n"; + } + + foreach ($parts as $part) { + $part_headers = array(); + + if (isset($part['Content-ID'])) { + $part_headers['Content-ID'] = '<' . $part['Content-ID'] . '>'; + } + + if (isset($part['Content-Type'])) { + $part_headers['Content-Type'] = $part['Content-Type']; + } + + if (isset($part['Content-Disposition'])) { + $part_headers['Content-Disposition'] = $part['Content-Disposition']; + } + elseif (strpos($part['Content-Type'], 'multipart/alternative') === FALSE) { + $part_headers['Content-Disposition'] = 'inline'; + } + + if (isset($part['Content-Transfer-Encoding'])) { + $part_headers['Content-Transfer-Encoding'] = $part['Content-Transfer-Encoding']; + } + + // Mail content provided as a string. + if (isset($part['content']) && $part['content']) { + if (!isset($part['Content-Transfer-Encoding'])) { + $part_headers['Content-Transfer-Encoding'] = '8bit'; + } + $part_body = $part['content']; + if (isset($part['name'])) { + $part_headers['Content-Type'] .= '; name="' . $part['name'] . '"'; + $part_headers['Content-Disposition'] .= '; filename="' . $part['name'] . '"'; + } + + // Mail content references in a filename. + } + else { + if (!isset($part['Content-Transfer-Encoding'])) { + $part_headers['Content-Transfer-Encoding'] = 'base64'; + } + + if (!isset($part['Content-Type'])) { + $part['Content-Type'] = file_get_mimetype($part['file']); + } + + if (isset($part['name'])) { + $part_headers['Content-Type'] .= '; name="' . $part['name'] . '"'; + $part_headers['Content-Disposition'] .= '; filename="' . $part['name'] . '"'; + } + + if (isset($part['file'])) { + $file = (@is_file($part['file'])) ? file_get_contents($part['file']) : $part['file']; + $part_body = chunk_split(base64_encode($file), 76, variable_get('mimemail_crlf', "\n")); + + } + } + + $body .= "\n--$boundary\n"; + $body .= mimemail_rfc_headers($part_headers) . "\n\n"; + $body .= isset($part_body) ? $part_body : ''; + } + $body .= "\n--$boundary--\n"; + + return array('headers' => $headers, 'body' => $body); +} + +/** + * Callback for preg_replace_callback(). + */ +function _mimemail_expand_links($matches) { + return $matches[1] . _mimemail_url($matches[2]); +} + +/** + * Generate a multipart message body with a text alternative for some HTML text. + * + * @param string $body + * The HTML message body. + * @param string $subject + * The message subject. + * @param boolean $plain + * (optional) Whether the recipient prefers plaintext-only messages. Defaults to FALSE. + * @param string $plaintext + * (optional) The plaintext message body. + * @param array $attachments + * (optional) The files to be attached to the message. + * + * @return array + * An associative array containing the following elements: + * - body: A string containing the MIME-encoded multipart body of a mail. + * - headers: An array that includes some headers for the mail to be sent. + * + * The first mime part is a multipart/alternative containing mime-encoded sub-parts for + * HTML and plaintext. Each subsequent part is the required image or attachment. + */ +function mimemail_html_body($body, $subject, $plain = FALSE, $plaintext = NULL, $attachments = array()) { + if (empty($plaintext)) { + // @todo Remove once filter_xss() can handle direct descendant selectors in inline CSS. + // @see http://drupal.org/node/1116930 + // @see http://drupal.org/node/370903 + // Pull out the message body. + preg_match('||mis', $body, $matches); + $plaintext = drupal_html_to_text($matches[0]); + } + if ($plain) { + // Plain mail without attachment. + if (empty($attachments)) { + $content_type = 'text/plain'; + return array( + 'body' => $plaintext, + 'headers' => array('Content-Type' => 'text/plain; charset=utf-8'), + ); + } + // Plain mail with attachement. + else { + $content_type = 'multipart/mixed'; + $parts = array(array( + 'content' => $plaintext, + 'Content-Type' => 'text/plain; charset=utf-8', + )); + } + } + else { + $content_type = 'multipart/mixed'; + + $plaintext_part = array('Content-Type' => 'text/plain; charset=utf-8', 'content' => $plaintext); + + // Expand all local links. + $pattern = '/(]+href=")([^"]*)/mi'; + $body = preg_replace_callback($pattern, '_mimemail_expand_links', $body); + + $mime_parts = mimemail_extract_files($body); + + $content = array($plaintext_part, array_shift($mime_parts)); + $content = mimemail_multipart_body($content, 'multipart/alternative', TRUE); + $parts = array(array('Content-Type' => $content['headers']['Content-Type'], 'content' => $content['body'])); + + if ($mime_parts) { + $parts = array_merge($parts, $mime_parts); + $content = mimemail_multipart_body($parts, 'multipart/related; type="multipart/alternative"', TRUE); + $parts = array(array('Content-Type' => $content['headers']['Content-Type'], 'content' => $content['body'])); + } + } + + if (is_array($attachments) && !empty($attachments)) { + foreach ($attachments as $a) { + $a = (object) $a; + $path = isset($a->uri) ? $a->uri : (isset($a->filepath) ? $a->filepath : NULL); + $content = isset($a->filecontent) ? $a->filecontent : NULL; + $name = isset($a->filename) ? $a->filename : NULL; + $type = isset($a->filemime) ? $a->filemime : NULL; + _mimemail_file($path, $content, $name, $type, 'attachment'); + $parts = array_merge($parts, _mimemail_file()); + } + } + + return mimemail_multipart_body($parts, $content_type); +} + +/** + * Helper function to format URLs. + * + * @param string $url + * The file path. + * @param boolean $to_embed + * (optional) Wheter the URL is used to embed the file. Defaults to NULL. + * + * @return string + * A processed URL. + */ +function _mimemail_url($url, $to_embed = NULL) { + $url = urldecode($url); + + $to_link = variable_get('mimemail_linkonly', 0); + $is_image = preg_match('!\.(png|gif|jpg|jpeg)!i', $url); + $is_absolute = file_uri_scheme($url) != FALSE || preg_match('!(mailto|callto|tel)\:!', $url); + + if (!$to_embed) { + if ($is_absolute) { + return str_replace(' ', '%20', $url); + } + } + else { + $url = preg_replace('!^' . base_path() . '!', '', $url, 1); + if ($is_image) { + // Remove security token from URL, this allows for styled image embedding. + // @see https://drupal.org/drupal-7.20-release-notes + $url = preg_replace('/\\?itok=.*$/', '', $url); + if ($to_link) { + // Exclude images from embedding if needed. + $url = file_create_url($url); + $url = str_replace(' ', '%20', $url); + } + } + return $url; + } + + $url = str_replace('?q=', '', $url); + @list($url, $fragment) = explode('#', $url, 2); + @list($path, $query) = explode('?', $url, 2); + + // If we're dealing with an intra-document reference, return it. + if (empty($path)) { + return '#' . $fragment; + } + + // Get a list of enabled languages. + $languages = language_list('enabled'); + $languages = $languages[1]; + + // Default language settings. + $prefix = ''; + $language = language_default(); + + // Check for language prefix. + $path = trim($path, '/'); + $args = explode('/', $path); + foreach ($languages as $lang) { + if (!empty($args) && $args[0] == $lang->prefix) { + $prefix = array_shift($args); + $language = $lang; + $path = implode('/', $args); + break; + } + } + + $options = array( + 'query' => ($query) ? drupal_get_query_array($query) : array(), + 'fragment' => $fragment, + 'absolute' => TRUE, + 'language' => $language, + 'prefix' => $prefix, + ); + + $url = url($path, $options); + + // If url() added a ?q= where there should not be one, remove it. + if (preg_match('!^\?q=*!', $url)) { + $url = preg_replace('!\?q=!', '', $url); + } + + $url = str_replace('+', '%2B', $url); + return $url; +} + +/** + * Formats an address string. + * + * @todo Could use some enhancement and stress testing. + * + * @param mixed $address + * A user object, a text email address or an array containing name, mail. + * @param boolean $simplify + * Determines if the address needs to be simplified. Defaults to FALSE. + * + * @return string + * A formatted address string or FALSE. + */ +function mimemail_address($address, $simplify = FALSE) { + if (is_array($address)) { + // It's an array containing 'mail' and/or 'name'. + if (isset($address['mail'])) { + if (empty($address['name']) || $simplify) { + return $address['mail']; + } + else { + return '"' . addslashes(mime_header_encode($address['name'])) . '" <' . $address['mail'] . '>'; + } + } + // It's an array of address items. + $addresses = array(); + foreach ($address as $a) { + $addresses[] = mimemail_address($a); + } + return $addresses; + } + + // It's a user object. + if (is_object($address) && isset($address->mail)) { + if (empty($address->name) || $simplify) { + return $address->mail; + } + else { + return '"' . addslashes(mime_header_encode($address->name)) . '" <' . $address->mail . '>'; + } + } + + // It's formatted or unformatted string. + // @todo: shouldn't assume it's valid - should try to re-parse + if (is_string($address)) { + return $address; + } + + return FALSE; +} diff --git a/sites/all/modules/mimemail/mimemail.info b/sites/all/modules/mimemail/mimemail.info new file mode 100644 index 0000000..dc16737 --- /dev/null +++ b/sites/all/modules/mimemail/mimemail.info @@ -0,0 +1,22 @@ +name = Mime Mail +description = Send MIME-encoded emails with embedded images and attachments. +dependencies[] = mailsystem +dependencies[] = system (>=7.24) +package = Mail +core = 7.x + +configure = admin/config/system/mimemail + +files[] = includes/mimemail.mail.inc + +; Tests +files[] = tests/mimemail.test +files[] = tests/mimemail_rules.test +files[] = tests/mimemail_compress.test + +; Information added by Drupal.org packaging script on 2017-05-14 +version = "7.x-1.0" +core = "7.x" +project = "mimemail" +datestamp = "1494775689" + diff --git a/sites/all/modules/mimemail/mimemail.install b/sites/all/modules/mimemail/mimemail.install new file mode 100644 index 0000000..a09034c --- /dev/null +++ b/sites/all/modules/mimemail/mimemail.install @@ -0,0 +1,113 @@ + 'MimeMailSystem', + 'mimemail' => 'MimeMailSystem', + ) + ); + + user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('edit mimemail user settings')); +} + +/** + * Implements hook_disable(). + */ +function mimemail_disable() { + mailsystem_clear(array('mimemail' => 'MimeMailSystem')); + variable_set('mimemail_alter', FALSE); +} + +/** + * Implements hook_uninstall(). + */ +function mimemail_uninstall() { + $variables = array( + 'mimemail_alter', + 'mimemail_crlf', + 'mimemail_engine', + 'mimemail_incoming', + 'mimemail_key', + 'mimemail_textonly', + 'mimemail_sitestyle', + 'mimemail_name', + 'mimemail_mail', + 'mimemail_format', + 'mimemail_simple_address', + 'mimemail_linkonly', + 'mimemail_preserve_class' + ); + foreach ($variables as $variable) { + variable_del($variable); + } +} + +/** + * Implements hook_requirements(). + * + * Ensures that the newly-required Mail System module is available, or else + * disables the Mime Mail module and returns an informative error message. + */ +function mimemail_requirements($phase) { + if ($phase === 'install' || module_exists('mailsystem')) { + return array(); + } + $args = array( + '!mailsystem' => url('http://drupal.org/project/mailsystem'), + '%mailsystem' => 'Mail System', + '!mimemail' => url('http://drupal.org/project/mimemail'), + '%mimemail' => 'Mime Mail', + ); + if ( module_enable(array('mailsystem')) + && module_load_include('module', 'mailsystem') + ) { + drupal_set_message( + t('The %mailsystem module has been enabled because the %mimemail module now requires it.', $args) + ); + return array(); + } + return array( + 'mimemail_mailsystem' => array( + 'title' => t('%mailsystem module', $args), + 'value' => t('Not installed'), + 'description' => t( + 'The %mimemail module dependencies have changed. Please download and install the required %mailsystem module, then re-enable the %mimemail module.', $args + ), + 'severity' => REQUIREMENT_ERROR, + ), + ); +} + +/** + * Check installation requirements. + */ +function mimemail_update_7000() { + if ($requirements = mimemail_requirements('runtime')) { + throw new DrupalUpdateException($requirements['mimemail_mailsystem']['description']); + } +} + +/** + * Deletes useless variables. + */ +function mimemail_update_7001() { + variable_del('mimemail_theme'); +} + +/** + * Generate new key for authenticating incoming messages. + */ +function mimemail_update_7002() { + variable_set('mimemail_key', drupal_random_key()); + return t('Mime Mail has generated a new key to authenticate incoming messages.'); +} diff --git a/sites/all/modules/mimemail/mimemail.module b/sites/all/modules/mimemail/mimemail.module new file mode 100644 index 0000000..0b39374 --- /dev/null +++ b/sites/all/modules/mimemail/mimemail.module @@ -0,0 +1,400 @@ + 'Mime Mail', + 'description' => 'Manage mime mail system settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('mimemail_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'mimemail.admin.inc', + 'file path' => $path, + ); + $items['mimemail'] = array( + 'page callback' => 'mimemail_post', + 'access callback' => 'mimemail_incoming_access', + 'type' => MENU_CALLBACK, + 'file' => 'mimemail.incoming.inc', + 'file path' => $path, + ); + return $items; +} + +/** + * Implements hook_permission(). + */ +function mimemail_permission() { + return array( + 'view mimemail user settings' => array( + 'title' => t('View Mime Mail user settings'), + 'description' => t('View user specific settings for Mime Mail.'), + ), + 'edit mimemail user settings' => array( + 'title' => t('Edit Mime Mail user settings'), + 'description' => t('Edit user specific settings for Mime Mail.'), + ), + 'send arbitrary files' => array( + 'title' => t('Send arbitrary files'), + 'description' => t('Attach or embed files located outside the public files directory.'), + 'restrict access' => TRUE, + ), + ); +} + +/** + * Access callback to process incoming messages. + */ +function mimemail_incoming_access() { + return variable_get('mimemail_incoming', FALSE); +} + +/** + * Implements hook_field_extra_fields(). + */ +function mimemail_field_extra_fields() { + $extra['user']['user'] = array( + 'form' => array( + 'mimemail' => array( + 'label' => t('Email'), + 'description' => t('Mime Mail module settings form elements.'), + 'weight' => 0, + ), + ), + 'display' => array( + 'mimemail' => array( + 'label' => t('Email'), + 'description' => t('Mime Mail module settings form elements.'), + 'weight' => 0, + ), + ), + ); + + return $extra; +} + +/** + * Implements hook_user_view(). + */ +function mimemail_user_view($account, $view_mode, $langcode) { + $account->content['mimemail'] = array( + '#type' => 'user_profile_category', + '#title' => t('Email'), + '#access' => user_access('view mimemail user settings'), + ); + + $account->content['mimemail']['textonly'] = array( + '#type' => 'user_profile_item', + '#title' => t('Plaintext email only'), + '#markup' => empty($account->data['mimemail_textonly']) ? t('No') : t('Yes'), + ); +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Adds the Mime Mail settings on the user settings page. + */ +function mimemail_form_user_profile_form_alter(&$form, &$form_state) { + if ($form['#user_category'] == 'account') { + $account = $form['#user']; + $form['mimemail'] = array( + '#type' => 'fieldset', + '#title' => t('Email settings'), + '#weight' => 5, + '#collapsible' => TRUE, + '#access' => user_access('edit mimemail user settings'), + ); + $form['mimemail']['mimemail_textonly'] = array( + '#type' => 'checkbox', + '#title' => t('Plaintext email only'), + '#default_value' => !empty($account->data['mimemail_textonly']) ? $account->data['mimemail_textonly'] : FALSE, + '#description' => t('Check this option if you do not wish to receive email messages with graphics and styles.'), + ); + } +} + +/** + * Implements hook_user_presave(). + */ +function mimemail_user_presave(&$edit, $account, $category) { + $edit['data']['mimemail_textonly'] = isset($edit['mimemail_textonly']) ? $edit['mimemail_textonly'] : 0; +} + +/** + * Implements hook_theme(). + */ +function mimemail_theme() { + module_load_include('inc', 'mimemail', 'theme/mimemail.theme'); + return mimemail_theme_theme(); +} + +/** + * Implements hook_mail(). + */ +function mimemail_mail($key, &$message, $params) { + $context = $params['context']; + $options = array('clear' => TRUE); + + // Prepare the array of the attachments. + $attachments = array(); + $attachments_string = trim($params['attachments']); + if (!empty($attachments_string)) { + $attachment_lines = array_filter(explode("\n", trim($attachments_string))); + foreach ($attachment_lines as $filepath) { + $attachments[] = array( + 'filepath' => trim($filepath), + ); + } + } + + // We handle different address headers if set. + $address_headers = array( + 'cc' => 'Cc', + 'bcc' => 'Bcc', + 'reply-to' => 'Reply-to', + 'list-unsubscribe' => 'List-Unsubscribe', + ); + foreach ($address_headers as $param_key => $address_header) { + $params[$param_key] = empty($params[$param_key]) ? array() : explode(',', $params[$param_key]); + if (!empty($params[$param_key])) { + foreach ($params[$param_key] as $key => $address) { + $params[$param_key][$key] = token_replace($address, $context, $options); + } + $message['headers'][$address_header] = implode(',', $params[$param_key]); + } + } + + $message['to'] = token_replace($message['to'], $context, $options); + $message['subject'] = token_replace($context['subject'], $context, $options); + $message['body'][] = token_replace($context['body'], $context, $options); + $message['params']['plaintext'] = token_replace($params['plaintext'], $context, $options); + $message['params']['attachments'] = $attachments; +} + +/** + * Retreives a list of all available mailer engines. + * + * @return array + * Mailer engine names. + */ +function mimemail_get_engines() { + $engines = array(); + foreach (module_implements('mailengine') as $module) { + $engines[$module] = module_invoke($module, 'mailengine', 'list'); + } + return $engines; +} + +/** + * Implements hook_mailengine(). + * + * @param string $op + * The operation to perform on the message. + * @param array $message + * The message to perform the operation on. + * + * @return boolean + * Returns TRUE if the operation was successful or FALSE if it was not. + */ +function mimemail_mailengine($op, $message = array()) { + module_load_include('inc', 'mimemail'); + + switch ($op) { + case 'list': + $engine = array( + 'name' => t('Mime Mail'), + 'description' => t("Default mailing engine."), + ); + return $engine; + case 'settings': + // Not implemented. + break; + case 'multiple': + case 'single': + case 'send': + // Default values. + $default = array( + 'to' => '', + 'subject' => '', + 'body' => '', + 'from' => '', + 'headers' => '' + ); + $message = array_merge($default, $message); + + // If 'Return-Path' isn't already set in php.ini, we pass it separately + // as an additional parameter instead of in the header. + // However, if PHP's 'safe_mode' is on, this is not allowed. + if (isset($message['headers']['Return-Path']) && !ini_get('safe_mode')) { + $return_path_set = strpos(ini_get('sendmail_path'), ' -f'); + if (!$return_path_set) { + $return_path = trim($message['headers']['Return-Path'], '<>'); + unset($message['headers']['Return-Path']); + } + } + + $crlf = variable_get('mimemail_crlf', MAIL_LINE_ENDINGS); + + $recipients = (!is_array($message['to'])) ? array($message['to']) : $message['to']; + $subject = mime_header_encode($message['subject']); + $body = preg_replace('@\r?\n@', $crlf, $message['body']); + $headers = mimemail_rfc_headers($message['headers']); + + $result = TRUE; + foreach ($recipients as $to) { + if (isset($return_path) && !empty($return_path)) { + if (isset($_SERVER['WINDIR']) || strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE) { + // On Windows, PHP will use the value of sendmail_from for the + // Return-Path header. + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $return_path); + $result = @mail($to, $subject, $body, $headers) && $result; + ini_set('sendmail_from', $old_from); + } + else { + // On most non-Windows systems, the "-f" option to the sendmail command + // is used to set the Return-Path. + $result = @mail($to, $subject, $body, $headers, '-f' . $return_path) && $result; + } + } + else { + // The optional $additional_parameters argument to mail() is not allowed + // if safe_mode is enabled. Passing any value throws a PHP warning and + // makes mail() return FALSE. + $result = @mail($to, $subject, $body, $headers) && $result; + } + } + + return $result; + } + + return FALSE; +} + +/** + * Prepares the message for sending. + * + * @param array $message + * An array containing the message data. The optional parameters are: + * - plain: Whether to send the message as plaintext only or HTML. If evaluates to TRUE, + * then the message will be sent as plaintext. + * - plaintext: Optional plaintext portion of a multipart email. + * - attachments: An array of arrays which describe one or more attachments. + * Existing files can be added by path, dinamically-generated files + * can be added by content. The internal array contains the following elements: + * - filepath: Relative Drupal path to an existing file (filecontent is NULL). + * - filecontent: The actual content of the file (filepath is NULL). + * - filename: The filename of the file. + * - filemime: The MIME type of the file. + * The array of arrays looks something like this: + * Array + * ( + * [0] => Array + * ( + * [filepath] => '/sites/default/files/attachment.txt' + * [filecontent] => 'My attachment.' + * [filename] => 'attachment.txt' + * [filemime] => 'text/plain' + * ) + * ) + * + * @return array + * All details of the message. + */ +function mimemail_prepare_message($message) { + module_load_include('inc', 'mimemail'); + + $module = $message['module']; + $key = $message['key']; + $to = $message['to']; + $from = $message['from']; + $subject = $message['subject']; + $body = $message['body']; + + $headers = isset($message['params']['headers']) ? $message['params']['headers'] : array(); + $plain = isset($message['params']['plain']) ? $message['params']['plain'] : NULL; + $plaintext = isset($message['params']['plaintext']) ? $message['params']['plaintext'] : NULL; + $attachments = isset($message['params']['attachments']) ? $message['params']['attachments'] : array(); + + $site_name = variable_get('site_name', 'Drupal'); + $site_mail = variable_get('site_mail', ini_get('sendmail_from')); + $simple_address = variable_get('mimemail_simple_address', 0); + + // Override site mails default sender when using default engine. + if ((empty($from) || $from == $site_mail) + && variable_get('mimemail_engine', 'mimemail') == 'mimemail') { + $mimemail_name = variable_get('mimemail_name', $site_name); + $mimemail_mail = variable_get('mimemail_mail', $site_mail); + $from = array( + 'name' => !empty($mimemail_name) ? $mimemail_name : $site_name, + 'mail' => !empty($mimemail_mail) ? $mimemail_mail : $site_mail, + ); + } + + // Body is empty, this is a plaintext message. + if (empty($body)) { + $plain = TRUE; + } + // Try to determine recipient's text mail preference. + elseif (is_null($plain)) { + if (is_object($to) && isset($to->data['mimemail_textonly'])) { + $plain = $to->data['mimemail_textonly']; + } + elseif (is_string($to) && valid_email_address($to)) { + if (is_object($account = user_load_by_mail($to)) && isset($account->data['mimemail_textonly'])) { + $plain = $account->data['mimemail_textonly']; + // Might as well pass the user object to the address function. + $to = $account; + } + } + } + + // Removing newline character introduced by _drupal_wrap_mail_line(); + $subject = str_replace(array("\n"), '', trim(drupal_html_to_text($subject))); + + $hook = array( + 'mimemail_message__' . $key, + 'mimemail_message__' . $module . '__' . $key, + ); + + $variables = array( + 'module' => $module, + 'key' => $key, + 'recipient' => $to, + 'subject' => $subject, + 'body' => $body, + 'message' => $message + ); + + if (!$plain) { + $body = theme($hook, $variables); + } + + foreach (module_implements('mail_post_process') as $module) { + $function = $module . '_mail_post_process'; + $function($body, $key); + } + + $plain = $plain || variable_get('mimemail_textonly', 0); + $from = mimemail_address($from); + $mail = mimemail_html_body($body, $subject, $plain, $plaintext, $attachments); + $headers = array_merge($message['headers'], $headers, $mail['headers']); + + $message['to'] = mimemail_address($to, $simple_address); + $message['from'] = $from; + $message['subject'] = $subject; + $message['body'] = $mail['body']; + $message['headers'] = mimemail_headers($headers, $from); + + return $message; +} diff --git a/sites/all/modules/mimemail/mimemail.rules.inc b/sites/all/modules/mimemail/mimemail.rules.inc new file mode 100644 index 0000000..53716e3 --- /dev/null +++ b/sites/all/modules/mimemail/mimemail.rules.inc @@ -0,0 +1,391 @@ + array( + 'label' => t('Send HTML e-mail'), + 'group' => t('System'), + 'parameter' => array( + 'key' => array( + 'type' => 'text', + 'label' => t('Key'), + 'description' => t('A key to identify the e-mail sent.'), + ), + 'to' => array( + 'type' => 'text', + 'label' => t('To'), + 'description' => t("The mail's recipient address. The formatting of this string must comply with RFC 2822."), + ), + 'cc' => array( + 'type' => 'text', + 'label' => t('CC Recipient'), + 'description' => t("The mail's carbon copy address. You may separate multiple addresses with comma."), + 'optional' => TRUE, + 'allow null' => TRUE, + ), + 'bcc' => array( + 'type' => 'text', + 'label' => t('BCC Recipient'), + 'description' => t("The mail's blind carbon copy address. You may separate multiple addresses with comma."), + 'optional' => TRUE, + 'allow null' => TRUE, + ), + 'from_name' => array( + 'type' => 'text', + 'label' => t('Sender name'), + 'description' => t("The sender's name. Leave it empty to use the site-wide configured name."), + 'optional' => TRUE, + 'allow null' => TRUE, + ), + 'from_mail' => array( + 'type' => 'text', + 'label' => t('Sender e-mail address'), + 'description' => t("The sender's address. Leave it empty to use the site-wide configured address."), + 'optional' => TRUE, + 'allow null' => TRUE, + ), + 'reply_to' => array( + 'type' => 'text', + 'label' => t('Reply e-mail address'), + 'description' => t("The address to reply to. Leave it empty to use the sender's address."), + 'optional' => TRUE, + 'allow null' => TRUE, + ), + 'list_unsubscribe' => array( + 'type' => 'text', + 'label' => t('Unsubscription e-mail and/or URL'), + 'description' => t("An e-mail address and/or a URL which can be used for unsubscription. Values must be enclosed by angle brackets and separated by a comma."), + 'optional' => TRUE, + ), + 'subject' => array( + 'type' => 'text', + 'label' => t('Subject'), + 'description' => t("The mail's subject."), + 'translatable' => TRUE, + ), + 'body' => array( + 'type' => 'text', + 'label' => t('Body'), + 'description' => t('The mail\'s HTML body. Will be formatted using the text format selected on the settings page.', array('@url' => url('admin/config/system/mimemail'))), + 'sanitize' => TRUE, + 'optional' => TRUE, + 'translatable' => TRUE, + ), + 'plaintext' => array( + 'type' => 'text', + 'label' => t('Plain text body'), + 'description' => t("The mail's plaintext body."), + 'optional' => TRUE, + 'translatable' => TRUE, + ), + 'attachments' => array( + 'type' => 'text', + 'label' => t('Attachments'), + 'description' => t("The mail's attachments, one file per line e.g. \"files/images/mypic.png\" without quotes."), + 'optional' => TRUE, + ), + 'language' => array( + 'type' => 'token', + 'label' => t('Language'), + 'description' => t('If specified, the language used for getting the mail message and subject.'), + 'options list' => 'entity_metadata_language_list', + 'optional' => TRUE, + 'default value' => LANGUAGE_NONE, + 'default mode' => 'selector', + ), + ), + 'provides' => array( + 'send_status' => array( + 'type' => 'boolean', + 'label' => t('Send status'), + ), + ), + 'base' => 'rules_action_mimemail', + 'access callback' => 'rules_system_integration_access', + ), + 'mimemail_to_users_of_role' => array( + 'label' => t('Send HTML mail to all users of a role'), + 'group' => t('System'), + 'parameter' => array( + 'key' => array( + 'type' => 'text', + 'label' => t('Key'), + 'description' => t('A key to identify the e-mail sent.'), + ), + 'roles' => array( + 'type' => 'list', + 'label' => t('Roles'), + 'options list' => 'entity_metadata_user_roles', + 'description' => t('Select the roles whose users should receive the mail.'), + ), + 'active' => array( + 'type' => 'boolean', + 'label' => t('Send to active users'), + 'description' => t('Send mail only to active users.'), + ), + 'from_name' => array( + 'type' => 'text', + 'label' => t('Sender name'), + 'description' => t("The sender's name. Leave it empty to use the site-wide configured name."), + 'optional' => TRUE, + 'allow null' => TRUE, + ), + 'from_mail' => array( + 'type' => 'text', + 'label' => t('Sender e-mail address'), + 'description' => t("The sender's address. Leave it empty to use the site-wide configured address."), + 'optional' => TRUE, + 'allow null' => TRUE, + ), + 'reply_to' => array( + 'type' => 'text', + 'label' => t('Reply e-mail address'), + 'description' => t("The address to reply to. Leave it empty to use the sender's address."), + 'optional' => TRUE, + 'allow null' => TRUE, + ), + 'subject' => array( + 'type' => 'text', + 'label' => t('Subject'), + 'description' => t("The mail's subject."), + 'translatable' => TRUE, + ), + 'body' => array( + 'type' => 'text', + 'label' => t('Body'), + 'description' => t("The mail's message HTML body."), + 'optional' => TRUE, + 'translatable' => TRUE, + ), + 'plaintext' => array( + 'type' => 'text', + 'label' => t('Plaintext body'), + 'description' => t("The mail's message plaintext body."), + 'optional' => TRUE, + 'translatable' => TRUE, + ), + 'attachments' => array( + 'type' => 'text', + 'label' => t('Attachments'), + 'description' => t("The mail's attachments, one file per line e.g. \"files/images/mypic.png\" without quotes."), + 'optional' => TRUE, + ), + 'language_user' => array( + 'type' => 'boolean', + 'label' => t("Send mail in each recipient's language"), + 'description' => t("If checked, the mail message and subject will be sent in each user's preferred language. You can safely leave the language selector below empty if this option is selected."), + ), + 'language' => array( + 'type' => 'token', + 'label' => t('Fixed language'), + 'description' => t('If specified, the fixed language used for getting the mail message and subject.'), + 'options list' => 'entity_metadata_language_list', + 'optional' => TRUE, + 'default value' => LANGUAGE_NONE, + 'default mode' => 'selector', + ), + ), + 'provides' => array( + 'send_status' => array( + 'type' => 'boolean', + 'label' => t('Send status'), + ), + ), + 'base' => 'rules_action_mimemail_to_users_of_role', + 'access callback' => 'rules_system_integration_access', + ), + ); +} + +/** + * Implements hook_rules_action_base_upgrade_map_name(). + */ +function mimemail_rules_action_mail_upgrade_map_name($element) { + return 'mimemail'; +} + +/** + * Implements hook_rules_action_base_upgrade_map_name(). + */ +function mimemail_rules_action_mail_to_user_upgrade_map_name($element) { + return 'mimemail'; +} + +/** + * Implements hook_rules_action_base_upgrade_map_name(). + */ +function mimemail_rules_action_mail_to_users_of_role_upgrade_map_name($element) { + return 'mimemail_to_users_of_role'; +} + +/** + * Implements hook_rules_action_base_upgrade(). + */ +function mimemail_rules_action_mail_upgrade($element, RulesPlugin $target) { + $target->settings['key'] = $element['#settings']['key']; + $target->settings['from_name'] = $element['#settings']['sender']; + $target->settings['from_mail'] = $element['#settings']['from']; + $target->settings['body'] = $element['#settings']['message_html']; + $target->settings['plaintext'] = $element['#settings']['message_plaintext']; +} + +/** + * Implements hook_rules_action_base_upgrade(). + */ +function mimemail_rules_action_mail_to_user_upgrade($element, RulesPlugin $target) { + switch ($element['#settings']['#argument map']['user']) { + case 'author': + $token = 'node:author'; + break; + case 'author_unchanged': + $token = 'node-unchanged:author'; + break; + case 'user': + $token = 'site:current-user'; + break; + } + $target->settings['to:select'] = $token . ':mail'; + mimemail_rules_action_mail_upgrade($element, $target); +} + +/** + * Implements hook_rules_action_base_upgrade(). + */ +function mimemail_rules_action_mail_to_users_of_role_upgrade($element, RulesPlugin $target) { + $target->settings['roles'] = $element['#settings']['recipients']; + mimemail_rules_action_mail_upgrade($element, $target); +} + +/** + * Action Implementation: Send HTML mail. + */ +function rules_action_mimemail($key, $to, $cc = NULL, $bcc = NULL, $from_name = NULL, $from_mail = NULL, $reply_to = NULL, $list_unsubscribe = NULL, $subject, $body, $plaintext = NULL, $attachments = array(), $langcode, $settings, RulesState $state, RulesPlugin $element) { + module_load_include('inc', 'mimemail'); + + // Set the sender name and from address. + if (empty($from_mail)) { + $from = NULL; + } + else { + $from = array( + 'name' => $from_name, + 'mail' => $from_mail, + ); + // Create an address string. + $from = mimemail_address($from); + } + + // Figure out the language to use - fallback is the system default. + $languages = language_list(); + $language = isset($languages[$langcode]) ? $languages[$langcode] : language_default(); + + $params = array( + 'context' => array( + 'subject' => $subject, + 'body' => $body, + 'action' => $element, + 'state' => $state, + ), + 'cc' => $cc, + 'bcc' => $bcc, + 'reply-to' => $reply_to, + 'list-unsubscribe' => $list_unsubscribe, + 'plaintext' => $plaintext, + 'attachments' => $attachments, + ); + + $message = drupal_mail('mimemail', $key, $to, $language, $params, $from); + + return array('send_status' => !empty($message['result'])); +} + +/** + * Action: Send HTML mail to all users of a specific role group(s). + */ +function rules_action_mimemail_to_users_of_role($key, $roles, $active, $from_name = NULL, $from_mail = NULL, $reply_to = NULL, $subject, $body, $plaintext = NULL, $attachments = array(), $use_userlang = FALSE, $langcode= NULL, $settings, RulesState $state, RulesPlugin $element) { + module_load_include('inc', 'mimemail'); + + // Set the sender name and from address. + if (empty($from_mail)) { + $from = NULL; + } + else { + $from = array( + 'name' => $from_name, + 'mail' => $from_mail, + ); + // Create an address string. + $from = mimemail_address($from); + } + + $query = db_select('users', 'u'); + $query->fields('u', array('mail', 'language')); + + if ($active) { + $query->condition('u.status', 1, '='); + } + + if (in_array(DRUPAL_AUTHENTICATED_RID, $roles)) { + $query->condition('u.uid', 0, '>'); + } + else { + $query->join('users_roles', 'r', 'u.uid = r.uid'); + $query->condition('r.rid', $roles, 'IN'); + $query->distinct(); + } + + $result = $query->execute(); + + $params = array( + 'context' => array( + 'subject' => $subject, + 'body' => $body, + 'action' => $element, + 'state' => $state, + ), + 'reply-to' => $reply_to, + 'plaintext' => $plaintext, + 'attachments' => $attachments, + ); + + // Create language list before initializing foreach. + $languages = language_list(); + + $message = array('result' => TRUE); + foreach ($result as $row) { + // Decide which language to use. + if (!$use_userlang || empty($row->language) || !isset($languages[$row->language])) { + $language = isset($languages[$langcode]) ? $languages[$langcode] : language_default(); + } + else { + $language = $languages[$row->language]; + } + + $message = drupal_mail('mimemail', $key, $row->mail, $language, $params, $from); + if (!$message['result']) { + break; + } + } + if ($message['result']) { + $role_names = array_intersect_key(user_roles(TRUE), array_flip($roles)); + watchdog('rules', 'Successfully sent HTML email to the role(s) %roles.', array('%roles' => implode(', ', $role_names))); + } + + return array('send_status' => !empty($message['result'])); +} + +/** + * @} + */ + diff --git a/sites/all/modules/mimemail/modules/mimemail_action/mimemail_action.info b/sites/all/modules/mimemail/modules/mimemail_action/mimemail_action.info new file mode 100644 index 0000000..fd7c9af --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_action/mimemail_action.info @@ -0,0 +1,14 @@ +name = "Mime Mail Action" +description = "Provide actions for Mime Mail." +package = Mail +dependencies[] = mimemail +dependencies[] = trigger +core = 7.x + + +; Information added by Drupal.org packaging script on 2017-05-14 +version = "7.x-1.0" +core = "7.x" +project = "mimemail" +datestamp = "1494775689" + diff --git a/sites/all/modules/mimemail/modules/mimemail_action/mimemail_action.module b/sites/all/modules/mimemail/modules/mimemail_action/mimemail_action.module new file mode 100644 index 0000000..9d690d1 --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_action/mimemail_action.module @@ -0,0 +1,198 @@ + array( + 'type' => 'system', + 'label' => t('Send HTML e-mail'), + 'configurable' => TRUE, + 'triggers' => array('any'), + ), + ); +} + +/** + * Implements a configurable Drupal action. Sends an email. + */ +function mimemail_send_email_action($entity, $context) { + if (empty($context['node'])) { + if (get_class($entity) == 'OgMembership') { + $context['user'] = user_load($entity->etid); + } + else { + $context['node'] = $entity; + } + } + + $to = token_replace($context['to'], $context); + + // If the recipient is a registered user with a language preference, use + // the recipient's preferred language. Otherwise, use the system default + // language. + $account = user_load_by_mail($to); + if ($account) { + $language = user_preferred_language($account); + } + else { + $language = language_default(); + } + + $params = array( + 'context' => array( + 'subject' => token_replace($context['subject'], $context), + 'body' => token_replace($context['body'], $context), + ), + 'key' => $context['key'], + 'cc' => $context['cc'], + 'bcc' => $context['bcc'], + 'reply-to' => $context['reply-to'], + 'plaintext' => token_replace($context['plaintext'], $context), + 'attachments' => $context['attachments'], + ); + + drupal_mail('mimemail', $context['key'], $to, $language, $params); +} + +/** + * Form for configurable Drupal action to send an HTML mail. + */ +function mimemail_send_email_action_form($context) { + $context += array( + 'key' => '', + 'to' => '', + 'cc' => '', + 'bcc' => '', + 'reply-to' => '', + 'subject' => '', + 'body' => '', + 'format' => filter_fallback_format(), + 'plaintext' => '', + 'attachments' => '' + ); + + $form['key'] = array( + '#type' => 'textfield', + '#title' => t('Key'), + '#default_value' => $context['key'], + '#description' => t('A key to identify the e-mail sent.'), + '#required' => TRUE, + ); + $form['to'] = array( + '#type' => 'textfield', + '#title' => t('Recipient'), + '#default_value' => $context['to'], + '#maxlength' => 254, + '#description' => t('The email address to which the message should be sent OR enter [node:author:mail], [comment:author:mail], etc. if you would like to send an e-mail to the author of the original post.'), + '#required' => TRUE, + ); + $form['cc'] = array( + '#type' => 'textfield', + '#title' => t('CC Recipient'), + '#default_value' => $context['cc'], + '#description' => t("The mail's carbon copy address. You may separate multiple addresses with comma."), + '#required' => FALSE, + ); + $form['bcc'] = array( + '#type' => 'textfield', + '#title' => t('BCC Recipient'), + '#default_value' => $context['bcc'], + '#description' => t("The mail's blind carbon copy address. You may separate multiple addresses with comma."), + '#required' => FALSE, + ); + $form['reply-to'] = array( + '#type' => 'textfield', + '#title' => t('Reply e-mail address'), + '#default_value' => $context['reply-to'], + '#description' => t("The address to reply to. Leave it empty to use the sender's address."), + '#required' => FALSE, + ); + $form['subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#maxlength' => 254, + '#default_value' => $context['subject'], + '#description' => t("The subject of the message."), + ); + $form['body'] = array( + '#type' => 'text_format', + '#title' => t('Body'), + '#default_value' => $context['body'], + '#format' => $context['format'], + '#description' => t('The HTML message that should be sent. You may include placeholders like [node:title], [user:name], and [comment:body] to represent data that will be different each time message is sent. Not all placeholders will be available in all contexts.'), + ); + $form['plaintext'] = array( + '#type' => 'textarea', + '#title' => t('Plain text body'), + '#default_value' => $context['plaintext'], + '#description' => t('Optional plaintext portion of a multipart message. You may include placeholders like [node:title], [user:name], and [comment:body] to represent data that will be different each time message is sent. Not all placeholders will be available in all contexts.'), + ); + $form['attachments'] = array( + '#type' => 'textarea', + '#title' => t('Attachments'), + '#default_value' => $context['attachments'], + '#description' => t('A list of attachments, one file per line e.g. "files/images/mypic.png" without quotes.'), + ); + + return $form; +} + +/** + * Validate the action form. + */ +function mimemail_send_email_action_validate($form, $form_state) { + $to = trim($form_state['values']['to']); + if (!valid_email_address($to) && strpos($to, ':mail') === FALSE) { + form_set_error('to', t('Enter a valid email address or use a token e-mail address such as %author.', array('%author' => '[node:author:mail]'))); + } + + $cc = explode(',', $form_state['values']['cc']); + foreach ($cc as $recipient) { + $recipient = trim($recipient); + if (!empty($recipient) && !valid_email_address($recipient) && strpos($recipient, ':mail') === FALSE) { + form_set_error('cc', t('Enter a valid email address or use a token e-mail address such as %author.', array('%author' => '[node:author:mail]'))); + } + } + + $bcc = explode(',', $form_state['values']['bcc']); + foreach ($bcc as $recipient) { + $recipient = trim($recipient); + if (!empty($recipient) && !valid_email_address($recipient) && strpos($recipient, ':mail') === FALSE) { + form_set_error('bcc', t('Enter a valid email address or use a token e-mail address such as %author.', array('%author' => '[node:author:mail]'))); + } + } + + $reply_to = trim($form_state['values']['reply-to']); + if (!empty($reply_to) && !valid_email_address($reply_to) && strpos($reply_to, ':mail') === FALSE) { + form_set_error('reply-to', t('Enter a valid email address or use a token e-mail address such as %author.', array('%author' => '[node:author:mail]'))); + } +} + +/** + * Handle submission of the action form. + */ +function mimemail_send_email_action_submit($form, $form_state) { + $form_values = $form_state['values']; + + $params = array( + 'key' => $form_values['key'], + 'to' => $form_values['to'], + 'cc' => $form_values['cc'], + 'bcc' => $form_values['bcc'], + 'reply-to' => $form_values['reply-to'], + 'subject' => $form_values['subject'], + 'body' => $form_values['body']['value'], + 'format' => $form_values['body']['format'], + 'plaintext' => $form_values['plaintext'], + 'attachments' => $form_values['attachments'], + ); + + return $params; +} diff --git a/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.inc b/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.inc new file mode 100644 index 0000000..ad5432e --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.inc @@ -0,0 +1,286 @@ +]+)>(.*)|mis', $message, $matches); + if (isset($matches[0]) && isset($matches[2])) { + $css = str_replace('', '', $css); + $css = preg_replace('|\{|', "\n{\n", $css); + $css = preg_replace('|\}|', "\n}\n", $css); + $html = str_replace($matches[0], '', $message); + $parts = array('html' => $html, 'css' => $css); + } + return $parts; +} + +/** + * Compress HTML and CSS into combined message + */ +class mimemail_compress { + private $html = ''; + private $css = ''; + private $unprocessable_tags = array('wbr'); + + public function __construct($html = '', $css = '') { + $this->html = $html; + $this->css = $css; + } + + // There are some HTML tags that DOMDocument cannot process, + // and will throw an error if it encounters them. + // These functions allow you to add/remove them if necessary. + // It only strips them from the code (does not remove actual nodes). + public function add_unprocessable_tag($tag) { + $this->unprocessable_tags[] = $tag; + } + + public function remove_unprocessable_tag($tag) { + if (($key = array_search($tag, $this->unprocessable_tags)) !== FALSE) { + unset($this->unprocessableHTMLTags[$key]); + } + } + + public function compress() { + if (!class_exists('DOMDocument', FALSE)) { + return $this->html; + } + + $body = $this->html; + // Process the CSS here, turning the CSS style blocks into inline CSS. + if (count($this->unprocessable_tags)) { + $unprocessable_tags = implode('|', $this->unprocessable_tags); + $body = preg_replace("/<($unprocessable_tags)[^>]*>/i", '', $body); + } + + $err = error_reporting(0); + $doc = new DOMDocument(); + + // Try to set character encoding. + if (function_exists('mb_convert_encoding')) { + $body = mb_convert_encoding($body, 'HTML-ENTITIES', "UTF-8"); + $doc->encoding= "UTF-8"; + } + + $doc->strictErrorChecking = FALSE; + $doc->formatOutput = TRUE; + $doc->loadHTML($body); + $doc->normalizeDocument(); + + $xpath = new DOMXPath($doc); + + // Get rid of comments. + $css = preg_replace('/\/\*.*\*\//sU', '', $this->css); + + // Process the CSS file for selectors and definitions. + preg_match_all('/^\s*([^{]+){([^}]+)}/mis', $css, $matches); + + $all_selectors = array(); + foreach ($matches[1] as $key => $selector_string) { + // If there is a blank definition, skip. + if (!strlen(trim($matches[2][$key]))) continue; + // Else split by commas and duplicate attributes so we can sort by selector precedence. + $selectors = explode(',', $selector_string); + foreach ($selectors as $selector) { + // Don't process pseudo-classes. + if (strpos($selector, ':') !== FALSE) continue; + $all_selectors[] = array( + 'selector' => $selector, + 'attributes' => $matches[2][$key], + 'index' => $key, // Keep track of where it appears in the file, since order is important. + ); + } + } + + // Now sort the selectors by precedence. + usort($all_selectors, array('self', 'sort_selector_precedence')); + + // Before we begin processing the CSS file, parse the document for inline + // styles and append the normalized properties (i.e., 'display: none' + // instead of 'DISPLAY: none') as selectors with full paths (highest + // precedence), so they override any file-based selectors. + $nodes = @$xpath->query('//*[@style]'); + if ($nodes->length > 0) { + foreach ($nodes as $node) { + $style = preg_replace_callback('/[A-z\-]+(?=\:)/S', create_function('$matches', 'return strtolower($matches[0]);'), $node->getAttribute('style')); + $all_selectors[] = array( + 'selector' => $this->calculateXPath($node), + 'attributes' => $style, + ); + } + } + + foreach ($all_selectors as $value) { + // Query the body for the xpath selector. + $nodes = $xpath->query($this->css_to_xpath(trim($value['selector']))); + + foreach ($nodes as $node) { + // If it has a style attribute, get it, process it, and append (overwrite) new stuff. + if ($node->hasAttribute('style')) { + // Break it up into an associative array. + $old_style = $this->css_style_to_array($node->getAttribute('style')); + $new_style = $this->css_style_to_array($value['attributes']); + // New styles overwrite the old styles (not technically accurate, but close enough). + $compressed = array_merge($old_style, $new_style); + $style = ''; + foreach ($compressed as $k => $v) { + $style .= (drupal_strtolower($k) . ':' . $v . ';'); + } + } + else { + // Otherwise create a new style. + $style = trim($value['attributes']); + } + $node->setAttribute('style', $style); + + // Convert float to align for images. + $float = preg_match('/float:(left|right)/', $style, $matches); + if ($node->nodeName == 'img' && $float) { + $node->setAttribute('align', $matches[1]); + $node->setAttribute('vspace', 5); + $node->setAttribute('hspace', 5); + } + } + } + + // This removes styles from your email that contain display:none. You could comment these out if you want. + $nodes = $xpath->query('//*[contains(translate(@style," ",""), "display:none")]'); + foreach ($nodes as $node) { + $node->parentNode->removeChild($node); + } + + if (variable_get('mimemail_preserve_class', 0) == FALSE) { + $nodes = $xpath->query('//*[@class]'); + foreach ($nodes as $node) { + $node->removeAttribute('class'); + } + } + + error_reporting($err); + + return $doc->saveHTML(); + } + + private static function sort_selector_precedence($a, $b) { + $precedenceA = self::get_selector_precedence($a['selector']); + $precedenceB = self::get_selector_precedence($b['selector']); + + // We want these sorted ascendingly so selectors with lesser precedence get processed first and selectors with greater precedence get sorted last. + return ($precedenceA == $precedenceB) ? ($a['index'] < $b['index'] ? -1 : 1) : ($precedenceA < $precedenceB ? -1 : 1); + } + + private static function get_selector_precedence($selector) { + $precedence = 0; + $value = 100; + // Ids: worth 100, classes: worth 10, elements: worth 1. + $search = array('\#', '\.', ''); + + foreach ($search as $s) { + if (trim($selector == '')) break; + $num = 0; + $selector = preg_replace('/' . $s . '\w+/', '', $selector, -1, $num); + $precedence += ($value * $num); + $value /= 10; + } + + return $precedence; + } + + /** + * Replace callback function that matches ID attributes. + */ + private static function replace_id_attributes($m) { + return (strlen($m[1]) ? $m[1] : '*') . '[@id="' . $m[2] . '"]'; + } + + /** + * Replace callback function that matches class attributes. + */ + private static function replace_class_attributes($m) { + return (strlen($m[1]) ? $m[1] : '*') . + '[contains(concat(" ",normalize-space(@class)," "),concat(" ","' . + implode('"," "))][contains(concat(" ",normalize-space(@class)," "),concat(" ","', explode('.', substr($m[2], 1))) . + '"," "))]'; + } + + /** + * Right now we only support CSS 1 selectors, but include CSS2/3 selectors are fully possible. + * + * @see http://plasmasturm.org/log/444 + */ + private function css_to_xpath($selector) { + if (drupal_substr($selector, 0, 1) == '/') { + // Already an XPath expression. + return $selector; + } + + // Returns an Xpath selector. + $search = array( + '/\s+>\s+/', // Matches any F element that is a child of an element E. + '/(\w+)\s+\+\s+(\w+)/', // Matches any F element that is a child of an element E. + '/\s+/', // Matches any F element that is a descendant of an E element. + '/(\w)\[(\w+)\]/', // Matches element with attribute. + '/(\w)\[(\w+)\=[\'"]?(\w+)[\'"]?\]/', // Matches element with EXACT attribute. + ); + $replace = array( + '/', + '\\1/following-sibling::*[1]/self::\\2', + '//', + '\\1[@\\2]', + '\\1[@\\2="\\3"]', + ); + + $result = preg_replace($search, $replace, trim($selector)); + $result = preg_replace_callback('/(\w+)?\#([\w\-]+)/', 'mimemail_compress::replace_id_attributes', $result); + $result = preg_replace_callback('/(\w+|\*)?((\.[\w\-]+)+)/', 'mimemail_compress::replace_class_attributes', $result); + + return '//' . $result; + } + + private function css_style_to_array($style) { + $definitions = explode(';', $style); + $css_styles = array(); + foreach ($definitions as $def) { + if (empty($def) || strpos($def, ':') === FALSE) continue; + list($key, $value) = explode(':', $def, 2); + if (empty($key) || empty($value)) continue; + $css_styles[trim($key)] = trim($value); + } + return $css_styles; + } + + /** + * Get the full path to a DOM node. + * + * @param DOMNode $node + * The node to analyze. + * + * @return string + * The full xpath to a DOM node. + * + * @see http://stackoverflow.com/questions/2643533/php-getting-xpath-of-a-domnode + */ + function calculateXPath(DOMNode $node) { + $xpath = ''; + $q = new DOMXPath($node->ownerDocument); + + do { + $position = 1 + $q->query('preceding-sibling::*[name()="' . $node->nodeName . '"]', $node)->length; + $xpath = '/' . $node->nodeName . '[' . $position . ']' . $xpath; + $node = $node->parentNode; + } + while (!$node instanceof DOMDocument); + + return $xpath; + } +} diff --git a/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.info b/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.info new file mode 100644 index 0000000..4c15829 --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.info @@ -0,0 +1,14 @@ +name = Mime Mail CSS Compressor +description = Converts CSS to inline styles in an HTML message. (Requires the PHP DOM extension.) +package = Mail +dependencies[] = mimemail +core = 7.x + +files[] = mimemail_compress.inc + +; Information added by Drupal.org packaging script on 2017-05-14 +version = "7.x-1.0" +core = "7.x" +project = "mimemail" +datestamp = "1494775689" + diff --git a/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.install b/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.install new file mode 100644 index 0000000..1e3bdc8 --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.install @@ -0,0 +1,31 @@ + $t('Mime Mail Compress requires the PHP DOM extension to be enabled.'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('Disabled'), + ); + } + + $requirements['dom']['title'] = $t('PHP DOM extension'); + + return $requirements; +} diff --git a/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.module b/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.module new file mode 100644 index 0000000..e538fc4 --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_compress/mimemail_compress.module @@ -0,0 +1,21 @@ +compress(); + $message = $output; + } +} diff --git a/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.info b/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.info new file mode 100644 index 0000000..ac3a01f --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.info @@ -0,0 +1,12 @@ +name = Mime Mail Example +description = Example of how to use the Mime Mail module. +dependencies[] = mimemail +package = Example modules +core = 7.x + +; Information added by Drupal.org packaging script on 2017-05-14 +version = "7.x-1.0" +core = "7.x" +project = "mimemail" +datestamp = "1494775689" + diff --git a/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.install b/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.install new file mode 100644 index 0000000..4f06264 --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.install @@ -0,0 +1,20 @@ + 'MimeMailSystem')); +} + +/** + * Implements hook_disable(). + */ +function mimemail_example_disable() { + mailsystem_clear(array('mimemail_example' => 'MimeMailSystem')); +} diff --git a/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.module b/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.module new file mode 100644 index 0000000..85fe957 --- /dev/null +++ b/sites/all/modules/mimemail/modules/mimemail_example/mimemail_example.module @@ -0,0 +1,170 @@ + 'Mime Mail Example', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('mimemail_example_form'), + 'access arguments' => array('access content'), + ); + + return $items; +} + +/** + * Implements hook_mail(). + */ +function mimemail_example_mail($key, &$message, $params) { + $message['subject'] = $params['subject']; + $message['body'][] = $params['body']; +} + +/** + * The example email contact form. + */ +function mimemail_example_form() { + global $user; + + $form['intro'] = array( + '#markup' => t('Use this form to send a HTML message to an e-mail address. No spamming!'), + ); + + $form['key'] = array( + '#type' => 'textfield', + '#title' => t('Key'), + '#default_value' => 'test', + '#required' => TRUE, + ); + + $form['to'] = array( + '#type' => 'textfield', + '#title' => t('To'), + '#default_value' => $user->mail, + '#required' => TRUE, + ); + + $form['from'] = array( + '#type' => 'textfield', + '#title' => t('Sender name'), + ); + + $form['from_mail'] = array( + '#type' => 'textfield', + '#title' => t('Sender e-mail address'), + ); + + $form['params'] = array( + '#tree' => TRUE, + 'headers' => array( + 'Cc' => array( + '#type' => 'textfield', + '#title' => t('Cc'), + ), + 'Bcc' => array( + '#type' => 'textfield', + '#title' => t('Bcc'), + ), + 'Reply-to' => array( + '#type' => 'textfield', + '#title' => t('Reply to'), + ), + 'List-unsubscribe' => array( + '#type' => 'textfield', + '#title' => t('List-unsubscribe'), + ), + ), + 'subject' => array( + '#type' => 'textfield', + '#title' => t('Subject'), + ), + 'body' => array( + '#type' => 'textarea', + '#title' => t('HTML message'), + ), + 'plain' => array( + '#type' => 'hidden', + '#states' => array( + 'value' => array( + ':input[name="body"]' => array('value' => ''), + ), + ), + ), + 'plaintext' => array( + '#type' => 'textarea', + '#title' => t('Plain text message'), + ), + 'attachments' => array( + '#name' => 'files[attachment]', + '#type' => 'file', + '#title' => t('Choose a file to send as attachment'), + ), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Send message'), + ); + + return $form; +} + +/** + * Form validation logic for the email contact form. + */ +function mimemail_example_form_validate($form, &$form_state) { + $values = &$form_state['values']; + + if (!valid_email_address($values['to'])) { + form_set_error('to', t('That e-mail address is not valid.')); + } + + $file = file_save_upload('attachment'); + if ($file) { + $file = file_move($file, 'public://'); + $values['params']['attachments'][] = array( + 'filepath' => $file->uri, + ); + } +} + +/** + * Form submission logic for the email contact form. + */ +function mimemail_example_form_submit($form, &$form_state) { + $values = $form_state['values']; + + $module = 'mimemail_example'; + $key = $values['key']; + $to = $values['to']; + $language = language_default(); + $params = $values['params']; + + if (!empty($values['from_mail'])) { + module_load_include('inc', 'mimemail'); + $from = mimemail_address(array( + 'name' => $values['from'], + 'mail' => $values['from_mail'], + )); + } + else { + $from = $values['from']; + } + + $send = TRUE; + + $result = drupal_mail($module, $key, $to, $language, $params, $from, $send); + if ($result['result'] == TRUE) { + drupal_set_message(t('Your message has been sent.')); + } + else { + drupal_set_message(t('There was a problem sending your message and it was not sent.'), 'error'); + } +} diff --git a/sites/all/modules/mimemail/tests/mimemail.test b/sites/all/modules/mimemail/tests/mimemail.test new file mode 100644 index 0000000..6619841 --- /dev/null +++ b/sites/all/modules/mimemail/tests/mimemail.test @@ -0,0 +1,99 @@ + 'Mime Mail unit tests', + 'description' => 'Test that Mime Mail helper functions work properly.', + 'group' => 'Mime Mail', + ); + } + + function setUp() { + drupal_load('module', 'mimemail'); + parent::setUp(); + } + + function testHeaders() { + // Test the regular expression for extracting the mail address. + $chars = array('-', '.', '+', '_'); + $name = $this->randomString(); + $local = $this->randomName() . $chars[array_rand($chars)] . $this->randomName(); + $domain = $this->randomName() . '-' . $this->randomName() . '.' . $this->randomName(rand(2, 4)); + $headers = mimemail_headers(array(), "$name <$local@$domain>"); + $result = $headers['Return-Path']; + $expected = "<$local@$domain>"; + $this->assertIdentical($result, $expected, 'Return-Path header field correctly set.'); + } + + function testUrl() { + $result = _mimemail_url('#'); + $this->assertIdentical($result, '#', 'Hash mark URL without fragment left intact.'); + + $url = '/sites/default/files/styles/thumbnail/public/image.jpg?itok=Wrl6Qi9U'; + $result = _mimemail_url($url, TRUE); + $expected = 'sites/default/files/styles/thumbnail/public/image.jpg'; + $this->assertIdentical($result, $expected, 'Security token removed from styled image URL.'); + + $expected = $url = 'public://' . $this->randomName() . ' ' . $this->randomName() . '.' . $this->randomName(3); + $result = _mimemail_url($url, TRUE); + $this->assertIdentical($result, $expected, 'Space in the filename of the attachment left intact.'); + } + +} + +/** + * Tests functions from the Mime Mail module. + */ +class MimeMailWebTestCase extends DrupalWebTestCase { + protected $adminUser; + + public static function getInfo() { + return array( + 'name' => 'Mime Mail web tests', + 'description' => 'Test that Mime Mail works properly.', + 'group' => 'Mime Mail', + ); + } + + public function setUp() { + parent::setUp('mailsystem', 'mimemail'); + + $permissions = array( + 'access administration pages', + 'administer site configuration', + ); + + // Check to make sure that the array of permissions are valid. + $this->checkPermissions($permissions, TRUE); + + // Create and login user. + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + public function testUrl() { + $this->drupalPost('admin/config/system/mimemail', + array('mimemail_linkonly' => TRUE), + t('Save configuration')); + + $url = 'public://' . $this->randomName() . ' ' . $this->randomName() . '.jpg'; + $result = _mimemail_url($url, TRUE); + $expected = str_replace(' ', '%20', file_create_url($url)); + $message = 'Stream wrapper converted to web accessible URL for linked image.'; + $this->assertIdentical($result, $expected, $message); + } + +} diff --git a/sites/all/modules/mimemail/tests/mimemail_compress.test b/sites/all/modules/mimemail/tests/mimemail_compress.test new file mode 100644 index 0000000..2b05613 --- /dev/null +++ b/sites/all/modules/mimemail/tests/mimemail_compress.test @@ -0,0 +1,31 @@ + 'Mime Mail Compress unit tests', + 'description' => 'Test that Mime Mail Compress helper functions work properly.', + 'group' => 'Mime Mail', + ); + } + + function setUp() { + drupal_load('module', 'mimemail_compress'); + parent::setUp(); + } + + + +} diff --git a/sites/all/modules/mimemail/tests/mimemail_rules.test b/sites/all/modules/mimemail/tests/mimemail_rules.test new file mode 100644 index 0000000..68dfdcf --- /dev/null +++ b/sites/all/modules/mimemail/tests/mimemail_rules.test @@ -0,0 +1,225 @@ + 'Rules integration', + 'description' => 'Test the Rules integration.', + 'group' => 'Mime Mail', + ); + } + + public function setUp() { + parent::setUp('mailsystem', 'locale', 'entity', 'entity_token', 'rules', 'mimemail'); + + $permissions = array( + 'access administration pages', + 'edit mimemail user settings', + 'administer languages', + 'administer rules', + 'bypass rules access', + 'access rules debug', + ); + + // Check to make sure that the array of permissions are valid. + $this->checkPermissions($permissions, TRUE); + + // Create and login user. + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + + // Enable another language too. + foreach (array('de', 'it') as $langcode) { + $edit = array(); + $edit['langcode'] = $langcode; + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + } + + // Make sure we are not using a stale list. + drupal_static_reset('language_list'); + } + + /** + * Create rule with "mimemail" action and fire it. + */ + public function testMailToUserAction() { + $settings = array( + 'key' => 'mail-key-' . $this->randomName(), + 'to' => $this->randomName() . '@example.com', + 'from' => $this->randomName() . '@example.com', + 'subject' => $this->randomName(), + 'body' => $this->randomName(60) . '


      ', + 'plaintext' => $this->randomName(30) . '


      ', + ); + + // Set no language for the mail and check if the system default is used. + $rule = rule(); + $rule->action('mimemail', array( + 'key' => $settings['key'], + 'to' => $settings['to'], + 'from_mail' => $settings['from'], + 'subject' => $settings['subject'], + 'body' => $settings['body'], + 'plaintext' => $settings['plaintext'], + 'language' => '', + ))->save(); + + $rule->execute(); + $mails = $this->drupalGetMails(array('key' => $settings['key'])); + $this->assertEqual(count($mails), 1); + $mail = reset($mails); + $this->assertEqual($mail['to'], $settings['to']); + $this->assertEqual($mail['from'], $settings['from']); + $this->assertEqual($mail['subject'], $settings['subject']); + $this->assertEqual($mail['params']['context']['body'], $settings['body']); + $this->assertEqual($mail['params']['plaintext'], $settings['plaintext']); + $this->assertEqual($mail['language']->language, language_default('language')); + + // Explicitly set another language for the mail. + $rule_action = $rule->elementMap()->lookup(3); + unset($rule_action->settings['language:select']); + $rule_action->settings['language'] = 'de'; + $rule_action->settings['key'] = $settings['key']; + $rule->save(); + $rule->execute(); + $mails = $this->drupalGetMails(array('key' => $settings['key'])); + $this->assertEqual(count($mails), 2); + $mail = end($mails); + $this->assertEqual($mail['language']->language, 'de'); + } + + /** + * Create rule with "mimemail_to_users_of_role" action and fire it. + */ + public function testMailToUsersOfRoleAction() { + $languages = language_list(); + + // Add more users and roles. + $users = array( + $this->randomName() . '@example.com' => 'en', + $this->randomName() . '@example.com' => 'de', + $this->randomName() . '@example.com' => 'it', + $this->randomName() . '@example.com' => '', + $this->randomName() . '@example.com' => 'invalid', + ); + + $mimemail_role = $this->drupalCreateRole(array()); + + foreach ($users as $email => $language) { + $user = $this->drupalCreateUser(array( + 'access administration pages', + )); + $user->language = $language; + $user->mail = $email; + $user->roles[$mimemail_role] = $mimemail_role; + user_save($user); + } + + $settings = array( + 'key' => 'mail-key-' . $this->randomName(), + 'from' => $this->randomName() . '@example.com', + 'subject' => $this->randomName(), + 'body' => $this->randomName(60) . '


      ', + 'plaintext' => $this->randomName(30) . '


      ', + ); + + // Rest the collected mails. + variable_set('drupal_test_email_collector', array()); + + // Send mails to all users of a role and respect the language of the users. + // Don't enforce a specific language as fallback use the system default. + $rule = rule(); + $rule->action('mimemail_to_users_of_role', array( + 'key' => $settings['key'], + 'from_mail' => $settings['from'], + 'subject' => $settings['subject'], + 'body' => $settings['body'], + 'plaintext' => $settings['plaintext'], + 'roles' => array($mimemail_role => $mimemail_role), + 'active' => TRUE, + 'language_user' => TRUE, + 'language' => '', + )); + $rule->save(); + $rule->execute(); + $mails = $this->drupalGetMails(array('key' => $settings['key'])); + $this->assertEqual(count($mails), count($users)); + $mail = reset($mails); + $this->assertEqual($mail['from'], $settings['from']); + $this->assertEqual($mail['subject'], $settings['subject']); + $this->assertEqual($mail['params']['context']['body'], $settings['body']); + $this->assertEqual($mail['params']['plaintext'], $settings['plaintext']); + foreach ($mails as $mail) { + // If the user hasn't a proper language the system default has to be used + // if the rules action doesn't provide a language to use. + $user_language = (!empty($languages[$users[$mail['to']]])) ? $users[$mail['to']] : language_default('language'); + $this->assertEqual($mail['language']->language, $user_language); + } + + // Rest the collected mails. + variable_set('drupal_test_email_collector', array()); + + // Send mails to all users of a role and respect the language of the users. + // Enforce German as fallback language if an user doesn't have a language. + $rule->elementMap()->lookup(3)->settings['language'] = 'de'; + $rule->save(); + $rule->execute(); + $mails = $this->drupalGetMails(array('key' => $settings['key'])); + $this->assertEqual(count($mails), count($users)); + foreach ($mails as $mail) { + // If the user hasn't a proper language the language set in the rules + // action has to be used. + $user_language = (!empty($languages[$users[$mail['to']]])) ? $users[$mail['to']] : 'de'; + $this->assertEqual($mail['language']->language, $user_language); + } + + // Rest the collected mails. + variable_set('drupal_test_email_collector', array()); + + // Send mails to all users of a role but use the same language for all. + $rule->elementMap()->lookup(3)->settings['language_user'] = FALSE; + $rule->elementMap()->lookup(3)->settings['language'] = 'it'; + $rule->save(); + $rule->execute(); + $mails = $this->drupalGetMails(array('key' => $settings['key'])); + foreach ($mails as $mail) { + $this->assertEqual($mail['language']->language, 'it'); + } + + // Rest the collected mails. + variable_set('drupal_test_email_collector', array()); + + // Send mails to all users of a role except deactivated users. + // Disable one of the users. + reset($users); + $user = user_load_by_mail(key($users)); + $user->status = 0; + user_save($user); + $rule->execute(); + $mails = $this->drupalGetMails(array('key' => $settings['key'])); + $this->assertEqual(count($mails), count($users) - 1); + + // Rest the collected mails. + variable_set('drupal_test_email_collector', array()); + + // Send mails to all users, also to deactivated ones. + $rule->elementMap()->lookup(3)->settings['active'] = FALSE; + $rule->save(); + $rule->execute(); + $mails = $this->drupalGetMails(array('key' => $settings['key'])); + // One user is disabled but it should be ignored. + $this->assertEqual(count($mails), count($users)); + } +} diff --git a/sites/all/modules/mimemail/theme/mimemail-message.tpl.php b/sites/all/modules/mimemail/theme/mimemail-message.tpl.php new file mode 100644 index 0000000..179573d --- /dev/null +++ b/sites/all/modules/mimemail/theme/mimemail-message.tpl.php @@ -0,0 +1,40 @@ + + + + + + + + + > +
      +
      + +
      +
      + + diff --git a/sites/all/modules/mimemail/theme/mimemail.theme.inc b/sites/all/modules/mimemail/theme/mimemail.theme.inc new file mode 100644 index 0000000..36dc818 --- /dev/null +++ b/sites/all/modules/mimemail/theme/mimemail.theme.inc @@ -0,0 +1,102 @@ + array( + 'variables' => array('module' => NULL, 'key' => NULL, 'recipient' => NULL, 'subject' => NULL, 'body' => NULL, 'message' => array()), + 'template' => 'mimemail-message', + 'pattern' => 'mimemail_message__', + 'file' => 'mimemail.theme.inc', + 'mail theme' => TRUE, + 'path' => $path, + ) + ); +} + +/** + * A preprocess function for theme('mimemail_message'). + * + * The $variables array initially contains the following arguments: + * - $recipient: The recipient of the message + * - $key: The mailkey associated with the message + * - $subject: The message subject + * - $body: The message body + * + * @see mimemail-message.tpl.php + */ +function template_preprocess_mimemail_message(&$variables) { + $theme = mailsystem_get_mail_theme(); + $themepath = drupal_get_path('theme', $theme); + + $sitestyle = variable_get('mimemail_sitestyle', 1); + $mailstyles = file_scan_directory($themepath, '#^mail(-.+)?\.(c|le|sc|sa)ss$#'); + + // Check recursively for the existence of a mail.css file in the theme folder. + if (!empty($mailstyles)) { + foreach ($mailstyles as $mailstyle) { + $styles = $mailstyle->uri; + } + } + // If no mail.css was found and the site style sheets including is enabled, + // gather all style sheets and embed a version of all style definitions. + elseif ($sitestyle) { + // Grab local.css if it exists (support for Fusion based themes). + $local = $themepath . '/css/local.css'; + if (@file_exists($local)) { + $css_all = drupal_add_css($local, array('group' => CSS_THEME)); + } + else { + $css_all = drupal_add_css(); + } + $css_files = array(); + foreach ($css_all as $key => $options) { + if ($options['group'] == CSS_THEME && $options['type'] == 'file' && + ($options['media'] == 'all' || $options['media'] == 'screen')) { + $css_files[$key] = $options; + } + } + if (variable_get('preprocess_css', FALSE)) { + $pattern = '|]*)>/', // Remove the style tag. + '/@import\s+url\("([^"]+)"\);+/', // Remove the import directive. + '|' . $GLOBALS['base_url'] . '/([^"?]*)[?"].*|' // Remove the base URL. + ); + $replacement = array('', '\1', '\1'); + } + $styles = preg_replace($pattern, $replacement, drupal_get_css($css_files)); + } + + $css = ''; + if (isset($styles)) { + // Process each style sheet. + foreach (explode("\n", $styles) as $style) { + if (!empty($style)) { + $css .= drupal_load_stylesheet($style, TRUE); + } + } + + // Wordwrap to adhere to RFC821 + $css = wordwrap($css, 700); + } + + // Set styles for the message. + $variables['css'] = $css; + + // Set template alternatives. + $variables['theme_hook_suggestions'][] = 'mimemail_message__' . str_replace('-', '_', $variables['key']); + + // Process identifiers to be proper CSS classes. + $variables['module'] = str_replace('_', '-', $variables['module']); + $variables['key'] = str_replace('_', '-', $variables['key']); +} diff --git a/sites/all/modules/module_missing_message_fixer/LICENSE.txt b/sites/all/modules/module_missing_message_fixer/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/module_missing_message_fixer/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/module_missing_message_fixer/README.txt b/sites/all/modules/module_missing_message_fixer/README.txt new file mode 100644 index 0000000..b0029f0 --- /dev/null +++ b/sites/all/modules/module_missing_message_fixer/README.txt @@ -0,0 +1,53 @@ + +CONTENTS OF THIS FILE +--------------------- + +* Introduction +* Requirements +* Installation +* Configuration +* Maintainers + +INTRODUCTION +------------ + +The Module Missing Message Fixer module displays a list of missing modules that appear in the message log after the Drupal 7.50 release and provides an interface to fix the entries. This module was designed in sn effort to help those people who didnt want to run SQL commands directly. + +* For a full description of the module, visit https://www.drupal.org/project/module_missing_message_fixer +* To submit bug reports and feature suggestions, or to track changes visit https://www.drupal.org/project/issues/module_missing_message_fixer + + +REQUIREMENTS +------------ + +This module requires no additional modules outside of Drupal core. + + +INSTALLATION +------------ + +Install the Module Missing Message Fixer module as you would normally install a contributed Drupal module. Visit https://www.drupal.org/docs/7/extending-drupal-7/installing-contributed-modules-find-import-enable-configure-drupal-7 for further information. + + +CONFIGURATION +------------- + +Through the UI interface: +1. Enable the Module Missing Message Fixer module. +2. Ensure that you have the proper permissions by navigating to Administration > People > Permissions and selecting the checkbox. +3. Navigate to Administration > Configuration > System > Missing Module Message Fixer. Select the missing modules you would like to fix and select the "Remove These Errors!" tab. The module will now go through and remove the "ghost records" from the systems table. +Through Drush: +1. $ drush en module_missing_message_fixer +2. $ drush module-missing-message-fixer-list OR $ drush mmmfl. This will generate a list of missing modules. +3. $ drush module-missing-message-fixer-fix machine_name_of_module (or --all) OR $ drush mmmff machine_name_of_module (or --all). This will fix the missing modules entities. + +For more information visit https://www.drupal.org/node/2487215 + + +MAINTAINERS +----------- + +* John Ouellet (labboy0276) - https://www.drupal.org/u/labboy0276 + +Supporting organization: +* Kalamuna - https://www.drupal.org/kalamuna diff --git a/sites/all/modules/module_missing_message_fixer/includes/module_missing_message_fixer.admin.inc b/sites/all/modules/module_missing_message_fixer/includes/module_missing_message_fixer.admin.inc new file mode 100644 index 0000000..27e7bbc --- /dev/null +++ b/sites/all/modules/module_missing_message_fixer/includes/module_missing_message_fixer.admin.inc @@ -0,0 +1,117 @@ + 'https://www.drupal.org/node/2763581', + 'content' => 'https://www.drupal.org/node/2763555', + 'field_collection_table' => 'https://www.drupal.org/node/2764331', + ); + + // Set the header. + $header = array( + 'name' => 'Name', + 'type' => 'Type', + 'filename' => 'Filename', + ); + // Grab all the modules in the system table. + $query = db_query("SELECT filename, type, name FROM {system}"); + + // Go through the query and check to see if the module exists in the directory. + foreach ($query->fetchAll() as $record) { + // Grab the checker. + $check = drupal_get_filename($record->type, $record->name, $record->filename, FALSE); + + // If drupal_get_filename returns null = we got problems. + if (is_null($check) && + $record->name != 'default' && + !array_key_exists($record->name, $special)) { + // Go ahead and set the row if all is well. + $rows[$record->name] = array( + 'name' => $record->name, + 'type' => $record->type, + 'filename' => $record->filename, + ); + } + // Go ahead and print out a special message for the user. + elseif (array_key_exists($record->name, $special)) { + // Add exception to this since content module seems to be ubercart sad only. + if ($record->name == 'content' && !module_exists('ubercart')) { + $rows[$record->name] = array( + 'name' => $record->name, + 'type' => $record->type, + 'filename' => $record->filename, + ); + } + // Everyone else fails into here. + else { + // Set the message. + $msg = t('The @module module has a patch. See this issue for more information. It WILL NOT be removed by Module Missing Message Fixer.', array( + '@module' => $record->name, + '@link' => $special[$record->name], + )); + // Now print it! + drupal_set_message($msg, 'status', FALSE); + } + } + } + + // Fancy title string. + $title = t('This list comes from the system table and is checked against the drupal_get_filename() function. See this issue for more information.', array( + '@link' => 'https://www.drupal.org/node/2487215', + )); + + // Title. + $form['title'] = array( + '#type' => 'item', + '#markup' => '

      ' . $title . '

      ', + ); + + // Fancy submit buttons to win this. + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Remove These Errors!'), + '#submit' => array('module_missing_message_fixer_form_submit'), + '#prefix' => '
      ', + '#suffix' => '
      ', + ); + + // Set the tables select to make this more granular. + $form['table'] = array( + '#type' => 'tableselect', + '#header' => $header, + '#options' => $rows, + '#empty' => t('No Missing Modules Found!!!'), + ); + + return $form; +} + +/** + * Submit handler for Missing Module Message Fixer Form. + */ +function module_missing_message_fixer_form_submit($form, &$form_state) { + $modules = array(); + // Go through each record and add it to the array to win. + foreach ($form_state['values']['table'] as $module) { + $modules[] = $module; + } + + // Delete if there is no modules. + if (count($modules) > 0) { + db_delete('system') + ->condition('name', $modules, 'IN') + ->execute(); + } +} diff --git a/sites/all/modules/module_missing_message_fixer/includes/module_missing_message_fixer.drush.inc b/sites/all/modules/module_missing_message_fixer/includes/module_missing_message_fixer.drush.inc new file mode 100644 index 0000000..0419832 --- /dev/null +++ b/sites/all/modules/module_missing_message_fixer/includes/module_missing_message_fixer.drush.inc @@ -0,0 +1,171 @@ + 'Name', + 'type' => 'Type', + 'filename' => 'Filename', + ); + } + + // These are special modules that have their own patches already. + // This will help eliminate some of the brute force of this module. + $special = array( + 'adminimal_theme' => 'https://www.drupal.org/node/2763581', + 'content' => 'https://www.drupal.org/node/2763555', + 'field_collection_table' => 'https://www.drupal.org/node/2764331', + ); + // Grab all the modules in the system table. + $query = db_query("SELECT filename, type, name FROM {system}"); + // Go through the query and check to see if the module exists in the directory. + foreach ($query->fetchAll() as $record) { + // Grab the checker. + $check = drupal_get_filename($record->type, $record->name, $record->filename, FALSE); + // If drupal_get_filename returns null = we got problems. + if (is_null($check) && + $record->name != 'default' && + !array_key_exists($record->name, $special)) { + // Go ahead and set the row if all is well. + $rows[$record->name] = array( + 'name' => $record->name, + 'type' => $record->type, + 'filename' => $record->filename, + ); + } + // Go ahead and print out a special message for the user. + elseif (array_key_exists($record->name, $special)) { + // Add exception to this since content module seems to be ubercart sad only. + if ($record->name == 'content' && !module_exists('ubercart')) { + $rows[$record->name] = array( + 'name' => $record->name, + 'type' => $record->type, + 'filename' => $record->filename, + ); + } + // Everyone else fails into here. + else { + // Set the message. + $msg = t('The @module module has a patch. See this issue for more information. It WILL NOT be removed by Module Missing Message Fixer.', array( + '@module' => $record->name, + '@link' => $special[$record->name], + )); + // Now print it! + drupal_set_message($msg, 'status', FALSE); + } + } + } + + // Return the rows for the fixer here. + if ($return) { + return $rows; + } + + // Print Table here instead of in the hook_command. + $output = count($rows) > 1 ? drush_format_table($rows, TRUE) : 'No Missing Modules Found!!!'; + drush_print($output); +} + +/** + * Implements hook_drush_help(). + */ +function module_missing_message_fixer_drush_help($section) { + switch ($section) { + case 'module-missing-message-fixer-list': + $result = dt("Returns a list of modules that have missing messages."); + case 'module-missing-message-fixer-fix': + $result = dt("Fixes a specified module that has missing messages. (optional --all)"); + } + return $result; +} + +/** + * Implements hook_drush_command(). + */ +function module_missing_message_fixer_drush_command() { + $items = array(); + $items['module-missing-message-fixer-list'] = array( + 'description' => dt('Returns a list of modules that have missing messages.'), + 'aliases' => array( + 'mmmfl', + ), + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL + ); + $items['module-missing-message-fixer-fix'] = array( + 'description' => dt('Fixes modules that have missing messages.'), + 'aliases' => array( + 'mmmff', + ), + 'arguments' => array( + 'name' => 'The name of the module to fix.', + ), + 'options' => array( + 'all' => dt('Fixes all module missing messages'), + ), + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL + ); + return $items; +} + +/** + * Drush command. + * + * Displays a list of modules that have missing messages. + */ +function drush_module_missing_message_fixer_list() { + module_missing_message_fixer_check_modules(); +} + +/** + * Drush command. + * + * @param string $name + * The name of the module to fix messages for. + */ +function drush_module_missing_message_fixer_fix($name = NULL) { + $module = array(); + if (drush_get_option('all') !== NULL) { + $rows = module_missing_message_fixer_check_modules(TRUE); + if (!empty($rows)) { + foreach ($rows as $row) { + $modules[] = $row['name']; + } + } + } + elseif ($name !== NULL) { + if (module_exists($name)) { + $modules[] = $name; + } + else { + drush_log(dt('Module ' . $name . ' was not found.'), 'error'); + } + } + else { + drush_log(dt('Missing input, provide module name or run with --all'), 'error'); + } + // Delete if there is no modules. + if (count($modules) > 0) { + db_delete('system') + ->condition('name', $modules, 'IN') + ->execute(); + if (drush_get_option('all') !== NULL) { + drush_log(dt('All missing references have been removed.'), 'success'); + } + elseif (isset($name)) { + if (in_array($name, $modules)) { + drush_log(dt('Reference to ' . $name . ' (if found) has been removed.'), 'success'); + } + } + } +} diff --git a/sites/all/modules/module_missing_message_fixer/module_missing_message_fixer.info b/sites/all/modules/module_missing_message_fixer/module_missing_message_fixer.info new file mode 100644 index 0000000..4517fb9 --- /dev/null +++ b/sites/all/modules/module_missing_message_fixer/module_missing_message_fixer.info @@ -0,0 +1,13 @@ +name = Module Missing Message Fixer +description = This module displays a list of missing modules that appear after the Drupal 7.50 release and lets you fix the entries. +package = Administration +core = 7.x + +configure = admin/config/system/module-missing-message-fixer + +; Information added by Drupal.org packaging script on 2017-07-25 +version = "7.x-1.5" +core = "7.x" +project = "module_missing_message_fixer" +datestamp = "1501017559" + diff --git a/sites/all/modules/module_missing_message_fixer/module_missing_message_fixer.module b/sites/all/modules/module_missing_message_fixer/module_missing_message_fixer.module new file mode 100644 index 0000000..b7548ec --- /dev/null +++ b/sites/all/modules/module_missing_message_fixer/module_missing_message_fixer.module @@ -0,0 +1,36 @@ + array( + 'title' => t('Administer Module Missing Message Fixer'), + ), + ); +} + +/** + * Implements hook_menu(). + */ +function module_missing_message_fixer_menu() { + $items = array(); + + $items['admin/config/system/module-missing-message-fixer'] = array( + 'title' => 'Missing Module Message Fixer', + 'description' => 'This module display a list of missing module that appear after the Drupal 7.50 release and lets you fix the entries.', + 'type' => MENU_NORMAL_ITEM, + 'page callback' => 'drupal_get_form', + 'page arguments' => array('module_missing_message_fixer_form'), + 'access arguments' => array('administer module missing message fixer'), + 'file' => 'includes/module_missing_message_fixer.admin.inc', + ); + + return $items; +} diff --git a/sites/all/themes/exp/css/mail.css b/sites/all/themes/exp/css/mail.css new file mode 100644 index 0000000..e69de29 diff --git a/sites/all/themes/exp/template/mail/mimemail-message.tpl.php b/sites/all/themes/exp/template/mail/mimemail-message.tpl.php new file mode 100644 index 0000000..179573d --- /dev/null +++ b/sites/all/themes/exp/template/mail/mimemail-message.tpl.php @@ -0,0 +1,40 @@ + + + + + + + + + > +
      +
      + +
      +
      + +