You are here

sms_valid.module in SMS Framework 6

Number validation feature module for Drupal SMS Framework.

@package sms @subpackage sms_valid

File

modules/sms_valid/sms_valid.module
View source
<?php

/**
 * @file
 * Number validation feature module for Drupal SMS Framework.
 *
 * @package sms
 * @subpackage sms_valid
 */

/**
 * Implement hook_sms_validate()
 *
 * @param $op
 *   Validation operation to work on.
 * @param $number
 *   Phone number string.
 * @param $options
 *   Array of options.
 *
 * @return
 *   NULL if validation succeeded. Error string if failed.
 *
 * @ingroup hooks
 */
function sms_valid_sms_validate($op, &$number, &$options) {
  if ($op == 'process') {
    if (variable_get('sms_valid_use_rulesets', FALSE) || array_key_exists('test', $options)) {
      $result = sms_valid_validate($number, $options);
      if ($result['pass']) {
        return NULL;
      }
      else {
        return array_pop($result['log']);
      }
    }
  }
}

/**
 * Implementation of hook_menu()
 *
 * @return
 *   Drupal menu item array.
 *
 * @ingroup hooks
 */
function sms_valid_menu() {
  $items = array();
  $items['admin/smsframework/validation'] = array(
    'title' => 'Number validation',
    'description' => 'Configure number validation and rulesets.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sms_valid_admin_settings_form',
      NULL,
    ),
    'access arguments' => array(
      'administer smsframework',
    ),
    'file' => 'sms_valid.admin.inc',
  );
  $items['admin/smsframework/validation/settings'] = array(
    'title' => 'Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/smsframework/validation/rulesets'] = array(
    'title' => 'Rulesets',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sms_valid_admin_rulesets_form',
      NULL,
    ),
    'access arguments' => array(
      'administer smsframework',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => -9,
    'file' => 'sms_valid.admin.inc',
  );
  $items['admin/smsframework/validation/ruleset'] = array(
    'title' => 'Add/Edit ruleset',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sms_valid_admin_ruleset_form',
    ),
    'access arguments' => array(
      'administer smsframework',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => -8,
    'file' => 'sms_valid.admin.inc',
  );
  $items['admin/smsframework/validation/ruleset/%'] = array(
    'title' => 'Edit ruleset',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sms_valid_admin_ruleset_form',
      4,
    ),
    'access arguments' => array(
      'administer smsframework',
    ),
    'type' => MENU_CALLBACK,
    'weight' => -7,
    'file' => 'sms_valid.admin.inc',
  );
  $items['admin/smsframework/validation/test'] = array(
    'title' => 'Test validation',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sms_valid_admin_test_form',
      NULL,
    ),
    'access arguments' => array(
      'administer smsframework',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => -6,
    'file' => 'sms_valid.admin.inc',
  );
  return $items;
}

/**
 * Implementation of hook_theme()
 *
 * @return
 *   Array of Drupal theme items.
 *
 * @ingroup hooks
 */
function sms_valid_theme() {
  return array(
    'sms_valid_admin_rulesets_form' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'file' => 'sms_valid.admin.inc',
    ),
    'sms_valid_admin_ruleset_form' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'file' => 'sms_valid.admin.inc',
    ),
  );
}

/**
 * Get all rulesets
 *
 * @return
 *   Array of rulesets.
 */
function sms_valid_get_all_rulesets() {
  $result = db_query("SELECT * FROM {sms_valid_rules}");
  $rulesets = array();
  while ($row = db_fetch_array($result)) {
    $prefix = $row['prefix'];
    $rulesets[$prefix] = $row;
    $rulesets[$prefix]['rules'] = unserialize($row['rules']);
  }
  return $rulesets;
}

/**
 * Get a ruleset for a given prefix
 *
 * @param $prefix
 *   A numberic prefix.
 *
 * @return
 *   A ruleset array or FALSE.
 */
function sms_valid_get_ruleset($prefix) {
  $result = db_query("SELECT * FROM {sms_valid_rules} WHERE prefix = %d LIMIT 1", $prefix);
  if (db_affected_rows()) {
    $ruleset = db_fetch_array($result);
    $ruleset['rules'] = unserialize($ruleset['rules']);
    return $ruleset;
  }
  return FALSE;
}

/**
 * Get the best ruleset for a given phone number
 *
 * @param $number
 *   A phone number.
 *
 * @return
 *   A ruleset array or NULL.
 */
