From 89fda7a86dbf98edff2ec7f8caf04a6abd029774 Mon Sep 17 00:00:00 2001 From: sun Date: Fri, 29 Mar 2013 17:19:09 +0100 Subject: [PATCH] - #1938768 by sun: Fixed text analysis is incompatible with enabled page cache/reverse proxy. --- mollom.admin.inc | 3 + mollom.module | 284 ++++++++++++++++++++++------------------ tests/mollom.test | 186 ++++++++++++++++++++------ tests/mollom_test.module | 27 ++++ tests/mollom_test_server.module | 3 + 5 files changed, 335 insertions(+), 168 deletions(-) diff --git a/mollom.admin.inc b/mollom.admin.inc index 5d9b121..9804360 100644 --- a/mollom.admin.inc +++ b/mollom.admin.inc @@ -187,6 +187,9 @@ function mollom_admin_configure_form($form, &$form_state, $mollom_form = NULL) { '#options' => $modes, '#default_value' => isset($mollom_form['mode']) ? $mollom_form['mode'] : key($modes), ); + $form['mollom']['mode'][MOLLOM_MODE_CAPTCHA] = array( + '#description' => t('Only choose CAPTCHA if text analysis is not suitable. Page caching is disabled on all pages containing CAPTCHA-only protected forms.'), + ); $all_permissions = array(); foreach (module_implements('permission') as $module) { diff --git a/mollom.module b/mollom.module index 0113c5f..41af870 100644 --- a/mollom.module +++ b/mollom.module @@ -686,8 +686,11 @@ function mollom_data_report_multiple($entity, array $ids, $feedback) { /** * Implements hook_form_alter(). * - * This function intercepts all forms in Drupal and Mollom-enables them if - * necessary. + * Protects all configured forms with Mollom. + * + * @see mollom_element_info() + * @see mollom_process_mollom() + * @see mollom_pre_render_mollom() */ function mollom_form_alter(&$form, &$form_state, $form_id) { // Skip installation and update forms. @@ -726,9 +729,6 @@ function mollom_form_alter(&$form, &$form_state, $form_id) { '#weight' => (isset($form['actions']['#weight']) ? $form['actions']['#weight'] - 1 : 99), '#tree' => TRUE, ); - // Enable caching of this form; required for our form validation and - // submit handlers. - $form_state['cache'] = TRUE; // Add Mollom form validation handlers. // Form-level validation handlers are required, since we need access to @@ -1464,7 +1464,9 @@ function _mollom_status($reset = FALSE) { * Outputs a warning message about enabled testing mode (once). */ function _mollom_testing_mode_warning() { - $warned = &drupal_static(__FUNCTION__); + // drupal_set_message() starts a session and disables page caching, which + // breaks cache-related tests. Thus, tests set the verbose variable to TRUE. + $warned = &drupal_static(__FUNCTION__, variable_get('mollom_testing_mode_omit_warning', NULL)); if (isset($warned)) { return; } @@ -1495,30 +1497,6 @@ function _mollom_fallback() { } /** - * @defgroup mollom_form_api Mollom Form API workarounds - * @{ - * Various helper functions to work around bugs in Form API. - * - * Mollom's integration with Form API is quite simple: - * - If a form is protected by Mollom, we setup initial information - * about the session and the form in $form_state['mollom']. - * - We mainly work in and after form validation. Textual analysis validates all - * values in the form as a form validation handler. If this validation fails, - * we alter the form (during validation) to add a CAPTCHA. If the CAPTCHA - * response is invalid, we still alter the form during validation to display a - * new CAPTCHA, but without the previously entered value. - * - Form API keeps our $form_state information, because we short-cut form - * rebuilding by issueing a form validation error until the submitted form - * values are valid. - * - In short, very roughly: - * - Form construction: Nothing. - * - Form processing: Nothing. - * - Form validation: Perform validation and alterations based on validation. - * - * @see mollom_form_alter() - */ - -/** * Implements hook_element_info(). */ function mollom_element_info() { @@ -1527,6 +1505,7 @@ function mollom_element_info() { '#process' => array( 'mollom_process_mollom', ), + '#pre_render' => array('mollom_pre_render_mollom'), ), ); } @@ -1544,28 +1523,61 @@ function mollom_theme() { } /** - * Form element #process callback for the 'mollom' element. + * #process callback for #type 'mollom'. + * + * Mollom supports two fundamentally different protection modes: + * - For text analysis, the state of a post is essentially tracked by the Mollom + * API/backend: + * - Every form submission attempt (re-)sends the post data to Mollom, and the + * API ensures to return the correct spamClassification each time. + * - The client-side logic fully relies on the returned spamClassification + * value to trigger the corresponding actions and does not track the state + * of the form submission attempt locally. + * - For example, when Mollom is "unsure" and the user solved the CAPTCHA, + * then subsequent Content API responses will return "ham" instead of + * "unsure", so the user is not asked to solve more than one CAPTCHA. + * - For CAPTCHA-only, the solution state of a CAPTCHA has to be tracked locally: + * - Unlike text analysis, a CAPTCHA can only be solved or not. Additionally, + * a CAPTCHA cannot be solved more than once. The Mollom API only returns + * (once) whether a CAPTCHA has been solved correctly. A previous state + * cannot be queried from Mollom. + * - State-tracking would not be necessary, if there could not be other form + * validation errors preventing the form from submitting directly, as well as + * "Preview" buttons that may rebuild the entire form from scratch (if there + * are no validation errors). + * - To track state, the Form API cache is enabled, which allows to store and + * retrieve the entire $form_state of a previous submission (attempt). + * - Furthermore, page caching is force-disabled, so as to ensure that cached + * form data is not re-used by different users/visitors. + * - In combination with the Form API cache, this is essentially equal to + * force-starting a session for all users that try to submit a CAPTCHA-only + * protected form. However, a session would persist across other pages. * - * The 'mollom' form element is stateful. The Mollom session ID that is exchanged - * between Drupal, the Mollom back-end, and the user allows us to keep track of - * the form validation state. + * @see mollom_form_alter() + * @see mollom_element_info() + * @see mollom_pre_render_mollom() */ function mollom_process_mollom($element, &$form_state, $complete_form) { // Setup initial Mollom session and form information. $form_state += array('mollom' => array()); $form_state['mollom'] += array( + // Only TRUE if the form is protected by text analysis. 'require_analysis' => $element['#mollom_form']['mode'] == MOLLOM_MODE_ANALYSIS, + // Becomes TRUE whenever a CAPTCHA needs to be solved. 'require_captcha' => $element['#mollom_form']['mode'] == MOLLOM_MODE_CAPTCHA, + // Becomes TRUE when the CAPTCHA has been solved. + // Only applies to CAPTCHA-only protected forms. Not necessarily TRUE for + // text analysis, even if a CAPTCHA has been solved previously. 'passed_captcha' => FALSE, + // The type of CAPTCHA to show; 'image' or 'audio'. 'captcha_type' => 'image', + // Becomes TRUE if the form is protected by text analysis and the submitted + // entity should be unpublished. 'require_moderation' => FALSE, + // Internally used bag for last Mollom API responses. 'response' => array( ), ); - // Instantiate a new Mollom class, unless there is not a cached one. - if (!isset($form_state['mollom']['class'])) { - $form_state['mollom']['class'] = mollom(); - } // Add remaining information about the registered form. $form_state['mollom'] += $element['#mollom_form']; @@ -1594,11 +1606,16 @@ function mollom_process_mollom($element, &$form_state, $complete_form) { ); $element['contentId'] = array( '#type' => 'hidden', - '#default_value' => isset($form_state['mollom']['response']['content']['id']) ? $form_state['mollom']['response']['content']['id'] : '', + // There is no default value; Form API will always use the value that was + // submitted last (including rebuild scenarios). '#attributes' => array('class' => 'mollom-content-id'), ); $element['captchaId'] = array( '#type' => 'hidden', + // For CAPTCHA-only protected forms, the most recent CAPTCHA ID is tracked + // in $form_state. A new CAPTCHA ID may be injected by mollom_get_captcha(). + // Furthermore, the CAPTCHA type can be switched between image and audio, + // and for each switch, there is a new CAPTCHA ID. '#default_value' => isset($form_state['mollom']['response']['captcha']['id']) ? $form_state['mollom']['response']['captcha']['id'] : '', '#attributes' => array('class' => 'mollom-captcha-id'), ); @@ -1616,13 +1633,20 @@ function mollom_process_mollom($element, &$form_state, $complete_form) { ); $element['spamScore'] = $data_spec; $element['spamClassification'] = $data_spec; - $element['solved'] = $data_spec; $element['qualityScore'] = $data_spec; $element['profanityScore'] = $data_spec; - $element['reason'] = $data_spec; $element['languages'] = $data_spec; + $element['reason'] = $data_spec; + $element['solved'] = $data_spec; // Add the CAPTCHA element. + // - Cannot be #required, since that would cause _form_validate() to output a + // validation error in situations in which the CAPTCHA is not required. + // - #access can also not start with FALSE, since the form structure may be + // cached, and Form API ignores all user input for inaccessible elements. + // Since this element needs to be hidden by the #pre_render callback, but that + // callback does not have access to $form_state, the 'passed_captcha' state is + // assigned as Boolean #solved = TRUE element property when solved correctly. $element['captcha'] = array( '#type' => 'textfield', '#title' => t('Word verification'), @@ -1635,14 +1659,21 @@ function mollom_process_mollom($element, &$form_state, $complete_form) { if (!variable_get('mollom_testing_mode', 0)) { $element['captcha']['#attributes']['autocomplete'] = 'off'; } - - // Request and inject an initial CAPTCHA. - if ($form_state['mollom']['require_captcha'] && !$form_state['mollom']['passed_captcha'] && empty($form_state['mollom']['response']['captcha']['id'])) { - mollom_form_add_captcha($element, $form_state); - } - // If no CAPTCHA is required or it was solved already, hide the element. - elseif (!$form_state['mollom']['require_captcha'] || $form_state['mollom']['passed_captcha']) { - $element['captcha']['#access'] = FALSE; + // For CAPTCHA-only protected forms, retrieve and show an initial CAPTCHA. + if (!$form_state['mollom']['require_analysis'] && $form_state['mollom']['require_captcha']) { + // Unless the CAPTCHA was solved, display the CAPTCHA. + if (!$form_state['mollom']['passed_captcha']) { + // Enable Form API caching, in order to track the state of the CAPTCHA. + $form_state['cache'] = TRUE; + // mollom_form_add_captcha() adds the CAPTCHA and disables page caching. + mollom_form_add_captcha($element, $form_state); + } + // Otherwise, resemble mollom_validate_captcha(). This case is only reached + // in case the form 1) is not cached, 2) fully validated, 3) was submitted, + // and 4) is getting rebuilt; e.g., "Preview" on comment and node forms. + else { + $element['captcha']['#solved'] = TRUE; + } } // Add a spambot trap. Purposively use 'homepage' as field name. @@ -1685,9 +1716,6 @@ function mollom_process_mollom($element, &$form_state, $complete_form) { * The current state of the form. Passed by reference. */ function mollom_form_add_captcha(&$element, &$form_state) { - // UX: Empty the CAPTCHA field value, as the user has to re-enter a new one. - $element['captcha']['#value'] = ''; - // Prevent the page cache from storing a form containing a CAPTCHA element. drupal_page_is_cacheable(FALSE); @@ -1695,9 +1723,9 @@ function mollom_form_add_captcha(&$element, &$form_state) { // If we get a response, add the image CAPTCHA to the form element. if (!empty($captcha)) { $element['captcha']['#access'] = TRUE; - $element['captcha']['#required'] = TRUE; $element['captcha']['#field_prefix'] = $captcha; - // Assign the session ID returned by Mollom. + + // Ensure that the latest CAPTCHA ID is output as value. $element['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['id']; $form_state['values']['mollom']['captchaId'] = $form_state['mollom']['response']['captcha']['id']; } @@ -1710,18 +1738,9 @@ function mollom_form_add_captcha(&$element, &$form_state) { } /** - * Form validation handler to perform textual analysis of submitted form values. - * - * Validation needs to re-run in case of a form validation error (elsewhere in - * the form). In case Mollom's textual analysis returns no definite result, we - * must trigger a CAPTCHA, but text analysis is always performed, even if the - * CAPTCHA was solved correctly. + * Form validation handler to perform textual analysis on submitted form values. */ function mollom_validate_analysis(&$form, &$form_state) { - // Text analysis may only ever be skipped, if we do not require it in the - // first place. With regard to that, $form_state['mollom']['require_analysis'] - // is only set once during initialization of $form_state['mollom'] in - // mollom_process_form() and must not be updated elsewhere. if (!$form_state['mollom']['require_analysis']) { return; } @@ -1737,8 +1756,8 @@ function mollom_validate_analysis(&$form, &$form_state) { if (isset($data['postId'])) { unset($data['postId']); } - if (isset($form_state['mollom']['response']['content']['id'])) { - $data['id'] = $form_state['mollom']['response']['content']['id']; + if (!empty($form_state['values']['mollom']['contentId'])) { + $data['id'] = $form_state['values']['mollom']['contentId']; } $data['checks'] = $form_state['mollom']['checks']; $data['strictness'] = $form_state['mollom']['strictness']; @@ -1748,7 +1767,7 @@ function mollom_validate_analysis(&$form, &$form_state) { if (in_array('spam', $data['checks']) && $form_state['mollom']['unsure'] == 'binary') { $data['unsure'] = 0; } - $result = $form_state['mollom']['class']->checkContent($data); + $result = mollom()->checkContent($data); // Use all available data properties for log messages below. $data += $all_data; @@ -1760,13 +1779,15 @@ function mollom_validate_analysis(&$form, &$form_state) { // Store the response returned by Mollom. $form_state['mollom']['response']['content'] = $result; - // Set form element values accordingly. Do not overwrite the entity ID with - // the contentId, nor a possibly existing captchaId. + // Set form values accordingly. Do not overwrite the entity ID. + // @todo Rename 'id' to 'entity_id'. $result['contentId'] = $result['id']; unset($result['id']); $form_state['values']['mollom'] = array_merge($form_state['values']['mollom'], $result); - // #value has to be set manually to output it in case of a validation error, - // since this is not a element-level but a form-level validation handler. + + // Ensure the latest content ID is output as value. + // form_set_value() is effectless, as this is not a element-level but a + // form-level validation handler. $form['mollom']['contentId']['#value'] = $result['contentId']; // Prepare watchdog message teaser text. @@ -1792,14 +1813,12 @@ function mollom_validate_analysis(&$form, &$form_state) { )); } - // Handle the final spam classification result. - // The Mollom backend is remembering results of previous mollom.checkContent - // invocations for a single user/post session. When content is re-checked - // during form validation, the result may change according to the values that - // have been submitted (which e.g. can change during previews). Only in case - // the spam check led to a 'unsure' result, and the user solved the CAPTCHA - // correctly, subsequent spam check results will likely be 'ham' (though not - // guaranteed). + // Handle the spam check result. + // The Mollom API takes over state tracking for each content ID/session. The + // spamClassification will usually turn into 'ham' after solving a CAPTCHA. + // It may also change to 'spam', if the user replaced the values with very + // spammy content. In any case, we always do what we are told to do. + // Note: The returned spamScore may diverge from the spamClassification. if (isset($result['spamClassification'])) { switch ($result['spamClassification']) { case 'ham': @@ -1825,25 +1844,16 @@ function mollom_validate_analysis(&$form, &$form_state) { break; case 'unsure': - mollom_log(array( - 'message' => 'Unsure: %teaser', - 'arguments' => array('%teaser' => $teaser), - ), WATCHDOG_INFO); - if ($form_state['mollom']['unsure'] == 'moderate') { $form_state['mollom']['require_moderation'] = TRUE; } else { - // Only throw a validation error and retrieve a CAPTCHA, if we check - // this post for the first time. Otherwise, mollom_validate_captcha() - // issued the CAPTCHA and needs to validate it prior to throwing any - // errors. - if (!$form_state['mollom']['require_captcha']) { - $form_state['mollom']['require_captcha'] = TRUE; - form_set_error('mollom][captcha', t('To complete this form, please complete the word verification below.')); - mollom_form_add_captcha($form['mollom'], $form_state); - } + $form_state['mollom']['require_captcha'] = TRUE; } + mollom_log(array( + 'message' => 'Unsure: %teaser', + 'arguments' => array('%teaser' => $teaser), + ), WATCHDOG_INFO); break; case MOLLOM_ANALYSIS_UNKNOWN: @@ -1869,28 +1879,26 @@ function mollom_validate_analysis(&$form, &$form_state) { * text analysis result is "unsure". */ function mollom_validate_captcha(&$form, &$form_state) { - // CAPTCHA validation may only be skipped, if we do not require it in the - // first place, or if the user already solved a CAPTCHA correctly. We need to - // validate, if $form_state['mollom']['require_captcha'] is TRUE, which is - // either set during initialization of $form_state['mollom'] in - // mollom_process_form(), or after performing text analysis. The second - // return condition, $form_state['mollom']['passed_captcha'], may only ever be - // set by this validation handler and must not be changed elsewhere. - if (!$form_state['mollom']['require_captcha'] || $form_state['mollom']['passed_captcha']) { - $form['mollom']['captcha']['#access'] = FALSE; + if (!$form_state['mollom']['require_captcha']) { + return; + } + + // If there is no CAPTCHA ID yet, retrieve one and throw an error. + if (empty($form_state['values']['mollom']['captchaId'])) { + mollom_form_add_captcha($form['mollom'], $form_state); + form_error($form['mollom']['captcha'], t('To complete this form, please complete the word verification below.')); return; } - // If the form is protected via text analysis and the result was "unsure", - // then this validation handler also runs when the CAPTCHA is initially - // requested by mollom_validate_analysis(), is about to be rendered, and - // before the user had a chance to solve it. In that case, there is no value - // that could be validated yet; i.e., the CAPTCHA value is empty. However, in - // all other cases, an empty value must be validated and sent to Mollom. The - // initial case has a special condition: mollom_validate_analysis() has set - // a form error on the CAPTCHA element asking the user to solve it in order to - // submit the form. - if ($form_state['values']['mollom']['captcha'] === '' && form_get_error($form['mollom']['captcha'])) { + // $form_state['mollom']['passed_captcha'] may only ever be set by this + // validation handler and must not be changed elsewhere. + // This only becomes TRUE for CAPTCHA-only protected forms, for which the + // CAPTCHA state is locally tracked in $form_state. For text analysis, the + // primary 'require_captcha' condition will not be TRUE unless needed in the + // first place. + if ($form_state['mollom']['passed_captcha']) { + $form['mollom']['captcha']['#access'] = FALSE; + $form['mollom']['captcha']['#solved'] = TRUE; return; } @@ -1904,7 +1912,7 @@ function mollom_validate_captcha(&$form, &$form_state) { return; } $data = array( - 'id' => $form_state['mollom']['response']['captcha']['id'], + 'id' => $form_state['values']['mollom']['captchaId'], 'solution' => $form_state['values']['mollom']['captcha'], 'authorIp' => $all_data['authorIp'], ); @@ -1914,7 +1922,7 @@ function mollom_validate_captcha(&$form, &$form_state) { if (isset($all_data['honeypot'])) { $data['honeypot'] = $all_data['honeypot']; } - $result = $form_state['mollom']['class']->checkCaptcha($data); + $result = mollom()->checkCaptcha($data); // Use all available data properties for log messages below. $data += $all_data; @@ -1925,18 +1933,21 @@ function mollom_validate_captcha(&$form, &$form_state) { // Store the response for #submit handlers. $form_state['mollom']['response']['captcha'] = $result; - // Set form element values accordingly. Do not overwrite the entity ID with - // the contentId, nor a possibly existing captchaId. + // Set form values accordingly. Do not overwrite the entity ID. + // @todo Rename 'id' to 'entity_id'. $result['captchaId'] = $result['id']; unset($result['id']); $form_state['values']['mollom'] = array_merge($form_state['values']['mollom'], $result); - // #value has to be set manually to output it in case of a validation error, - // since this is not a element-level but a form-level validation handler. + + // Ensure the latest CAPTCHA ID is output as value. + // form_set_value() is effectless, as this is not a element-level but a + // form-level validation handler. $form['mollom']['captchaId']['#value'] = $result['captchaId']; if (!empty($result['solved'])) { $form_state['mollom']['passed_captcha'] = TRUE; $form['mollom']['captcha']['#access'] = FALSE; + $form['mollom']['captcha']['#solved'] = TRUE; mollom_log(array( 'message' => 'Correct CAPTCHA', @@ -1953,6 +1964,33 @@ function mollom_validate_captcha(&$form, &$form_state) { } /** + * #pre_render callback for #type 'mollom'. + * + * - Hides the CAPTCHA if it is not required or the solution was correct. + * - Marks the CAPTCHA as required. + */ +function mollom_pre_render_mollom($element) { + // If there is no CAPTCHA ID, then there is no CAPTCHA that can be displayed. + // If a CAPTCHA was solved, then the widget makes no sense either. + if (empty($element['captchaId']['#value']) || !empty($element['captcha']['#solved'])) { + $element['captcha']['#access'] = FALSE; + } + else { + // The form element cannot be marked as #required, since _form_validate() + // would throw an element validation error on an empty value otherwise, + // before the form-level validation handler is executed. + // #access cannot default to FALSE, since the $form may be cached, and + // Form API ignores user input for all elements that are not accessible. + $element['captcha']['#required'] = TRUE; + } + + // UX: Empty the CAPTCHA field value, as the user has to re-enter a new one. + $element['captcha']['#value'] = ''; + + return $element; +} + +/** * Form validation handler to perform post-validation tasks. */ function mollom_validate_post(&$form, &$form_state) { @@ -2025,10 +2063,6 @@ function mollom_form_submit($form, &$form_state) { } /** - * @} End of "defgroup mollom_form_api". - */ - -/** * Instantiates a new Mollom client. * * @param $class @@ -2457,10 +2491,7 @@ function mollom_get_captcha(&$form_state) { if (!empty($form_state['mollom']['response']['content']['id'])) { $data['contentId'] = $form_state['mollom']['response']['content']['id']; } - // @todo $form_state may not contain a Mollom class when being called from - // mollom_captcha_js() until that gets converted to #ajax framework. - $mollom = (isset($form_state['mollom']['class']) ? $form_state['mollom']['class'] : mollom()); - $result = $mollom->createCaptcha($data); + $result = mollom()->createCaptcha($data); // Add a log message to prevent the request log from appearing without a // message on CAPTCHA-only protected forms. @@ -2470,6 +2501,7 @@ function mollom_get_captcha(&$form_state) { if (is_array($result) && isset($result['url'])) { $url = $result['url']; + $form_state['mollom']['response'][$key] = $url; $form_state['mollom']['response']['captcha']['id'] = $result['id']; } else { diff --git a/tests/mollom.test b/tests/mollom.test index fa77fda..4240a55 100644 --- a/tests/mollom.test +++ b/tests/mollom.test @@ -125,6 +125,9 @@ class MollomWebTestCase extends DrupalWebTestCase { // automatically create testing API keys. variable_set('mollom_testing_create_keys', $this->createKeys); + // Disable testing mode warnings. + variable_set('mollom_testing_mode_omit_warning', TRUE); + // D7's new default theme Bartik is bogus in various locations, which leads // to failing tests. // @todo Remove this override. @@ -384,6 +387,34 @@ class MollomWebTestCase extends DrupalWebTestCase { } /** + * Saves a mollom_form entity to protect a given form with Mollom. + * + * @param string $form_id + * The form id to protect. + * @param int $mode + * The protection mode. Defaults to MOLLOM_MODE_ANALYSIS. + * @param array $values + * (optional) An associative array of properties to additionally set on the + * mollom_form entity. + * + * @return int + * The save status, as returned by mollom_form_save(). + */ + protected function setProtection($form_id, $mode = MOLLOM_MODE_ANALYSIS, $values = array()) { + if (!$mollom_form = mollom_form_load($form_id)) { + $mollom_form = mollom_form_new($form_id); + } + $mollom_form['mode'] = $mode; + if ($values) { + foreach ($values as $property => $value) { + $mollom_form[$property] = $value; + } + } + $status = mollom_form_save($mollom_form); + return $status; + } + + /** * Configure Mollom protection for a given form. * * @param $form_id @@ -398,7 +429,7 @@ class MollomWebTestCase extends DrupalWebTestCase { * (optional) An array of POST data to pass through to drupalPost() when * configuring the form's protection. */ - protected function setProtection($form_id, $mode = MOLLOM_MODE_ANALYSIS, $fields = NULL, $edit = array()) { + protected function setProtectionUI($form_id, $mode = MOLLOM_MODE_ANALYSIS, $fields = NULL, $edit = array()) { // Always start from overview page, also to make debugging easier. $this->drupalGet('admin/config/content/mollom'); // Determine whether the form is already protected. @@ -911,6 +942,17 @@ class MollomWebTestCase extends DrupalWebTestCase { )); $this->assertNotEqual($first, $second, $message); } + + /** + * Enables aggressive page caching options to resemble reverse-proxies. + */ + protected function enablePageCache() { + variable_set('cache', 1); + variable_set('page_cache_maximum_age', 180); + // A minimum cache lifetime causes cache_clear_all() to start a session. + //variable_set('cache_lifetime', 60); + } + } /** @@ -941,6 +983,9 @@ class MollomTestingModeTestCase extends MollomWebTestCase { function setUp() { parent::setUp(array('mollom', 'mollom_test')); + // Enable testing mode warnings. + variable_del('mollom_testing_mode_omit_warning'); + $this->admin_user = $this->drupalCreateUser(array( 'access administration pages', 'administer mollom', @@ -954,7 +999,7 @@ class MollomTestingModeTestCase extends MollomWebTestCase { $this->drupalLogin($this->admin_user); // Protect mollom_test_form. - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS); + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS); // Setup production API keys. They must be retained. $publicKey = 'the-invalid-mollom-api-key-value'; @@ -1683,7 +1728,7 @@ class MollomBypassAccessTestCase extends MollomWebTestCase { */ function testBypassAccess() { $this->drupalLogin($this->admin_user); - $this->setProtection('comment_node_article_form'); + $this->setProtectionUI('comment_node_article_form'); $this->drupalLogout(); $node = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => 'node body'))), 'type' => 'article')); @@ -1771,7 +1816,7 @@ class MollomFallbackModeTestCase extends MollomWebTestCase { function testBlock() { // Enable Mollom for the request password form. $this->drupalLogin($this->admin_user); - $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('user_pass', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Set the fallback strategy to 'blocking mode'. @@ -1799,7 +1844,7 @@ class MollomFallbackModeTestCase extends MollomWebTestCase { function testAccept() { // Enable Mollom for the request password form. $this->drupalLogin($this->admin_user); - $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('user_pass', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Set the fallback strategy to 'accept mode'. @@ -1819,7 +1864,7 @@ class MollomFallbackModeTestCase extends MollomWebTestCase { */ function testFailover() { $this->drupalLogin($this->admin_user); - $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('user_pass', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Set the fallback strategy to 'blocking mode', so that if the failover @@ -2089,7 +2134,7 @@ class MollomProfanityTestCase extends MollomWebTestCase { 'mollom[checks][spam]' => TRUE, 'mollom[checks][profanity]' => FALSE, ); - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, $edit_config); + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, $edit_config); $this->drupalLogout(); // Assert that the profanity filter is disabled. @@ -2107,7 +2152,7 @@ class MollomProfanityTestCase extends MollomWebTestCase { 'mollom[checks][spam]' => FALSE, 'mollom[checks][profanity]' => TRUE, ); - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, $edit_config); + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, $edit_config); $this->drupalLogout(); // Verify that the profanity filter now blocks this content. @@ -2133,7 +2178,7 @@ class MollomProfanityTestCase extends MollomWebTestCase { 'mollom[checks][profanity]' => TRUE, 'mollom[checks][spam]' => TRUE, ); - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, $edit_config); + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, $edit_config); $this->drupalLogout(); // Sequence: Post profanity (ham), remove profanity (still ham), and expect @@ -2455,7 +2500,7 @@ class MollomFormConfigurationTestCase extends MollomWebTestCase { function testFormAlter() { // Enable CAPTCHA-only protection for request user password form. $this->drupalLogin($this->admin_user); - $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('user_pass', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Verify regular form protection. @@ -2489,7 +2534,7 @@ class MollomUserFormsTestCase extends MollomWebTestCase { // Verify that the protection mode defaults to CAPTCHA. $this->drupalGet('admin/config/content/mollom/add/user_pass'); $this->assertFieldByName('mollom[mode]', MOLLOM_MODE_CAPTCHA); - $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('user_pass', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Create a new user. @@ -2517,7 +2562,7 @@ class MollomUserFormsTestCase extends MollomWebTestCase { // Verify that the protection mode defaults to CAPTCHA. $this->drupalGet('admin/config/content/mollom/add/user_register_form'); $this->assertFieldByName('mollom[mode]', MOLLOM_MODE_CAPTCHA); - $this->setProtection('user_register_form', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('user_register_form', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Retrieve initial count of registered users. @@ -2576,7 +2621,7 @@ class MollomUserFormsTestCase extends MollomWebTestCase { variable_set('user_register', USER_REGISTER_VISITORS); $this->drupalLogin($this->admin_user); - $this->setProtection('user_register_form', MOLLOM_MODE_ANALYSIS); + $this->setProtectionUI('user_register_form', MOLLOM_MODE_ANALYSIS); $this->drupalLogout(); // Retrieve initial count of registered users. @@ -2627,7 +2672,7 @@ class MollomUserFormsTestCase extends MollomWebTestCase { variable_set('user_register', USER_REGISTER_VISITORS); $this->drupalLogin($this->admin_user); - $this->setProtection('user_register_form', MOLLOM_MODE_ANALYSIS, array(), array( + $this->setProtectionUI('user_register_form', MOLLOM_MODE_ANALYSIS, array(), array( 'mollom[unsure]' => 'moderate', )); $this->drupalLogout(); @@ -2726,7 +2771,7 @@ class MollomProfileFormsTestCase extends MollomWebTestCase { } // Enable text analysis protection for user registration form. - $this->setProtection('user_register_form', MOLLOM_MODE_ANALYSIS); + $this->setProtectionUI('user_register_form', MOLLOM_MODE_ANALYSIS); $this->drupalLogout(); // Test each supported field separately. @@ -2788,7 +2833,7 @@ class MollomNodeFormTestCase extends MollomWebTestCase { function testData() { // Enable Mollom CAPTCHA protection for Article nodes. $this->drupalLogin($this->admin_user); - $this->setProtection('article_node_form', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('article_node_form', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Login and submit a node. @@ -2810,7 +2855,7 @@ class MollomNodeFormTestCase extends MollomWebTestCase { */ function testRetain() { $this->drupalLogin($this->admin_user); - $this->setProtection('article_node_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('article_node_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[checks][profanity]' => TRUE, 'mollom[discard]' => 0, )); @@ -2844,7 +2889,7 @@ class MollomNodeFormTestCase extends MollomWebTestCase { // Protect the article node type. $this->drupalLogin($this->admin_user); - $this->setProtection('article_node_form', MOLLOM_MODE_ANALYSIS); + $this->setProtectionUI('article_node_form', MOLLOM_MODE_ANALYSIS); $this->drupalLogout(); // Login and submit a protected article node. @@ -2942,7 +2987,7 @@ class MollomCommentFormTestCase extends MollomWebTestCase { function testCaptchaProtectedCommentForm() { // Enable Mollom CAPTCHA protection for comments. $this->drupalLogin($this->admin_user); - $this->setProtection('comment_node_article_form', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('comment_node_article_form', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Request the comment reply form. There should be a CAPTCHA form. @@ -2956,7 +3001,7 @@ class MollomCommentFormTestCase extends MollomWebTestCase { // required field. $this->postIncorrectCaptcha(NULL, array(), t('Preview')); $this->assertText(t('Comment field is required.')); - $this->assertResponseIDInForm('captchaId', TRUE); + $this->assertResponseIDInForm('captchaId'); $this->assertNoPrivacyLink(); // Try to submit a correct answer for the CAPTCHA, still without required @@ -2995,7 +3040,7 @@ class MollomCommentFormTestCase extends MollomWebTestCase { function testTextAnalysisProtectedCommentForm() { // Enable Mollom text-classification for comments. $this->drupalLogin($this->admin_user); - $this->setProtection('comment_node_article_form'); + $this->setProtectionUI('comment_node_article_form'); $this->drupalLogout(); // Request the comment reply form. Initially, there should be no CAPTCHA. @@ -3117,7 +3162,7 @@ class MollomContactFormTestCase extends MollomWebTestCase { function testProtectContactUserForm() { // Enable Mollom for the contact form. $this->drupalLogin($this->admin_user); - $this->setProtection('contact_personal_form'); + $this->setProtectionUI('contact_personal_form'); $this->drupalLogout(); $this->drupalLogin($this->web_user); @@ -3147,7 +3192,7 @@ class MollomContactFormTestCase extends MollomWebTestCase { function testProtectContactSiteForm() { // Enable Mollom for the contact form. $this->drupalLogin($this->admin_user); - $this->setProtection('contact_site_form'); + $this->setProtectionUI('contact_site_form'); $this->drupalLogout(); // Add some fields to the contact form so that it is active. @@ -3428,7 +3473,7 @@ class MollomDataTestCase extends MollomWebTestCase { */ function testFormButtonValues() { $this->drupalLogin($this->admin_user); - $this->setProtection('mollom_test_form'); + $this->setProtectionUI('mollom_test_form'); $this->drupalLogout(); // Verify that neither the "Submit" nor the "Add" button value is contained @@ -3463,7 +3508,7 @@ class MollomDataTestCase extends MollomWebTestCase { $this->resetAll(); $this->drupalLogin($this->admin_user); - $this->setProtection('comment_node_article_form'); + $this->setProtectionUI('comment_node_article_form'); // Create a node we can comment on. $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); @@ -3558,7 +3603,7 @@ class MollomDataTestCase extends MollomWebTestCase { function testHoneypot() { // Enable protection for mollom_test_form. $this->drupalLogin($this->admin_user); - $this->setProtection('mollom_test_form'); + $this->setProtectionUI('mollom_test_form'); $this->drupalLogout(); // Verify that the hidden honeypot field is output. @@ -3588,7 +3633,7 @@ class MollomDataTestCase extends MollomWebTestCase { // Change form protection to CAPTCHA only. $this->drupalLogin($this->admin_user); - $this->setProtection('mollom_test_form', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); $this->resetServerRecords(); @@ -3624,7 +3669,7 @@ class MollomDataTestCase extends MollomWebTestCase { function testPostIdMapping() { // Enable protection for mollom_test_form. $this->drupalLogin($this->admin_user); - $this->setProtection('mollom_test_form'); + $this->setProtectionUI('mollom_test_form'); $this->drupalLogout(); // Submit a mollom_test thingy. @@ -3803,7 +3848,7 @@ class MollomAnalysisTestCase extends MollomWebTestCase { */ function testUnsureBinary() { $this->drupalLogin($this->admin_user); - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[unsure]' => 'binary', )); $this->drupalLogout(); @@ -3852,12 +3897,12 @@ class MollomAnalysisTestCase extends MollomWebTestCase { $this->drupalLogin($this->admin_user); // Verify that mollom_basic_elements_test_form cannot be configured to put // posts into moderation queue. - $this->setProtection('mollom_basic_elements_test_form'); + $this->setProtectionUI('mollom_basic_elements_test_form'); $this->drupalGet('admin/config/content/mollom/manage/mollom_basic_elements_test_form'); $this->assertNoFieldByName('mollom[unsure]'); // Configure mollom_test_form to retain unsure posts. - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[unsure]' => 'moderate', )); $this->drupalLogout(); @@ -3913,12 +3958,12 @@ class MollomAnalysisTestCase extends MollomWebTestCase { $this->drupalLogin($this->admin_user); // Verify that mollom_basic_test_form cannot be configured to put posts into // moderation queue. - $this->setProtection('mollom_basic_elements_test_form'); + $this->setProtectionUI('mollom_basic_elements_test_form'); $this->drupalGet('admin/config/content/mollom/manage/mollom_basic_elements_test_form'); $this->assertNoFieldByName('mollom[discard]'); // Configure mollom_test_form to accept bad posts. - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[checks][profanity]' => TRUE, 'mollom[discard]' => 0, )); @@ -4018,6 +4063,63 @@ class MollomAnalysisTestCase extends MollomWebTestCase { } /** + * Tests basic text analysis functionality with enabled caching. + */ +class MollomAnalysisPageCacheTestCase extends MollomWebTestCase { + + protected $disableDefaultSetup = TRUE; + + public static function getInfo() { + return array( + 'name' => 'Text analysis with caching', + 'description' => 'Tests basic text analysis functionality with enabled caching.', + 'group' => 'Mollom', + ); + } + + function setUp() { + parent::setUp(array('mollom', 'mollom_test')); + $this->setKeys(); + $this->assertValidKeys(); + $this->enablePageCache(); + } + + /** + * Tests text analysis. + */ + function testAnalysis() { + $this->setProtection('mollom_test_form'); + // Prime the form + page cache. + $this->drupalGet('mollom-test/form'); + $this->assertText('Views: 1'); + $this->drupalGet('mollom-test/form'); + $this->assertText('Views: 1'); + $this->assertUnsureSubmit(NULL, array('title'), array(), 'Submit'); + + $this->drupalGet('mollom-test/form'); + $this->assertText('Views: 1'); + $this->assertUnsureSubmit(NULL, array('title'), array(), 'Submit'); + + $this->drupalGet('mollom-test/form'); + $this->assertText('Views: 1'); + $this->assertSpamSubmit(NULL, array('title'), array(), 'Submit'); + + $this->drupalGet('mollom-test/form'); + $this->assertText('Views: 1'); + $this->assertHamSubmit(NULL, array('title'), array(), 'Submit'); + } + + /** + * Tests text analysis with additionally enabled Form API cache. + */ + function testAnalysisFormCache() { + variable_set('mollom_test.form.cache', TRUE); + $this->testAnalysis(); + } + +} + +/** * Tests CAPTCHA functionality. */ class MollomCaptchaTestCase extends MollomWebTestCase { @@ -4046,7 +4148,7 @@ class MollomCaptchaTestCase extends MollomWebTestCase { $this->web_user = $this->drupalCreateUser(array()); $this->drupalLogin($this->admin_user); - $this->setProtection('mollom_test_form', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); } @@ -4189,7 +4291,7 @@ class MollomReportTestCase extends MollomWebTestCase { */ function testReportComment() { $this->drupalLogin($this->admin_user); - $this->setProtection('comment_node_article_form'); + $this->setProtectionUI('comment_node_article_form'); $this->drupalLogout(); $this->node = $this->drupalCreateNode(array('type' => 'article')); @@ -4226,7 +4328,7 @@ class MollomReportTestCase extends MollomWebTestCase { */ function testMassReportComments() { $this->drupalLogin($this->admin_user); - $this->setProtection('comment_node_article_form'); + $this->setProtectionUI('comment_node_article_form'); $this->drupalLogout(); $this->node = $this->drupalCreateNode(array('type' => 'article')); @@ -4292,7 +4394,7 @@ class MollomModerateUserTestCase extends MollomWebTestCase { parent::setUp(); $this->drupalLogin($this->admin_user); - $this->setProtection('user_register_form', MOLLOM_MODE_CAPTCHA); + $this->setProtectionUI('user_register_form', MOLLOM_MODE_CAPTCHA); $this->drupalLogout(); // Allow visitors to register. @@ -4414,7 +4516,7 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase { parent::setUp(array('mollom_test')); $this->drupalLogin($this->admin_user); - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[discard]' => 0, 'mollom[moderation]' => 1, )); @@ -4583,7 +4685,7 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase { // Test no Mollom moderation access with disabled integration. $this->drupalLogin($this->admin_user); - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[moderation]' => FALSE, )); @@ -4593,7 +4695,7 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase { $record = mollom_test_load($data->id); $this->assertTrue(!empty($record) && $record->status == 0, 'Test post still exists.'); - $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[moderation]' => 1, )); $this->drupalLogout(); @@ -4716,12 +4818,12 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase { user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('create article content', 'view own unpublished content')); $this->drupalLogin($this->admin_user); - $this->setProtection('user_register_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('user_register_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[unsure]' => 'moderate', 'mollom[discard]' => 0, 'mollom[moderation]' => 1, )); - $this->setProtection('article_node_form', MOLLOM_MODE_ANALYSIS, NULL, array( + $this->setProtectionUI('article_node_form', MOLLOM_MODE_ANALYSIS, NULL, array( 'mollom[unsure]' => 'moderate', 'mollom[discard]' => 0, 'mollom[moderation]' => 1, diff --git a/tests/mollom_test.module b/tests/mollom_test.module index 22e34ff..2c6333a 100644 --- a/tests/mollom_test.module +++ b/tests/mollom_test.module @@ -46,6 +46,11 @@ function mollom_test_menu() { 'page arguments' => array('mollom_test_delete_form', 2), 'access callback' => TRUE, ); + $items['mollom-test/form/views/reset'] = array( + 'page callback' => 'mollom_test_views_reset', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -115,6 +120,15 @@ function mollom_test_mollom_form_info($form_id) { } /** + * Page callback; Resets the mollom_test_form() [page] view counter. + */ +function mollom_test_views_reset() { + variable_del('mollom_test.form.views'); + cache_clear_all(); + drupal_goto(); +} + +/** * Form builder for Mollom test form. */ function mollom_test_form($form, &$form_state, $mid = NULL) { @@ -137,6 +151,19 @@ function mollom_test_form($form, &$form_state, $mid = NULL) { // Always add an empty field the user can submit. $form_state['storage']['field']['new'] = ''; + // Output a page view counter for page/form cache testing purposes. + $count = variable_get('mollom_test.form.views', 1); + $reset_link = l('Reset', 'mollom-test/form/views/reset', array('query' => drupal_get_destination())); + $form['views'] = array( + '#markup' => '

' . 'Views: ' . $count++ . ' ' . $reset_link . '

', + ); + variable_set('mollom_test.form.views', $count); + + // Conditionally enable form caching. + if (variable_get('mollom_test.form.cache', FALSE)) { + $form_state['cache'] = TRUE; + } + $form['#tree'] = TRUE; $form['mid'] = array( '#type' => 'hidden', diff --git a/tests/mollom_test_server.module b/tests/mollom_test_server.module index 7135726..8c54c27 100644 --- a/tests/mollom_test_server.module +++ b/tests/mollom_test_server.module @@ -97,6 +97,9 @@ function mollom_test_server_rest_get_auth_header() { * Delivery callback for REST API endpoints. */ function mollom_test_server_rest_deliver($page_callback_result) { + // All fake-server responses are not cached. + drupal_page_is_cacheable(FALSE); + drupal_add_http_header('Content-Type', 'application/xml; charset=utf-8'); $xml = new DOMDocument('1.0', 'utf-8'); -- 1.7.11.msysgit.1