Index: mollom.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.module,v retrieving revision 1.94 diff -u -p -r1.94 mollom.module --- mollom.module 3 Nov 2010 14:52:06 -0000 1.94 +++ mollom.module 16 Dec 2010 17:17:59 -0000 @@ -402,7 +402,7 @@ function mollom_cron() { } /** - * Load a Mollom data record from the database. + * Loads a Mollom session data record from the database. * * @param $entity * The entity type to retrieve data for. @@ -410,7 +410,23 @@ function mollom_cron() { * The entity id to retrieve data for. */ function mollom_data_load($entity, $id) { - return db_query_range('SELECT * FROM {mollom} WHERE entity = :entity AND id = :id', 0, 1, array(':entity' => $entity, ':id' => $id))->fetchObject(); + $data = mollom_data_load_multiple($entity, array($id)); + return isset($data[$id]) ? $data[$id] : FALSE; +} + +/** + * Loads multiple Mollom session data records from the database. + * + * @param $entity + * The entity type to retrieve data for. + * @param $ids + * A list of entity ids to retrieve data for. + */ +function mollom_data_load_multiple($entity, array $ids) { + return db_query('SELECT * FROM {mollom} WHERE entity = :entity AND id IN (:ids)', array( + ':entity' => $entity, + ':ids' => $ids, + ))->fetchAllAssoc('id'); } /** @@ -460,23 +476,26 @@ function mollom_data_save($data) { /** * Updates stored Mollom session data to mark a bad post as moderated. * - * @param $entity - * The entity type of the moderated post. - * @param $id - * The entity id of the moderated post. - */ -function mollom_data_moderate($entity, $id) { - $data = mollom_data_load($entity, $id); - // Nothing to do, if no data exists. - if (!$data) { + * @param $data + * An object containing Mollom session data for an entity. When updating + * Mollom data attached to a (potentially statically cached) entity, callers + * should directly pass the $entity->mollom object, so changes are visible in + * the attached data. + * + * @see mollom_entity_load() + * @see mollom_entity_update() + */ +function mollom_data_moderate($data) { + // Nothing to do, if the post was already moderated. This check is done here + // to simplify the calling code. + if (empty($data->moderate)) { return; } - // Report the session to Mollom. _mollom_send_feedback($data->session_id, 'ham'); - // Mark the session data as moderated. $data->moderate = 0; + // Update the stored session data. mollom_data_save($data); } @@ -2107,14 +2126,28 @@ function mollom_mail_add_report_link(&$m } /** + * Implements hook_entity_load(). + */ +function mollom_entity_load($entities, $type) { + // Attach Mollom session data to all loaded entities, if any. + $data = mollom_data_load_multiple($type, array_keys($entities)); + foreach ($entities as $id => $entity) { + if (isset($data[$id])) { + $entities[$id]->mollom = $data[$id]; + } + } +} + +/** * Implements hook_entity_update(). */ function mollom_entity_update($entity, $type) { - // If an existing entity is published and we have session data stored for it, - // mark the data as moderated. - if (!empty($entity->status)) { - $id = reset(entity_extract_ids($type, $entity)); - mollom_data_moderate($type, $id); + if (isset($entity->status)) { + // If the entity has been published and has session data attached, mark it + // as moderated. + if (isset($entity->mollom) && isset($entity->original->status) && $entity->original->status == 0 && $entity->status == 1) { + mollom_data_moderate($entity->mollom); + } } } Index: tests/mollom.test =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/mollom/tests/mollom.test,v retrieving revision 1.77 diff -u -p -r1.77 mollom.test --- tests/mollom.test 16 Dec 2010 14:45:55 -0000 1.77 +++ tests/mollom.test 16 Dec 2010 16:56:05 -0000 @@ -574,6 +574,20 @@ class MollomWebTestCase extends DrupalWe } /** + * Replaces the server list to point to the local fake server implementation. + */ + protected function enableTestServer() { + variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version=')); + } + + /** + * Reverts the server list to use real Mollom servers in following requests. + */ + protected function disableTestServer() { + variable_del('mollom_servers'); + } + + /** * Retrieve submitted XML-RPC values from testing server implementation. * * @param $method @@ -2086,7 +2100,7 @@ class MollomCommentFormTestCase extends } function setUp() { - parent::setUp(array('comment')); + parent::setUp(array('comment', 'mollom_test')); $this->web_user = $this->drupalCreateUser(array('create article content')); $this->node = $this->drupalCreateNode(array('type' => 'article', 'uid' => $this->web_user->uid)); @@ -2219,11 +2233,105 @@ class MollomCommentFormTestCase extends } /** + * Tests retaining bad comments and moderating them. + * + * The Comment module integration requires a dedicated test to account for the + * custom behavior of the "skip comment approval" user permission, which + * affects the publishing status of comments. This test only verifies the + * expected behavior in combination with that permission. Common expectations + * are covered by MollomAnalysisTestCase::testRetain(). + */ + function testRetain() { + $langcode = LANGUAGE_NONE; + + // Enable text analysis for comment form, retaining bad posts. + $this->drupalLogin($this->admin_user); + $this->setProtection('comment_node_article_form', MOLLOM_MODE_ANALYSIS, NULL, array( + 'mollom[discard]' => 0, + )); + $this->drupalLogout(); + + // Publish new comments by default. + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('post comments', 'skip comment approval')); + + // Enable testing server to track all communication. + $this->enableTestServer(); + + // Post a spam comment and verify that is unpublished, despite the user + // permission. + $edit = array( + 'subject' => $this->randomString(), + "comment_body[$langcode][0][value]" => 'spam', + ); + $this->drupalPost('comment/reply/' . $this->node->nid, $edit, t('Save')); + + // @todo D7 bug: Missing $reset argument for comment_load() + comment_load_multiple(). + $comment = entity_load('comment', array(), array('subject' => $edit['subject']), FALSE); + $comment = reset($comment); + $data = $this->assertMollomData('comment', $comment->cid); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertEqual($comment->status, 0, t('Unpublished comment found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); + $this->assertSame('moderate', $data->moderate, 1); + $this->assertTrue(empty($feedback), t('No feedback was sent.')); + + // Log in as administrator to publish the retained comment, and verify that + // stored session data is updated and feedback is sent to Mollom. + $this->drupalLogin($this->admin_user); + $admin_edit = array( + 'status' => 1, + ); + $this->drupalPost('comment/' . $comment->cid . '/edit', $admin_edit, t('Save')); + $comment = entity_load('comment', array($comment->cid), array(), FALSE); + $comment = reset($comment); + $data = $this->assertMollomData('comment', $comment->cid); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertEqual($comment->status, 1, t('Published comment found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); + $this->assertSame('moderate', $data->moderate, 0); + $this->assertSame('session_id', $feedback['session_id'], $data->session_id); + $this->assertSame('feedback', $feedback['feedback'], 'ham'); + $this->drupalLogout(); + + // Post a unsure comment and verify that is published, and no feedback is + // sent. + $edit = array( + 'subject' => $this->randomString(), + "comment_body[$langcode][0][value]" => 'unsure', + ); + $this->drupalPost('comment/reply/' . $this->node->nid, $edit, t('Save')); + $this->postCorrectCaptcha(NULL, array(), t('Save')); + $comment = entity_load('comment', array(), array('subject' => $edit['subject']), FALSE); + $comment = reset($comment); + $data = $this->assertMollomData('comment', $comment->cid); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertEqual($comment->status, 1, t('Published comment found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_UNSURE); + $this->assertSame('moderate', $data->moderate, 0); + $this->assertTrue(empty($feedback), t('No feedback was sent.')); + + // Post a ham comment and verify that is published, and no feedback is sent. + $edit = array( + 'subject' => $this->randomString(), + "comment_body[$langcode][0][value]" => 'ham', + ); + $this->drupalPost('comment/reply/' . $this->node->nid, $edit, t('Save')); + $comment = entity_load('comment', array(), array('subject' => $edit['subject']), FALSE); + $comment = reset($comment); + $data = $this->assertMollomData('comment', $comment->cid); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertEqual($comment->status, 1, t('Published comment found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_HAM); + $this->assertSame('moderate', $data->moderate, 0); + $this->assertTrue(empty($feedback), t('No feedback was sent.')); + } + + /** * Return the number of comments for a node of the given node ID. We * can't use comment_num_all() here, because that is statically cached * and therefore will not work correctly with the SimpleTest browser. */ - private function getCommentCount($nid) { + protected function getCommentCount($nid) { return db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $nid))->fetchField(); } @@ -2838,8 +2946,8 @@ class MollomAnalysisTestCase extends Mol $this->drupalPost('mollom-test/form', $edit, 'Submit'); $mid = $this->assertTestSubmitData(); $data = $this->assertMollomData('mollom_test', $mid); - $record = mollom_test_load($mid); - $this->assertEqual($record['status'], 0, t('Unpublished test post found.')); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 0, t('Unpublished test post found.')); $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); $this->assertSame('profanity', $data->profanity, 1); $this->assertSame('moderate', $data->moderate, 1); @@ -2853,8 +2961,8 @@ class MollomAnalysisTestCase extends Mol $this->drupalPost(NULL, $edit, 'Submit'); $mid = $this->assertTestSubmitData($mid); $data = $this->assertMollomData('mollom_test', $mid); - $record = mollom_test_load($mid); - $this->assertEqual($record['status'], 0, t('Unpublished test post found.')); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 0, t('Unpublished test post found.')); $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); $this->assertSame('profanity', $data->profanity, 1); $this->assertSame('moderate', $data->moderate, 1); @@ -2867,8 +2975,8 @@ class MollomAnalysisTestCase extends Mol $this->drupalPost('mollom-test/form/' . $mid, $edit, 'Submit'); $mid = $this->assertTestSubmitData($mid); $data = $this->assertMollomData('mollom_test', $mid); - $record = mollom_test_load($mid); - $this->assertEqual($record['status'], 1, t('Published test post found.')); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 1, t('Published test post found.')); $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); $this->assertSame('profanity', $data->profanity, 1); $this->assertSame('moderate', $data->moderate, 0); @@ -2892,12 +3000,105 @@ class MollomAnalysisTestCase extends Mol } $mid = $this->assertTestSubmitData(); $data = $this->assertMollomData('mollom_test', $mid); - $record = mollom_test_load($mid); - $this->assertEqual($record['status'], 1, t('Published test post %body found.', array('%body' => $body))); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 1, t('Published test post %body found.', array('%body' => $body))); $this->assertSame('spam', $data->spam, $expected['spam']); $this->assertSame('profanity', $data->profanity, $expected['profanity']); $this->assertSame('moderate', $data->moderate, 0); } + + // Verify that no feedback is sent for new, unpublished SPAM comments. + // @todo Entirely rethink which tests actually need to run against the real + // Mollom backend. MollomResponseTestCase might be the only one. + $this->enableTestServer(); + $edit = array( + 'title' => $this->randomString(), + 'body' => 'spam', + ); + $this->drupalPost('mollom-test/form', $edit, 'Submit'); + $mid = $this->assertTestSubmitData(); + $data = $this->assertMollomData('mollom_test', $mid); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 0, t('Unpublished test post found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); + $this->assertSame('profanity', $data->profanity, 0); + $this->assertSame('moderate', $data->moderate, 1); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertTrue(empty($feedback), t('No feedback sent for the published test post.')); + + // Verify that publishing the post sends feedback. + $this->drupalLogin($this->admin_user); + $edit = array( + 'status' => TRUE, + ); + $this->drupalPost('mollom-test/form/' . $mid, $edit, 'Submit'); + $mid = $this->assertTestSubmitData($mid); + $data = $this->assertMollomData('mollom_test', $mid); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 1, t('Published test post found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); + $this->assertSame('profanity', $data->profanity, 0); + $this->assertSame('moderate', $data->moderate, 0); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertSame('session_id', $feedback['session_id'], $data->session_id); + $this->assertSame('feedback', $feedback['feedback'], 'ham'); + + // Verify that no feedback is sent for new, published HAM comments. + $this->drupalLogout(); + $edit = array( + 'title' => $this->randomString(), + 'body' => 'ham', + ); + $this->drupalPost('mollom-test/form', $edit, 'Submit'); + $mid = $this->assertTestSubmitData(); + $data = $this->assertMollomData('mollom_test', $mid); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 1, t('Published test post found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_HAM); + $this->assertSame('profanity', $data->profanity, 0); + $this->assertSame('moderate', $data->moderate, 0); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertTrue(empty($feedback), t('No feedback sent for the published test post.')); + + // Change the form protection discard bad posts. + $this->drupalLogin($this->admin_user); + $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( + 'mollom[discard]' => 1, + )); + $this->drupalLogout(); + + // Verify that no feedback is sent for new, published HAM comments. + $edit = array( + 'title' => $this->randomString(), + 'body' => 'ham', + ); + $this->drupalPost('mollom-test/form', $edit, 'Submit'); + $mid = $this->assertTestSubmitData(); + $data = $this->assertMollomData('mollom_test', $mid); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 1, t('Published test post found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_HAM); + $this->assertSame('profanity', $data->profanity, 0); + $this->assertSame('moderate', $data->moderate, 0); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertTrue(empty($feedback), t('No feedback sent for the published test post.')); + + // Verify that no feedback is sent for new, published UNSURE comments. + $edit = array( + 'title' => $this->randomString(), + 'body' => 'unsure', + ); + $this->drupalPost('mollom-test/form', $edit, 'Submit'); + $this->postCorrectCaptcha(NULL, array(), 'Submit'); + $mid = $this->assertTestSubmitData(); + $data = $this->assertMollomData('mollom_test', $mid); + $record = mollom_test_load($mid, TRUE); + $this->assertEqual($record->status, 1, t('Published test post found.')); + $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_UNSURE); + $this->assertSame('profanity', $data->profanity, 0); + $this->assertSame('moderate', $data->moderate, 0); + $feedback = $this->getServerRecord('mollom.sendFeedback'); + $this->assertTrue(empty($feedback), t('No feedback sent for the published test post.')); } /** Index: tests/mollom_test.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/mollom/tests/mollom_test.module,v retrieving revision 1.15 diff -u -p -r1.15 mollom_test.module --- tests/mollom_test.module 9 Oct 2010 23:13:01 -0000 1.15 +++ tests/mollom_test.module 16 Dec 2010 17:22:03 -0000 @@ -264,7 +264,11 @@ function mollom_test_form($form, &$form_ // be incomplete. Therefore, the default values always have to be overloaded. $form_state += array('storage' => array()); if (isset($mid) && ($record = mollom_test_load($mid))) { - $form_state['storage'] = $record; + $form_state['mollom_test'] = $record; + $form_state['storage'] = (array) $record; + } + else { + $form_state['mollom_test'] = new stdClass; } $form_state['storage'] += array( 'mid' => $mid, @@ -367,21 +371,19 @@ function mollom_test_form_submit($form, $form_state['values']['field'][] = $form_state['values']['field']['new']; unset($form_state['values']['field']['new']); - // Allow modules to alter the record before saving. This is done for code - // consistency only. - // @see mollom_mollom_test_presave() form_state_values_clean($form_state); - module_invoke_all('mollom_test_presave', $form_state['values']); - // Store submission. - $update = !empty($form_state['values']['mid']) ? 'mid' : array(); - drupal_write_record('mollom_test', $form_state['values'], $update); + $record = $form_state['mollom_test']; + entity_form_submit_build_entity('mollom_test', $record, $form, $form_state); + + mollom_test_save($record); + $form_state['values']['mid'] = $record->mid; // Redirect to stored entry. - $form_state['redirect'] = 'mollom-test/form/' . $form_state['values']['mid']; + $form_state['redirect'] = 'mollom-test/form/' . $record->mid; drupal_set_message('Successful form submission.'); - drupal_set_message('
' . var_export($form_state['values'], TRUE) . ''); + drupal_set_message('
' . var_export($record, TRUE) . ''); } /** @@ -402,21 +404,48 @@ function mollom_test_mollom_form_moderat } /** - * Implements hook_mollom_test_presave() on behalf of mollom.module. + * Implements hook_entity_info(). */ -function mollom_mollom_test_presave($record) { - // If an existing record is published and we have session data stored for it, - // mark the data as moderated. - if (!empty($record['mid']) && $record['status']) { - mollom_data_moderate('mollom_test', $record['mid']); - } +function mollom_test_entity_info() { + $entities = array( + 'mollom_test' => array( + 'label' => 'Mollom test', + 'base table' => 'mollom_test', + 'entity keys' => array( + 'id' => 'mid', + 'label' => 'title', + ), + ), + ); + return $entities; } /** * Loads a {mollom_test} data record by id. */ -function mollom_test_load($mid) { - return db_query('SELECT * FROM {mollom_test} WHERE mid = :mid', array(':mid' => $mid))->fetchAssoc(); +function mollom_test_load($mid, $reset = FALSE) { + $records = entity_load('mollom_test', array($mid), array(), $reset); + return isset($records[$mid]) ? $records[$mid] : FALSE; +} + +/** + * Saves a {mollom_test} data record. + */ +function mollom_test_save($record) { + // Load the stored entity, if any. + if (!empty($record->mid) && !isset($record->original)) { + $record->original = entity_load_unchanged('mollom_test', $record->mid); + } + + module_invoke_all('mollom_test_presave', $record); + + // Insert or update the record. + $update = !empty($record->mid) ? 'mid' : array(); + drupal_write_record('mollom_test', $record, $update); + + $op = ($update ? 'update' : 'insert'); + module_invoke_all('mollom_test_' . $op, $record); + module_invoke_all('entity_' . $op, $record, 'mollom_test'); } /**