function sms_valid_get_ruleset_for_number($number) {

  // Strip all non-digit chars including whitespace
  $number = preg_replace('/[^0-9]/', '', $number);

  // Make an array of potential prefixes from the given number
  for ($i = 0; $i < strlen($number); $i++) {
    $potential_prefixes[] = substr($number, 0, $i + 1);
  }

  // Get the potential rulesets from the DB
  $result = db_query("SELECT * FROM {sms_valid_rules} WHERE prefix IN (" . db_placeholders($potential_prefixes, 'int') . ")", $potential_prefixes);

  // Choose the ruleset with the best match (most chars = highest prefix)
  $best_ruleset = NULL;
  $last_prefix = NULL;
  while ($row = db_fetch_array($result)) {
    if ($row['prefix'] > $last_prefix) {
      $best_ruleset = $row;
    }
    $best_ruleset['rules'] = unserialize($best_ruleset['rules']);
  }
  return $best_ruleset;
}

/**
 * Get prefixes for a given ISO country code
 *
 * @param $iso2
 *   A two-character ISO-3166-1 alpha-2 country code
 *
 * @return
 *   Array of prefix numbers.
 */
function sms_valid_get_prefixes_for_iso2($iso2) {
  $result = db_query("SELECT prefix FROM {sms_valid_rules} WHERE iso2 = '%s'", $prefix);
  $prefixes = array();
  while ($row = db_fetch_object($result)) {
    $prefixes[] = $row->prefix;
  }
  return $prefixes;
}

/**
 * Check what directions are enabled for a ruleset
 *
 * @param $prefix
 *   A prefix number.
 * @param $dir
 *   The direction code that you want to check. See SMS_DIR_* constants.
 *
 * @return
 *   Boolean. Whether the ruleset is enabled for this direction.
 */
