

See part 2 This is the first of a two-part technical post. We will examine Drupal architecture and custom code used in a Beaconfire project.See part 2 here

If you’re reading our Drupal tech posts, Beaconfire RED is hiring and we want to talk to you! Come join our tech team in 2017 and play with Drupal 8!

At Beaconfire we recently completed a Drupal 7 site with a previously unfamiliar requirement: allow users to authenticate using an email address or mobile phone number. As expected, this adds complexity in many areas beyond simply modifying an input field.

We had to implement support for each of the following:

Registration Form Add mobile phone field Remove email-specific requirement Disable mail field description (it was no longer applicable to the registration logic)

Login Form Change email field to email/phone combination field

Add custom validation to both login & registration forms to properly require at least email or mobile phone (it validates more than this, but this will be covered later)

In addition, this Drupal site needed to integrate as a sign-on gateway for a third-party application. The integration of that authentication is a simple key-based data-transfer, but the responsibility of verifying identity became a core part of our responsibility. To complete this, we had to enable email-based verification of all accounts. Of course, this means that we have to build mobile phone verification as well! The components we ended up building included:

Custom validation logic whenever mobile phone used during registration

Custom field in database to track whether mobile phone has been verified

When user registers with only a mobile phone, generate mobile code, associate with user, and send via SMS to registered phone number

When user registers with only a mobile phone, redirect to custom verification page to enter code

Validate custom phone verification form and perform one-time login if successful

For users registered with phone and email address, show phone verification status on account page and allow user to verify account when logged in

If you’re reading this blog post, we want people like you on our team! We’re hiring an experienced PHP Developer. Find out more, or apply for this job

To fulfill the needs of the mobile sign up, we will add and create the following:

Implement the Twilio PHP SDK

Create a wrapper method to send text messages

Create a class named Code to handle verification codes for mobile users

To improve the user experience (primarily on mobile) we also updated the following (requires Entity):

Convert all email (not email & mobile phone combination) fields to type “email”

Convert all zip code fields to type “number”

Convert all phone (not email & mobile phone combination) fields to type “tel”

This project also had requirements to support an initial seven languages, maintain a separate member database outside of Drupal’s users table, and payment processing. The completion of these requirements won’t be specifically covered here, but references to them may pop-up to clarify why certain decisions were made. Next we will begin a technical walk-through of the solutions created by Beaconfire.

Requirements:

To complete this example, you will need a Twilio account with either a purchased number, or a trial number with your mobile phone number verified as a trial recipient.

Prerequisites:

This example will assume knowledge of Drupal module development. We won’t cover how to create your .info files and a few other common steps for the sake of brevity.

Step 1: Preparing the Registration Form

The first step here is to add the mobile phone field to the registration form. This can be done either through Drupal’s custom user fields, or through hook_form_alter. For this site, we created the field in Drupal to take advantage of the existing support for the user object. To do this, go to admin/config/people/accounts/fields and add the field. We created a text field named “field_user_phone_cell.”

With that field in place, we now need to customize the display of the existing elements on the registration form. Create a module (we will refer to it as “bf_registration”) and in it add a function named bf_registration_form_alter. In this form, we add the following:

function bf_registration_form_alter(&$form, &$form_state,$form_id) { switch ($form_id) { /* Register Form */ case 'user_register_form': // Add custom submit handler $form['#submit'] = is_array($form['#submit']) ? $form['#submit'] : array($form['#submit']); $form['#submit'][] = 'bf_registration_form_register_submit'; // Make mail field not required $form['account']['mail']['#required'] = FALSE; // Change email and telephone fields to HTML5 types (this requires the entity module) $form['account']['mail']['#type'] = 'emailfield'; $form['field_user_phone_cell']['und'][0]['value']['#type'] = 'telfield'; // Add custom validate handler to beginning of array array_unshift($form['#validate'],'bf_registration_form_register_validate'); // Remove description from mail field $form['account']['mail']['#description'] = null; // Make zip code field of type "number" $form['field_zip_code']['und'][0]['value']['#type'] = "numberfield"; break; } }

Step 2: Preparing the Login Form

Inside the bf_register_form_alter, add a second case to the switch statement:

/* Login Form */ case 'user_login': // On login form, hide name field and add email/mobile phone field $form['name']['#type'] = 'hidden'; $form['name']['#required'] = false; $form['combo'] = array( "#type" => "textfield", "#title" => t("Phone or Email Address"), "#weight" => -10, "#required" => true, "#description" => t("Enter the mobile phone number or email address provided during registration"), "#attributes" => array( "placeholder" => t("(777-777-7777 or email@mail.com)") ) ); // Update description for password field $form['pass']['#description'] = t("Enter the password that accompanies your mobile phone number or email address"); // Apply custom validation $form["#validate"] = array('bf_registration_form_login_validate'); break;

It’s important to note that, instead of re-using the “name” field, we have instead hidden it and made it not required. This was intentionally done to avoid conflicts with Drupal’s core logic. It’s likely that re-using the name field would not have caused any problems, but in the effort to avoid conflicts we attempt to use custom fields where possible.

Step 3: Registration Validation

In step 1 we prepended the function bf_registration_form_register_validate to the validate handler array. We inserted this at the beginning of the array because it needs to fire before the Drupal core validate method for the registration form. This is because we need to populate email address and username from the data available to make our methods work properly with Drupal’s core registration functionality. This is our validation method:

function bf_registration_form_register_validate($form, &$form_state){ $mail = $form_state['input']['mail']; // Store actual mail value provided because $mail will be overwritten with cell value if needed $actual_mail = $mail ? $mail : null; // Validate cell phone $cell_phone = if( isset($form_state['input']['field_user_phone_cell']['und']) && trim($form_state['input']['field_user_phone_cell']['und'][0]['value'])){ $form_state['input']['field_user_phone_cell']['und'][0]['value']; } if($cell_phone && !$cell_phone = validate_mobile_number($form_state['input']['field_user_phone_cell']['und'][0]['value'])) form_set_error('field_user_phone_cell', t('The cell phone number provided is not valid.')); if($cell_phone){ // Check if cell phone number is already in use in Drupal DB $result = db_select('field_data_field_user_phone_cell','c') ->fields('c') ->condition('field_user_phone_cell_value',$cell_phone,'=') ->range(0,1) ->execute() ->fetchAssoc(); if($result){ form_set_error('field_user_phone_cell', t('The cell phone number provided is already in use.')); $form_state['values']['mail'] = 'ph@fake.mail.org'; $cell_phone = null; } else{ // Update value in form data with clean number $form['field_user_phone_cell']['#parents'] = array('field_user_phone_cell'); form_set_value($form['field_user_phone_cell'], array('und' => array(0 => array('value' => $cell_phone))), $form_state); } } // Validate zip code (validate_zip_code is a method that strips non-numeric characters and verifies its length is correct if(!$zip = validate_zip_code($form_state['input']['field_zip_code']['und'][0]['value'])) form_set_error('field_zip_code',t("The zip code provided is not valid")); // If only cell phone was provided, generate an email address to make Drupal happy if($cell_phone && !$mail){ $mail = preg_replace("/[^0-9]/", "", $cell_phone)."@members.domain.org"; $form_state['values']['mail'] = $mail; } // Verify either an email address or cell number was provided if(!$cell_phone && !$mail){ form_set_error('', t('You must provide a valid cell phone number or email address.')); // Add placeholder value to email to avoid triggering extra validation errors $form_state['values']['mail'] = 'ph@fake.org'; } }

With this method we determine if a cell phone number was provided, and if so, validate it. In addition, if a cell phone was provided and an email address was not, we generate an email address using a variation of the site’s domain name (in this example domain.org) to ensure that Drupal has an email address on file. This proved to be a much smoother process than trying to modify Drupal to not expect an email address.

Step 4: Submit Registration

In bf_registration_form_alter we appended a custom submit handler to the registration form. The function for this handler is:

function bf_registration_form_register_submit($form,&$form_state){ // Load user object that was created by the previous submit handler $user = user_load_by_name($form_state['values']['name']); // If registering with a mobile number only, send to mobile confirmation URL & change confirmation message if(isset($user->field_user_phone_cell['und'][0]['value']) && strpos($user->mail,"@members.domain.org") <> false){ $form_state['redirect'] = 'user/register/mobile/'.$user->field_user_phone_cell['und'][0]['value']; // Generate code $code = new MobileCode(array( "drupal_uid" => $user->uid )); $code->Generate(); $code->Save(); // Send code $message_id = send_twilio_message($code->code, $user->field_user_phone_cell['und'][0]['value']); // Disable email confirmation message $message_key = array_search(t("A welcome message with further instructions has been sent to your e-mail address."), $_SESSION['messages']['status']); if(!$message_id){ // Phone number was not valid. This account can't be verified because the message was not sent. Delete the account $form_state['redirect'] = 'user/register'; // Delete the status message saying the registration was successful drupal_get_messages('status',TRUE); user_delete($user->uid); } else{ if(is_numeric($message_key)){ $_SESSION['messages']['status'][$message_key] = t('A text message has been sent to %phone with a verification code. Please enter the code below.',array("%phone"=>$user->field_user_phone_cell['und'][0]['value'])); } } } }

This method calls a few things that we have yet to define. It references a Code class that we will define soon as well as a send_twilio_message method. This is a wrapper method for the Twilio PHP SDK we use to easily send basic text messages. We will define both of these in Part 2 of this series.

Step 5: Login Form Custom Validation

In bf_registration_form_alter, for the login form case, we did a few things:

Hide the name field and make it not required Create a custom combo field that can be used for either mobile number or email address Override the core login validation for our own method

For the third item it’s important to know the difference between this and the validate/submit handlers for the registration form. Instead of appending or prepending our validation handler, we are replacing Drupal’s core validation entirely. In doing so, we have to be sure that we are careful not to open any security gaps.

function bf_registration_form_login_validate($form, &$form_state){ $combo = trim($form_state['values']['combo']); $pass = trim($form_state['values']['pass']); // Check if combo value and password provided. If not, drupal will handle error message if(!$combo || !$pass) return; // Determine if combo field provided with email or phone number if(strpos($combo,"@") === false){ // Assume mobile phone number if(!($cell_phone = validate_mobile_number($combo))){ form_set_error('combo',t("The mobile number provided does not appear to be valid")); } else{ // Number appears to be valid, try to find user in DB $result = db_query("SELECT u.name FROM {users} u, {field_data_field_user_phone_cell} p WHERE p.field_user_phone_cell_value = :phone AND u.uid = p.entity_id LIMIT 0,1", array( ":phone"=>$cell_phone ) ); $row = $result->fetchAssoc(); if(!$row || !isset($row['name'])){ // User could not be found with this mobile number form_set_error('combo',t("The mobile number provided could not be found")); } else{ $name = $row['name']; } } } else{ // Assume email address $row = db_select('users', 'u') ->fields('u') ->condition('mail', $combo,'=') ->range(0,1) ->execute() ->fetchAssoc(); if(!$row || !isset($row['name'])){ // User could not be found with this email address form_set_error('combo',t("The email address provided could not be found")); } else{ $name = $row['name']; } } // If name was found attempt to authenticate if(isset($name)){ $uid = user_authenticate($name,$pass); if($uid == false){ form_set_error('',t('The mobile phone/email address and password combination provided is not valid')); } else{ // Log user in global $user; $user = user_load($uid); $form_state['uid'] = $uid; user_login_submit(array(), $form_state); } } }

This function uses another utility method named validate_mobile_number. Let’s define that in this module file as well at the end:

function validate_mobile_number($number){ // Strip out non-numeric characters and check length. It must be 10 digits $cell_phone = preg_replace("/[^0-9]/", "", $number); // Verify string length == 10 if(strlen($cell_phone) <> 10) return false; return $cell_phone; }

This method will return either a stripped numeric string if it it has 10 digits, or it will return false.

Next Steps

In part 2 we will create a Code class as well as the other helper methods to allow users to receive verification codes and apply them as one-time logins.