Index: mollom.admin.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.admin.inc,v retrieving revision 1.41 diff -u -p -r1.41 mollom.admin.inc --- mollom.admin.inc 26 Sep 2010 19:42:55 -0000 1.41 +++ mollom.admin.inc 27 Sep 2010 19:47:48 -0000 @@ -13,9 +13,8 @@ function mollom_admin_form_list() { _mollom_testing_mode_warning(); $modes = array( - MOLLOM_MODE_DISABLED => t('None'), - MOLLOM_MODE_CAPTCHA => t('CAPTCHA'), MOLLOM_MODE_ANALYSIS => t('Text analysis'), + MOLLOM_MODE_CAPTCHA => t('CAPTCHA'), ); $header = array( @@ -29,7 +28,10 @@ function mollom_admin_form_list() { $mollom_form = mollom_form_load($form_id); $rows[] = array( $mollom_form['title'], - $modes[$mollom_form['mode']], + t('!protection-mode (@reject)', array( + '!protection-mode' => $modes[$mollom_form['mode']], + '@reject' => $mollom_form['reject'] ? t('reject') : t('unpublish'), + )), l(t('Configure'), 'admin/config/content/mollom/manage/' . $form_id), l(t('Unprotect'), 'admin/config/content/mollom/unprotect/' . $form_id), ); @@ -153,6 +155,27 @@ function mollom_admin_configure_form($fo ); if (!empty($mollom_form['elements'])) { + // By default, Mollom module rejects all posts that did not successfully + // pass mollom.checkContent. Instead of rejecting posts, site admins + // can optionally configure that posts shall be unpublished instead, so + // they can go through manual moderation. The negated form widget may + // look odd, but users can rather relate to more common terms like + // "unpublish" or "moderation". Furthermore, this setting should stay + // optional and disabled by default, as Mollom should reject bad posts. + $form['mollom']['reject'] = array( + '#type' => 'checkbox', + '#title' => t('Reject bad posts'), + '#default_value' => $mollom_form['reject'], + '#description' => t('Disable to manually moderate all posts.'), + // Only possible for forms supporting moderation of unpublished posts. + '#access' => !empty($mollom_form['reject callback']), + '#states' => array( + 'visible' => array( + ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS), + ), + ), + ); + // If not re-configuring an existing protection, make it the default. if (!isset($mollom_form['mode'])) { $form['mollom']['mode']['#default_value'] = MOLLOM_MODE_ANALYSIS; Index: mollom.api.php =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.api.php,v retrieving revision 1.6 diff -u -p -r1.6 mollom.api.php --- mollom.api.php 12 Sep 2010 22:05:55 -0000 1.6 +++ mollom.api.php 27 Sep 2010 19:47:48 -0000 @@ -150,6 +150,9 @@ * $form_info = array( * // Optional: User permission list to skip Mollom's protection for. * 'bypass access' => array('administer instant messages'), + * // Optional: Function to invoke to unpublish a bad form submission + * // instead of rejecting it. + * 'reject callback' => 'im_mollom_reject', * // Optional: To allow textual analysis of the form values, the form * // elements needs to be registered individually. The keys are the * // field keys in $form_state['values']. Sub-keys are noted using "][" @@ -205,6 +208,16 @@ * Additionally, the "post_id" data property always needs to be mapped to a form * element that holds the entity id. * + * When registering a 'reject callback', then the registered function needs to + * be available when the form is validated, and it is responsible for changing + * the submitted form values in a way that results in an unpublished post ending + * up in a moderation queue: + * @code + * function im_mollom_reject(&$form, &$form_state) { + * $form_state['values']['status'] = 0; + * } + * @endcode + * * @see mollom_node * @see mollom_comment * @see mollom_user @@ -276,6 +289,10 @@ function hook_mollom_form_list() { * current user to determine whether to protect the form with Mollom or do * not validate submitted form values. If the current user has at least one * of the listed permissions, the form will not be protected. + * - reject callback: (optional) A function name to invoke when a form + * submission would normally be rejected. This allows modules to put such + * posts into a moderation queue (i.e., accept but not publish them) by + * altering the $form or $form_state information being passed by reference. * - mail ids: (optional) An array of mail IDs that will be sent as a result * of this form being submitted. When these mails are sent, a 'report to * Mollom' link will be included at the bottom of the mail body. Be sure to Index: mollom.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.install,v retrieving revision 1.28 diff -u -p -r1.28 mollom.install --- mollom.install 25 Sep 2010 01:05:41 -0000 1.28 +++ mollom.install 27 Sep 2010 20:13:18 -0000 @@ -109,6 +109,13 @@ function mollom_schema() { 'not null' => TRUE, 'default' => 0, ), + 'reject' => array( + 'description' => 'Whether to perform a delayed reject (1) or not (0) in upcoming cron runs.', + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), // Server response columns are NULL by default, because any default value // would have an unintended meaning. Also, values are stored in individual // columns, so as to be able to join and filter/sort on these values for @@ -174,6 +181,13 @@ function mollom_schema() { 'not null' => FALSE, 'serialize' => TRUE, ), + 'reject' => array( + 'description' => 'Whether to reject (1) or unpublish (0) bad form submissions.', + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 1, + ), 'enabled_fields' => array( 'description' => 'Form elements to analyze.', 'type' => 'text', @@ -723,3 +737,27 @@ function mollom_update_7007() { } } } + +/** + * Add {mollom_form}.reject and {mollom}.reject columns. + */ +function mollom_update_7008() { + if (!db_field_exists('mollom_form', 'reject')) { + db_add_field('mollom_form', 'reject', array( + 'description' => 'Whether to reject (1) or unpublish (0) bad form submissions.', + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 1, + )); + } + if (!db_field_exists('mollom', 'reject')) { + db_add_field('mollom', 'reject', array( + 'description' => 'Whether to perform a delayed reject (1) or not (0) in upcoming cron runs.', + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + )); + } +} Index: mollom.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.module,v retrieving revision 1.85 diff -u -p -r1.85 mollom.module --- mollom.module 25 Sep 2010 01:05:41 -0000 1.85 +++ mollom.module 27 Sep 2010 21:03:53 -0000 @@ -399,6 +399,27 @@ function mollom_cron() { db_delete('mollom') ->condition('changed', $expired, '<') ->execute(); + + // Delete all bad posts older than two weeks, which have not been published. + $form_list = mollom_form_list(); + $delete = array(); + $query = db_select('mollom', 'm') + ->fields('m') + ->condition('reject', 1) + ->condition('changed', REQUEST_TIME - 86400 * 14, '<') + ->condition(db_or() + ->condition('spam', MOLLOM_ANALYSIS_SPAM) + ->condition('profanity', 0.5, '>=') + ); + foreach ($query->execute() as $data) { + if (isset($form_list[$data->form_id]['entity delete multiple callback'])) { + $function = $form_list[$data->form_id]['entity delete multiple callback']; + $delete[$function][] = $data->did; + } + } + foreach ($delete as $function => $ids) { + $function($ids); + } } /** @@ -437,36 +458,60 @@ function mollom_data_load($entity, $id) * The entity id the data belongs to. * @param $form_id * The form ID the session data belongs to. - * @param $response - * An associative array of session data returned by Mollom. + * @param $data + * An associative array or object of session data returned by Mollom. * * @todo Remove usage of global $mollom variable. */ -function mollom_data_save($entity, $id, $form_id, $response) { - // Nothing to do, if we do not have a valid Mollom response. - if (empty($GLOBALS['mollom']['response']['session_id'])) { - return FALSE; - } - $data = array( - 'entity' => $entity, - 'did' => $id, - 'form_id' => $form_id, - 'session' => $response['session_id'], - 'changed' => time(), - ); - $data += $response; - - // Convert languages into a string. - if (!empty($data['languages'])) { +function mollom_data_save($entity, $id, $form_id, $data) { + $is_array = is_array($data); + if (!$is_array) { + $data = (array) $data; + } + + $data['entity'] = $entity; + $data['did'] = $id; + $data['form_id'] = $form_id; + // @todo Rename {mollom}.session. + if (isset($data['session_id'])) { + $data['session'] = $data['session_id']; + } + $data['changed'] = REQUEST_TIME; + // Convert languages array into a string. + if (isset($data['languages']) && is_array($data['languages'])) { $data['languages'] = implode(' ', $data['languages']); } $update = db_query_range("SELECT 'did' FROM {mollom} WHERE entity = :entity AND did = :did", 0, 1, array(':entity' => $entity, ':did' => $id))->fetchField(); drupal_write_record('mollom', $data, $update ? $update : array()); + + // Convert back into an object, if originally passed $data has been one. + if (!$is_array) { + $data = (object) $data; + } return $data; } /** + * Updates stored Mollom session data to mark a bad post as good. + * + * @param $data + * A Mollom session data object, as loaded from {mollom}. + * + * @todo Find a better function name. + */ +function mollom_data_unreject($data) { + if (isset($data->profanity)) { + $data->profanity = 0; + } + if (isset($data->spam)) { + $data->spam = MOLLOM_ANALYSIS_HAM; + } + $data->reject = 0; + mollom_data_save($data->entity, $data->did, $data->form_id, $data); +} + +/** * Deletes a Mollom session data record from the database. * * @param $entity @@ -627,6 +672,7 @@ function mollom_form_alter(&$form, &$for // Add Mollom form validation handlers. $form['#validate'][] = 'mollom_validate_analysis'; $form['#validate'][] = 'mollom_validate_captcha'; + $form['#validate'][] = 'mollom_validate_post'; // Add a submit handler to remove form state storage. $form['#submit'][] = 'mollom_form_submit'; @@ -755,6 +801,7 @@ function mollom_form_info($form_id, $mod 'module' => $module, 'entity' => NULL, 'mode' => NULL, + 'reject' => TRUE, 'bypass access' => array(), 'elements' => array(), 'mapping' => array(), @@ -1223,6 +1270,13 @@ function mollom_process_mollom($element, } $form_state['mollom'] += $element['#mollom_form']; + // By default, bad form submissions are rejected, unless the form was + // configured to unpublish bad posts. 'reject' may only be FALSE, if there is + // a valid 'reject callback'. Otherwise, it must be TRUE. + if (empty($form_state['mollom']['reject callback']) || !function_exists($form_state['mollom']['reject callback'])) { + $form_state['mollom']['reject'] = TRUE; + } + // Add the Mollom session element. $element['session_id'] = array( '#type' => 'hidden', @@ -1321,10 +1375,19 @@ function mollom_validate_analysis(&$form $form_state['mollom']['response'] = $result; $form['mollom']['session_id']['#value'] = $result['session_id']; + // Prepare watchdog message teaser text. + $teaser = truncate_utf8(strip_tags(isset($data['post_title']) ? $data['post_title'] : isset($data['post_body']) ? $data['post_body'] : '--'), 40); + // Handle the profanity check result. if (isset($result['profanity']) && $result['profanity'] >= 0.5) { - form_set_error('mollom', t('Your submission has triggered the profanity filter and will not be accepted until the inappropriate language is removed.')); - watchdog('mollom', 'Profanity:
@message
Result:
@result
', array('@message' => print_r($data, TRUE), '@result' => print_r($result, TRUE))); + if ($form_state['mollom']['reject']) { + form_set_error('mollom', t('Your submission has triggered the profanity filter and will not be accepted until the inappropriate language is removed.')); + } + _mollom_watchdog(array( + 'Profanity: %teaser' => array('%teaser' => $teaser), + 'Data:
@data
' => array('@data' => $data), + 'Result:
@result
' => array('@result' => $result), + )); } // Handle the spam check result. @@ -1335,7 +1398,6 @@ function mollom_validate_analysis(&$form // the spam check led to a MOLLOM_ANALYSIS_UNSURE result, and the user solved // the CAPTCHA correctly, subsequent spam check results will likely be // MOLLOM_ANALYSIS_HAM (though not guaranteed). - $teaser = truncate_utf8(strip_tags(isset($data['post_title']) ? $data['post_title'] : isset($data['post_body']) ? $data['post_body'] : '--'), 40); if (isset($result['spam'])) { switch ($result['spam']) { case MOLLOM_ANALYSIS_HAM: @@ -1349,7 +1411,9 @@ function mollom_validate_analysis(&$form case MOLLOM_ANALYSIS_SPAM: $form_state['mollom']['require_captcha'] = FALSE; - form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.')); + if ($form_state['mollom']['reject']) { + form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.')); + } _mollom_watchdog(array( 'Spam: %teaser' => array('%teaser' => $teaser), 'Data:
@data
' => array('@data' => $data), @@ -1475,6 +1539,26 @@ function mollom_validate_captcha(&$form, } /** + * Form validation handler to perform post-validation tasks. + * + * Since our individual form validation handlers are not re-run after positive + * validation, any changes applied to form values will not persist across + * multiple form submission attempts and rebuilds. + */ +function mollom_validate_post(&$form, &$form_state) { + // Unpublish a post instead of rejecting it. If 'reject' is not TRUE, then + // the 'reject callback' is responsible for altering $form_state in a way that + // the post ends up unpublished in a moderation queue. Most callbacks will + // only want to set a value in $form_state. Technically, modules do not need + // to implement a 'reject callback' to achieve this, they may simply add a + // custom form validation handler (or use an existing one). + if (!$form_state['mollom']['reject']) { + $function = $form_state['mollom']['reject callback']; + $function($form, $form_state); + } +} + +/** * Form submit handler to flush Mollom session and form information from cache. */ function mollom_form_submit($form, &$form_state) { @@ -1494,6 +1578,10 @@ function mollom_form_submit($form, &$for $data = mollom_form_get_values($form_state['values'], array(), $form_state['mollom']['mapping']); // We only consider non-empty and non-zero values as valid entity ids. if (!empty($data['post_id'])) { + // Mark this post for potential delayed rejection, in case it the form + // has been configured to accept bad posts. + $form_state['mollom']['response']['reject'] = !$form_state['mollom']['reject']; + // Save the Mollom session data. mollom_data_save($form_state['mollom']['entity'], $data['post_id'], $form_state['mollom']['form_id'], $form_state['mollom']['response']); } } @@ -1979,19 +2067,13 @@ function node_mollom_form_list() { 'entity' => 'node', 'bundle' => $type->type, 'delete form' => 'node_delete_confirm', + 'entity delete multiple callback' => 'node_delete_multiple', ); } return $forms; } /** - * Implements hook_node_delete(). - */ -function mollom_node_delete($node) { - mollom_data_delete('node', $node->nid); -} - -/** * Implements hook_mollom_form_info(). */ function node_mollom_form_info($form_id) { @@ -2003,6 +2085,7 @@ function node_mollom_form_info($form_id) // @todo This is incompatible with node access. 'bypass access' => array('bypass node access', 'edit any ' . $type->type . ' content'), 'bundle' => $type->type, + 'reject callback' => 'node_mollom_reject', 'elements' => array(), 'mapping' => array( 'post_id' => 'nid', @@ -2027,6 +2110,39 @@ function node_mollom_form_info($form_id) } /** + * Mollom reject callback. + */ +function node_mollom_reject(&$form, &$form_state) { + $form_state['values']['status'] = 0; +} + +/** + * Implements hook_node_presave(). + */ +function mollom_node_presave($comment) { + // If an existing node is published and we have session data stored for it, + // send 'ham' feedback to Mollom. + if (!empty($node->nid) && $node->status) { + if ($data = mollom_data_load('node', $node->nid)) { + // Report the session to Mollom. + // @todo Not supported yet. + // _mollom_send_feedback($data->session, 'ham'); + + // Update the stored session data and make sure this post is no longer + // deleted in upcoming cron runs. + mollom_data_unreject($data); + } + } +} + +/** + * Implements hook_node_delete(). + */ +function mollom_node_delete($node) { + mollom_data_delete('node', $node->nid); +} + +/** * Implements hook_form_FORMID_alter(). */ function mollom_form_node_multiple_delete_confirm_alter(&$form, &$form_state) { @@ -2069,6 +2185,7 @@ function comment_mollom_form_list() { 'entity' => 'comment', 'bundle' => 'comment_node_' . $type->type, 'delete form' => 'comment_confirm_delete', + 'entity delete multiple callback' => 'comment_delete_multiple', ); } return $forms; @@ -2081,6 +2198,7 @@ function comment_mollom_form_info($form_ $form_info = array( 'mode' => MOLLOM_MODE_ANALYSIS, 'bypass access' => array('administer comments'), + 'reject callback' => 'comment_mollom_reject', 'elements' => array( 'subject' => t('Subject'), // @todo Update for Field API. @@ -2099,6 +2217,32 @@ function comment_mollom_form_info($form_ } /** + * Mollom reject callback. + */ +function comment_mollom_reject(&$form, &$form_state) { + $form_state['values']['status'] = COMMENT_NOT_PUBLISHED; +} + +/** + * Implements hook_comment_presave(). + */ +function mollom_comment_presave($comment) { + // If an existing comment is published and we have session data stored for it, + // send 'ham' feedback to Mollom. + if (!empty($comment->cid) && $comment->status == COMMENT_PUBLISHED) { + if ($data = mollom_data_load('comment', $comment->cid)) { + // Report the session to Mollom. + // @todo Not supported yet. + // _mollom_send_feedback($data->session, 'ham'); + + // Update the stored session data and make sure this post is no longer + // deleted in upcoming cron runs. + mollom_data_unreject($data); + } + } +} + +/** * Implements hook_comment_delete(). */ function mollom_comment_delete($comment) { @@ -2144,11 +2288,10 @@ function user_mollom_form_list() { 'title' => t('User registration form'), 'entity' => 'user', 'delete form' => 'user_cancel_confirm_form', + 'entity delete multiple callback' => 'user_delete_multiple', ); $forms['user_pass'] = array( 'title' => t('User password request form'), - 'entity' => 'user', - 'delete form' => 'user_cancel_confirm_form', ); return $forms; } @@ -2162,6 +2305,7 @@ function user_mollom_form_info($form_id) $form_info = array( 'mode' => MOLLOM_MODE_CAPTCHA, 'bypass access' => array('administer users'), + 'reject callback' => 'user_mollom_reject', 'mapping' => array( 'post_id' => 'uid', 'author_name' => 'name', @@ -2208,6 +2352,13 @@ function mollom_form_user_multiple_cance } /** + * Mollom reject callback. + */ +function user_mollom_reject(&$form, &$form_state) { + $form_state['values']['status'] = 0; +} + +/** * @} End of "name mollom_user". */