function sms_valid_ruleset_is_enabled($prefix, $dir = SMS_DIR_OUT) {
  $result = db_query("SELECT dirs_enabled FROM {sms_valid_rules} WHERE prefix = %d LIMIT 1", $prefix);
  $dirs_enabled = db_result($result);

  // There must be a better way of doing this, but this works ok
  if ($dirs_enabled == SMS_DIR_ALL) {
    return TRUE;
  }
  if ($dirs_enabled == SMS_DIR_OUT && $dir == SMS_DIR_OUT) {
    return TRUE;
  }
  if ($dirs_enabled == SMS_DIR_IN && $dir == SMS_DIR_IN) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Set enabled directions for a ruleset
 *
 * @param $prefix
 *   A prefix number.
 * @param $dir
 *   The direction code that you want to set. See SMS_DIR_* constants.
 *
 * @return
 *   Bollean. Result of the DB query.
 */
function sms_valid_ruleset_set_status($prefix, $dir = SMS_DIR_ALL) {
  return db_query("UPDATE {sms_valid_rules} SET dirs_enabled = %d WHERE prefix = %d", $dir, $prefix);
}

/**
 * Create or update a ruleset
 *
 * @param $ruleset
 *   A ruleset array.
 *
 * @return
 *   Boolean. Result of the DB query.
 */
function sms_valid_save_ruleset($ruleset) {
  $prefix = $ruleset['prefix'];
  $name = $ruleset['name'];
  $iso2 = $ruleset['iso2'];
  $rules_z = serialize($ruleset['rules']);
  $dirs_enabled = $ruleset['dirs_enabled'];
  if (sms_valid_get_ruleset($prefix)) {

    // The ruleset exists so we should update
    return db_query("UPDATE {sms_valid_rules} SET\n                    name = '%s', rules = '%s', dirs_enabled = %d,\n                    iso2 = '%s' WHERE prefix = %d", $name, $rules_z, $dirs_enabled, $iso2, $prefix);
  }
  else {

    // The ruleset does not exist so we should create
    return db_query("INSERT INTO {sms_valid_rules}\n                     (prefix, name, rules, dirs_enabled, iso2)\n                     VALUES\n                     (%d, '%s', '%s', %d, '%s')", $prefix, $name, $rules_z, $dirs_enabled, $iso2);
  }
}

/**
 * Delete a ruleset
 *
 * @param $prefix
 *   A prefix number.
 *
 * @return
 *   Boolean. Result of the DB query.
 */
function sms_valid_delete_ruleset($prefix) {
  return db_query("DELETE FROM {sms_valid_rules} WHERE prefix = %d", $prefix);
}

/**
 * Get the rules for a prefix
 *
 * @param $rules
 *   A prefix number.
 *
 * @return
 *   An array of rules.
 */
function sms_valid_get_rules($prefix) {
  $ruleset = sms_valid_get_ruleset($prefix);
  return unserialize($ruleset['rules']);
}

/**
 * Distill rules text into a rules array
 *
 * @param $text
 *   A text string containing rules for a ruleset.
 *
 * @return
 *   An array of rules.
 */
function sms_valid_text_to_rules($text) {
  $lines = explode("\n", $text);
  $rules = array();
  foreach ($lines as $line) {
    if (empty($line)) {
      continue;
    }

    // Capture any comments and then strip them
    preg_match('/\\#(.*)/', $line, $matches);
    $comment = trim($matches[1]);
    $line = trim(preg_replace('/\\#.*/', '', $line));

    // Check if we are allowing or denying, deny by default
    $allow = preg_match('/\\+/', $line) ? TRUE : FALSE;

    // Erase non-digit chars to get the prefix
    $rule_prefix = trim(preg_replace('/[\\D]/', '', $line));

    // Add to rules array
    $rules[$rule_prefix] = array(
      'allow' => $allow,
      'comment' => $comment,
    );
  }
  return $rules;
}

/**
 * Implode a rules array into rules text
 *
 * @param $rules
 *   A rules array.
 *
 * @return
 *   A text string containing rules for a ruleset.
 */
function sms_valid_rules_to_text($rules) {
  $lines = array();
  if ($rules && is_array($rules)) {
    foreach ($rules as $rule_prefix => $rule) {
      $allow = $rule['allow'] ? '+' : '-';
      $comment = $rule['comment'] ? '    # ' . $rule['comment'] : '';
      $lines[] = $rule_prefix . $allow . $comment;
    }
  }
  return implode("\n", $lines);
}

/**
 * Get country codes for form options
 *
 * @param $include_null_option
 *   Whether to include a null option in the resulting array. TRUE or FALSE.
 *
 * @return
 *   Options array that can be used in a form select element.
 */
function sms_valid_get_rulesets_for_form($include_null_option = FALSE) {

  // We only really need a null option on the send form
  if ($include_null_option) {
    $options[-1] = '(auto select)';
  }

  // Other options
  $rulesets = sms_valid_get_all_rulesets();
  foreach ($rulesets as $prefix => $ruleset) {
    $suffix = !empty($ruleset['iso2']) ? ' (' . $ruleset['iso2'] . ')' : '';
    $options[$prefix] = $prefix . ' : ' . $ruleset['name'] . $suffix;
  }
  return $options;
}

/**
 * Check if number is a local number
 *
 * @param $number
 *   A phone number
 *
 * @return
 *   Boolean. Whether this is a local number.
 */
function sms_valid_is_local_number($number) {
  $prefix = variable_get('sms_valid_local_number_prefix', '0');

  // A blank prefix string makes this return false
  if ($prefix !== '' && preg_match("/^{$prefix}/", $number)) {
    return TRUE;
  }
  else {
    return FALSE;
  }
}

/**
 * Validate a number
 *
 * @param $number
 *   A phone number.
 * @param $options
 *   An array of options.
 *   - dir  : Direction of message. See SMS_DIR_* constants.
 *
 * @return
 *   Array with the validation result.
 *   - pass : The validation result.
 *       - TRUE  if the number passed validation checks.
 *       - FALSE if the number is denied by validation.
 *       - NULL  if the number could not be validated.
 *   - log  : Array of log strings. The last record is the most significant.
 */
function sms_valid_validate(&$number, &$options = array()) {
  $result = array(
    'pass' => NULL,
    'log' => array(),
  );

  // Set the default direction if not specified in options
  $dir = array_key_exists('dir', $options) ? $options['dir'] : SMS_DIR_OUT;
  $use_global_ruleset = variable_get('sms_valid_use_global_ruleset', FALSE);
  $global_ruleset = variable_get('sms_valid_global_ruleset', '64');
  $local_number_prefix = variable_get('sms_valid_local_number_prefix', '0');
  $local_number_ruleset = variable_get('sms_valid_local_number_ruleset', '64');
  $last_resort_enabled = variable_get('sms_valid_last_resort_enabled', FALSE);
  $last_resort_ruleset = variable_get('sms_valid_last_resort_ruleset', NULL);

  // Check if we should use a specific ruleset prefix
  if (array_key_exists('prefix', $options) && $options['prefix'] >= 0) {
    $specific_prefix = $options['prefix'];
  }
  else {
    $specific_prefix = NULL;
  }

  // Check for zero-length value
  if (!strlen($number)) {
    $result['log'][] = t('You must enter a phone number.');
    return $result;
  }

  // Remove all whitespace
  $number = preg_replace('/[^\\d]/', '', $number);

  // Check if we should use a specific ruleset
  if ($specific_prefix) {
    $prefix = $specific_prefix;
    $ruleset = sms_valid_get_ruleset($prefix);

    // Strip ruleset prefix (if exist) and leading zeros from the number
    $num = preg_replace("/^{$prefix}/", '', $number);
    $num = ltrim($num, '0');
  }
  elseif ($use_global_ruleset) {
    $result['log'][] = 'Using the global prefix validation ruleset.';
    $prefix = $global_ruleset;
    $ruleset = sms_valid_get_ruleset($prefix);
    $num = $number;
  }
  elseif (sms_valid_is_local_number($number)) {
    $prefix = $local_number_ruleset;
    $ruleset = sms_valid_get_ruleset($prefix);
    $result['log'][] = 'Identified local number. Using ruleset prefix ' . $prefix;

    // Strip the local prefix from number
    $num = preg_replace("/^{$local_number_prefix}/", '', $number);
  }
  else {
    $ruleset = sms_valid_get_ruleset_for_number($number);
    if ($ruleset) {
      $result['log'][] = 'Identified ruleset prefix ' . $prefix;
      $prefix = $ruleset['prefix'];
    }
    else {

      // Could not identify ruleset prefix
      $result['log'][] = 'Could NOT identify the ruleset prefix for number ' . $number;
      if ($last_resort_enabled && $last_resort_ruleset) {

        // We have a last resort to use
        $result['log'][] = 'Using last resort ruleset prefix ' . $last_resort_ruleset;
        $prefix = $last_resort_ruleset;
        $ruleset = sms_valid_get_ruleset($prefix);
      }
      else {

        // No last resort. Fail hard.
        $result['log'][] = 'No matching rulesets and no last resort ruleset configured.';
        $result['log'][] = 'Cannot validate this number. Denied by default.';
        return $result;
      }
    }

    // Strip the ruleset prefix from the number
    $num = preg_replace("/^{$prefix}/", '', $number);
  }

  // Get the rules for this ruleset
  $rules = $ruleset['rules'];

  // Sort the rules by prefix (key) in reverse order (largest to smallest)
  krsort($rules);

  // Check whether this ruleset is enabled for the direction of communication
  if (!sms_valid_ruleset_is_enabled($prefix, $dir)) {
    $result['log'][] = "Number prefix {$prefix} does not allow messages in this direction.";
    $result['pass'] = FALSE;
    return $result;
  }

  // Test the number against each rule prefix until we get a match
  if (!empty($rules)) {
    foreach ($rules as $rule_prefix => $rule) {
      $result['log'][] = 'Trying rule with prefix ' . $rule_prefix . ' on number ' . $num;
      if (preg_match("/^{$rule_prefix}/", $num)) {
        if ($rule['allow']) {

          // Set the full validated number and return
          $number = $prefix . $num;
          $result['log'][] = "Explicit allow for prefix {$prefix} {$rule_prefix}";
          $result['pass'] = TRUE;
          return $result;
        }
        else {
          $result['log'][] = "Explicit deny for prefix {$prefix} {$rule_prefix}";
          $result['pass'] = FALSE;
          return $result;
        }
      }
    }
  }

  # No matching rules. Default deny.
  $result['log'][] = "Cannot validate this number. Denied by default.";
  return $result;
}

Functions

Namesort descending Description
sms_valid_delete_ruleset Delete a ruleset
sms_valid_get_all_rulesets Get all rulesets
sms_valid_get_prefixes_for_iso2 Get prefixes for a given ISO country code
sms_valid_get_rules Get the rules for a prefix
sms_valid_get_ruleset Get a ruleset for a given prefix
sms_valid_get_rulesets_for_form Get country codes for form options
sms_valid_get_ruleset_for_number Get the best ruleset for a given phone number
sms_valid_is_local_number Check if number is a local number
sms_valid_menu Implementation of hook_menu()
sms_valid_ruleset_is_enabled Check what directions are enabled for a ruleset
sms_valid_ruleset_set_status Set enabled directions for a ruleset
sms_valid_rules_to_text Implode a rules array into rules text
sms_valid_save_ruleset Create or update a ruleset
sms_valid_sms_validate Implement hook_sms_validate()
sms_valid_text_to_rules Distill rules text into a rules array
sms_valid_theme Implementation of hook_theme()
sms_valid_validate Validate a number