Note: This article was originally published on the Growing Venture Solutions website.
When a form protected by spam prevention measures such as captcha or Mollom is submitted with drupal_execute, validation can fail unless the spam protection is properly suppressed.
This blog post describes the background and solution to a bug that previously existed in the Signup Integration for Ubercart module (uc_signup), and explains the techniques used to fix the bug. It is written with developers and aspiring developers in mind, though other people interested in how Drupal works might also find it interesting.
drupal_execute is a function often used in data imports that allows a developer to take a collection of form values and submit them programatically.
A main reason to use drupal_execute over another technique such as user_save() is that with drupal_execute, Drupal calls the validation and submission functions for the form.
The Context: How uc_signup Uses drupal_execute
In the Signup integration for Ubercart module, we sometimes create a new user account and populate the user's profile with data that was submitted on a form separate from the core user profile form. In earlier versions of uc_signup, we created the new account with user_save, however this allowed crafty users to leave required fields blank by skipping the form and proceeding to checkout, so we switched to drupal_execute which ensures that the form's validation gets executed.
The drupal_execute function is relatively easy to use -- just pass in the form_id and form values you'd like to submit to the form.
Captchas often take the shape of "those squiggly letters" used to determine whether you are a human being or a computer program that submits spam to websites.
Uc_signup can collect profile information for multiple users on a single "attendee contact information" form that is integrated into the Ubercart checkout process.
Uc_signup's Attendee Contact Information" form.
By design, this form has a unique form_id, so it isn't protected by captchas that protect the user registration form. The required fields on the attendee information form correspond to those on the user registration form -- except that on the attendee information form, there is no captcha. When uc_signup takes user-entered data from this form and submits it into the user registration form, validation on the user registration form fails because this form has a captcha, which was never presented to the user and is therefore not filled in.
Finding a Solution
So, we needed a way to bypass the captcha protection.
Should we unset the form validation function from the captcha element? Should we remove the captcha element from the form altogether?
The simplest and least error-prone solution turns out to be to prevent the captcha from ever being added to the user registration form for this specific situation.
Both internal spam prevention services (such as captcha and spam.module) and external ones (such as Mollom and Akismet) use Drupal's user permissions system to allow certain users to bypass the protection that they add to forms.
drupal_execute submits forms with the permissions of whichever user is logged in (or an anonymous user). In most cases, people who are using the attendee information form will not have permission to bypass spam protection site-wide. The solution to our captcha validation problem is to have our module submit the form as if it were a privileged user. That way, the captcha will never be added to the form.
User Impersonation: Not just for Halloween.
User impersonation is a technique that developers can use to allow a particular section of code to be run as a particular user. In Drupal, the user with UID 1 has all permissions available on a site. In uc_signup, we impersonate User 1 before performing the drupal_execute so that captcha waves protection for the form.
Practicing Safe Impersonation
It's important to follow best practices when performing user impersonation. Not doing so can allow users on your site to gain control over the account being used for the impersonation, resulting in a serious security vulnerability.
global $user;, setting the $user variable equal to a different user account will cause the currently logged in user to switch to that account. That is why it is a best practice to use another variable, such as $account, when working with user accounts without the intention of impersonating users.
You must set session_save_session(FALSE); before switching to the impersonated user. The reason for this is that if your action fails or interrupts the request with a drupal_goto and session saving is enabled, the user will then be logged in as user 1, with all privileges on your site. By setting this value to FALSE and then back to TRUE, we make sure that the impersonation only exists when we're performing the required action, and does not persist if the action fails.
The code snippet below is a more heavily commented version of the code used in uc_signup. This code snippet impersonates User 1, submits the user registration form with the previously entered values, and then switches the session back to the user who was previously logged in. Note that we have three variables that refer to user accounts:
global $user - The account of the currently logged in user.
$temp_user - The variable used to store the current user's account while we temporarily switch the session to the privileged user.
$account - The account created as a result of submitting the user registration form.
This code actually appears inside of a
foreach loop and some conditional logic, so that it if the attendee contact information form collects information for multiple new users, the user registration form is submitted once for each new account.
To get the full context of this snippet, you may wish to view the full contents of uc_signup.module.
//Load the global $user object that contains the account of the currently logged in user.
//Preserve this account in the $temp_user variable so that we can switch back to it after impersonating the privileged user.
$temp_user = $user;
//We must set this to FALSE in case the operation on the following lines fails.
//Switch the currently logged in user to user 1.
$user = user_load(1);
//Submit the user registration form.
//Switch back to the account we saved in the $temp_user variable.
$user = $temp_user;
//Restore session saving.
//Populate the $account variable with the account created as a result of submitting the user_register form. We later go on to sign up this user to one or more events that are being purchased.
$account = $form_state['user'];