You are here

shurly.module in ShURLy 6

Same filename and directory in other branches
  1. 8 shurly.module
  2. 7 shurly.module

description http://www.youtube.com/watch?v=Qo7qoonzTCE

@todo

  • click to copy link as a Views field
  • add some watchdog logging
  • add hook for other modules to create additional/substitute long URL validation
  • add option/permission to reactivate URLs

File

shurly.module
View source
<?php

/**
 * @file description http://www.youtube.com/watch?v=Qo7qoonzTCE
 *
 * @todo
 *   - click to copy link as a Views field
 *   - add some watchdog logging
 *   - add hook for other modules to create additional/substitute long URL validation
 *   - add option/permission to reactivate URLs
 */

/**
 * Implementation of hook_help().
 */
function shurly_help($path, $arg) {
  $output = '';
  switch ($path) {
    case "admin/help#shurly":
      $output = '<div style="white-space:pre-wrap">' . htmlentities(file_get_contents('README.markdown', FILE_USE_INCLUDE_PATH)) . '</div>';
      break;
  }
  return $output;
}

/**
 * Implementation of hook_menu()
 */
function shurly_menu() {

  // callback for creation of URLs
  $items = array();
  $items['shurly'] = array(
    'title' => 'Create URL',
    'description' => 'Create a short URL',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'shurly_create_form',
    ),
    'access arguments' => array(
      'Create short URLs',
    ),
  );
  $items['shurly/delete/%'] = array(
    'title' => 'Delete URL',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'shurly_confirm_delete_form',
      2,
    ),
    'access callback' => 'shurly_delete_access',
    'access arguments' => array(
      2,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/build/shurly/settings'] = array(
    'title' => 'Settings',
    'description' => t('Configure ShURLy.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'shurly_settings_form',
    ),
    'access arguments' => array(
      'Administer short URLs',
    ),
    'file' => 'shurly.admin.inc',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implementation of hook_perm()
 */
function shurly_perm() {
  return array(
    'Create short URLs',
    'Enter custom URLs',
    'View own URL stats',
    'Delete own URLs',
    'Administer short URLs',
  );
}
function shurly_block($op = 'list', $delta = 0, $edit = array()) {

  // create a block to add URL
  switch ($op) {
    case 'list':
      $blocks['form'] = array(
        'info' => t('Short URL form'),
      );
      $blocks['bookmarklet'] = array(
        'info' => t('ShURLy bookmarklet'),
      );
      return $blocks;
    case 'view':

      // don't show the block when user is on the callback page
      if ($delta == 'form' && user_access('Create short URLs') && arg(0) != 'shurly') {
        $block = array(
          'subject' => t('Create a short URL'),
          'content' => drupal_get_form('shurly_create_form'),
        );
        return $block;
      }
      if ($delta == 'bookmarklet' && user_access('Create short URLs')) {
        drupal_add_css(drupal_get_path('module', 'shurly') . '/shurly.css');
        $block = array(
          'subject' => t('Bookmarklet'),
          'content' => t("<p>Drag this link to your bookmark bar to quickly create a short URL from any page: <a class=\"shurly-bookmarklet\" href=\"!jsurl\">!sitename</a><br />\nOr install the <a href=\"http://github.com/downloads/Lullabot/shurly/shurly.safariextz\">Safari browser extension</a>.</p>", array(
            '!jsurl' => "javascript:void(location.href='" . _surl('shurly', array(
              'absolute' => TRUE,
            )) . "&url='+encodeURIComponent(location.href))",
            '!sitename' => variable_get('site_name', 'Drupal'),
          )),
        );
        return $block;
      }
      break;
  }
}

/**
 * Implementation of hook_boot()
 */
function shurly_boot() {

  // if the path has any slashes in it, it's not a short URL
  // so we can bail out and save ourselves a database call
  if (isset($_GET['q']) && strpos($_GET['q'], '/') === FALSE) {
    $row = db_fetch_object(db_query("SELECT rid, destination FROM {shurly} WHERE source = '%s' AND active = 1", $_GET['q']));
    if ($row) {
      shurly_goto($row);
    }
  }
}

/**
 * Implementation of hook_theme()
 */
function shurly_theme($existing, $type, $theme, $path) {
  return array(
    'shurly_create_form' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_views_api.
 * Notifies the Views module that we're compatible with a particular API revision.
 */
function shurly_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'shurly') . '/views',
  );
}

/**
 * Access callback for deleting (deactivating) a URL
 */
function shurly_delete_access($rid) {
  if (is_numeric($rid)) {
    global $user;
    if (!$user->uid) {

      // anonymous users can't delete URLs
      return FALSE;
    }

    // see if there's a row
    $row = db_fetch_object(db_query('SELECT uid, source, destination FROM {shurly} WHERE rid = %d', $rid));

    // if there's a row, and either the user is an admin, or they've got permission to create and they own this URL, then let them access
    if ($row && (user_access('Administer short URLs') || user_access('Delete own URLs') && $row->uid == $user->uid)) {
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * Confirmation form to delete a link
 */
function shurly_confirm_delete_form(&$form_state, $rid) {
  $row = db_fetch_object(db_query('SELECT destination FROM {shurly} WHERE rid = %d', $rid));
  $form['rid'] = array(
    '#type' => 'value',
    '#value' => $rid,
  );

  // the 'destination' argument here is a bit of a hack...
  return confirm_form($form, t('Are you sure you want to delete and deactivate this URL?'), rawurldecode($_GET['destination']), t('You are about to deactivate the link which redirects to %url. Once this item is deleted, you will not be able to create another link with the same short URL.', array(
    '%url' => $row->destination,
  )));
}

/**
 * Submit handler for above form
 */
function shurly_confirm_delete_form_submit($form, &$form_state) {
  drupal_set_message(t('URL has been deactivated'));
  shurly_set_link_active($form_state['values']['rid'], 0);
}

/**
 * The main form to create new short URLs.
 */
function shurly_create_form($form_state) {
  global $base_url;
  $form['long_url'] = array(
    '#title' => t('Enter a long URL to make short'),
    '#type' => 'textfield',
    '#maxlength' => 255,
    '#default_value' => isset($form_state['storage']['shurly']['long_url']) ? $form_state['storage']['shurly']['long_url'] : (isset($_GET['url']) ? $_GET['url'] : 'http://'),
    '#attributes' => array(
      'tabindex' => 1,
    ),
  );
  $short_default = user_access('Enter custom URLs') ? isset($form_state['storage']['shurly']['short_url']) ? $form_state['storage']['shurly']['short_url'] : '' : '';
  $form['short_url'] = array(
    '#type' => 'textfield',
    '#size' => 6,
    '#field_prefix' => variable_get('shurly_base', $base_url) . '/',
    '#field_suffix' => ' <span class="shurly-choose">&lt;--- ' . t('create custom URL') . '</span>',
    '#default_value' => $short_default,
    '#access' => user_access('Enter custom URLs'),
    '#attributes' => array(
      'tabindex' => 2,
    ),
  );
  if (isset($form_state['storage']['shurly']['final_url'])) {
    $form['result'] = array(
      '#type' => 'textfield',
      '#size' => 30,
      '#value' => $form_state['storage']['shurly']['final_url'],
      '#prefix' => '<div class="shurly-result">',
      '#suffix' => '</div>',
      '#field_prefix' => t('Your short URL: '),
      '#field_suffix' => ' <div id="shurly-copy-container" style="position:relative;"><div id="shurly-copy">' . t('copy') . '</div></div>
      <div><a href="http://twitter.com?status=' . urlencode($form_state['storage']['shurly']['final_url']) . '">' . t('Create a Twitter message with this URL') . '</a></div>',
    );
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Shrink it!'),
    '#attributes' => array(
      'tabindex' => 3,
    ),
  );
  unset($form_state['storage']['shurly']);
  return $form;
}
function theme_shurly_create_form($form) {
  $path = drupal_get_path('module', 'shurly');
  drupal_add_css($path . '/shurly.css');
  drupal_add_js($path . '/zeroclipboard/ZeroClipboard.js');
  drupal_add_js($path . '/shurly.js');
  drupal_add_js("ZeroClipboard.setMoviePath( '" . base_path() . $path . '/zeroclipboard/ZeroClipboard.swf' . "' );", 'inline');
  $output = '';
  $output .= '<div class="container-inline">';
  $output .= drupal_render($form['long_url']);
  $output .= drupal_render($form['submit']);
  $output .= '</div>';
  $output .= drupal_render($form['short_url']);
  $output .= drupal_render($form);
  return $output;
}
function shurly_create_form_validate(&$form, &$form_state) {
  if (!user_access('Create short URLs')) {
    form_set_error('', t('You do not have permission to create short URLs on this site'));
    return;
  }
  $rate_limit = shurly_rate_limit_allowed();
  if (!$rate_limit['allowed']) {
    form_set_error('', t('Rate limit exceeded. You are limited to @rate requests per @time minute period.', array(
      '@rate' => $rate_limit['rate'],
      '@time' => $rate_limit['time'],
    )));
    return;
  }
  $form_state['values']['long_url'] = trim($form_state['values']['long_url']);
  $form_state['values']['short_url'] = trim($form_state['values']['short_url']);
  $vals = $form_state['values'];

  // check that they've entered a URL
  if ($vals['long_url'] == '' || $vals['long_url'] == 'http://' || $vals['long_url'] == 'https://') {
    form_set_error('long_url', t('Please enter a web URL'));
  }
  elseif (!shurly_validate_long($form_state['values']['long_url'], $form_state['values']['short_url'])) {
    shurly_errors_to_form();
  }
  if ($vals['short_url'] != '') {

    // a custom short URL has been entered
    $form_state['custom'] = TRUE;
    if (!shurly_validate_custom($vals['short_url'])) {
      form_set_error('short_url', t('Short URL contains unallowed characters'));
    }
    elseif ($exists = shurly_url_exists($vals['short_url'], $vals['long_url'])) {
      form_set_error('short_url', t('This short URL has already been used'));
    }
    elseif (!shurly_path_available($vals['short_url'])) {
      form_set_error('short_url', t('This custom URL is reserved. Please choose another.'));
    }
  }
  else {

    // custom short URL field is empty
    $form_state['custom'] = FALSE;
    if ($exist = shurly_get_latest_short($vals['long_url'], $GLOBALS['user']->uid)) {
      $short = $exist;

      // we flag this as URL Exists so that it displays but doesn't get saved to the db
      $form_state['url_exists'] = TRUE;
    }
    else {
      $short = shurly_next_url();
    }
    $form_state['values']['short_url'] = $short;
    $form_state['storage']['shurly']['short_url'] = $short;
  }
}
function shurly_create_form_submit($form, &$form_state) {
  global $base_url;

  // submit the short URL form
  $long_url = $form_state['storage']['shurly']['long_url'] = $form_state['values']['long_url'];
  $short_url = $form_state['storage']['shurly']['short_url'] = $form_state['values']['short_url'];
  $final_url = $form_state['storage']['shurly']['final_url'] = rawurldecode(_surl($short_url, array(
    'absolute' => TRUE,
    'base_url' => variable_get('shurly_base', $base_url),
  )));
  $custom = $form_state['custom'];
  if (empty($form_state['url_exists'])) {
    shurly_save_url($long_url, $short_url, NULL, $custom);
  }
}

/**
 * From http://www.php.net/manual/en/function.base-convert.php#52450
 *
 * Parameters:
 * $num - your decimal integer
 * $base - base to which you wish to convert $num (leave it 0 if you are providing $index or omit if you're using default (62))
 * $index - if you wish to use the default list of digits (0-1a-zA-Z), omit this option, otherwise provide a string (ex.: "zyxwvu")
 */
function shurly_dec2any($num, $base = 62, $index = FALSE) {
  if (!$base) {
    $base = strlen($index);
  }
  elseif (!$index) {

    // note: we could rearrange this string to get more random looking URLs
    // another note, to create printable URLs, omit the following characters: 01lIO
    $index = substr("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 0, $base);
  }
  $out = "";
  for ($t = floor(log10($num) / log10($base)); $t >= 0; $t--) {
    $a = floor($num / pow($base, $t));
    $out = $out . substr($index, $a, 1);
    $num = $num - $a * pow($base, $t);
  }
  return $out;
}

/**************************************************************
 * Backport of the flood controls from Drupal 7
 * these functions won't be needed in the D7 version of ShURLy
 **************************************************************/

/**
 * Implements hook_cron().
 */
function shurly_cron() {

  // Cleanup the flood.
  db_query('DELETE FROM {shurly_flood} WHERE expiration < %d', time());
}

/**
 * Function to store the flood event.
 */
function shurly_flood_register_event($name, $window = 3600, $identifier = NULL) {
  if (!isset($identifier)) {
    $identifier = ip_address();
  }
  db_query("INSERT INTO {shurly_flood} (event, identifier, timestamp, expiration) VALUES ('%s', '%s', %d, %d)", $name, ip_address(), time(), time() + $window);
}

/**
 * Function to check if the current user
 * is in the flood table.
 */
function shurly_flood_is_allowed($name, $threshold, $window = 3600, $identifier = NULL) {
  if (!isset($identifier)) {
    $identifier = ip_address();
  }
  $number = db_result(db_query("SELECT COUNT(*) FROM {shurly_flood} WHERE event = '%s' AND identifier = '%s' AND timestamp > %d", $name, $identifier, time() - $window));
  return $number < $threshold;
}

/******************************************************
 * API functions
 ******************************************************
 */

/**
 * API function to shorten a URL
 * @arg $long_url - the long URL to shorten
 * @arg $custom - optional custom short URL,
 *   user_access('enter custom URLs') should be checked prior to calling shurly_shorten()
 *
 * @return an array with the following keys
 *   'success' => TRUE or FALSE
 *   'error' => reason for for failure
 *   'long_url' => the long url
 *   'short_url' => the short url
 */
function shurly_shorten($long_url, $custom = NULL, $account = NULL) {
  global $base_url;
  $success = FALSE;
  $account = $account ? $account : $GLOBALS['user'];
  $error = '';
  $no_save = FALSE;
  $rate_limit = shurly_rate_limit_allowed($account);
  if (!$rate_limit['allowed']) {
    $error = t('Rate limit exceeded. You are limited to @rate requests per @time minute period.', array(
      '@rate' => $rate_limit['rate'],
      '@time' => $rate_limit['time'],
    ));
  }
  elseif (!shurly_validate_long($long_url)) {
    $error = implode(' ', shurly_get_errors());
  }
  elseif (is_null($custom)) {
    $latest = shurly_get_latest_short($long_url, $account->uid);
    if ($latest) {
      $no_save = TRUE;
      $success = TRUE;
      $short = $latest;
    }
    else {
      $short = shurly_next_url();
    }
  }
  else {
    $short = $custom;
    if (!shurly_validate_custom($short) || !shurly_path_available($short)) {
      $error .= $error ? ' ' : '';
      $error .= t('Invalid short URL.');
    }
    elseif (shurly_url_exists($short)) {
      $error .= $error ? ' ' : '';
      $error .= t('Existing short URL.');
    }
  }
  if (!$error && !$no_save) {
    if (shurly_save_url($long_url, $short, $account, $custom)) {
      $success = TRUE;
    }
    else {
      $error = t('Unknown database error.');
    }
  }
  return array(
    'success' => $success,
    'error' => $error,
    'longUrl' => $long_url,
    'shortUrl' => isset($short) ? _surl($short, array(
      'absolute' => TRUE,
      'base_url' => variable_get('shurly_base', $base_url),
    )) : '',
    'user' => (int) $account->uid,
  );
}

/**
 * Function to get the long url.
 */
function shurly_expand($short, $account = NULL) {
  $error = '';
  $success = FALSE;
  $account = $account ? $account : $GLOBALS['user'];
  $rate_limit = shurly_rate_limit_allowed($account);
  if (!$rate_limit['allowed']) {
    $error = t('Rate limit exceeded. You are limited to @rate requests per @time minute period.', array(
      '@rate' => $rate_limit['rate'],
      '@time' => $rate_limit['time'],
    ));
  }
  elseif ($redirect = shurly_get_redirect($short, TRUE)) {
    $success = TRUE;
    $long_url = $redirect->destination;
  }
  else {
    $error = t('Not found');
  }
  return array(
    'success' => $success,
    'error' => $error,
    'longUrl' => $long_url,
    'shortUrl' => _surl($short, array(
      'absolute' => TRUE,
    )),
    'user' => (int) $account->uid,
  );
}

/**
 * Check rate limit for this user
 * return an array in the following format
 * array(
 *  'allowed' => TRUE/FALSE
 *  'rate' => number of requests allowed
 *  'time' => period of time in minutes
 * )
 */
function shurly_rate_limit_allowed($account = NULL) {
  if (!isset($account)) {
    global $user;
    $account = $user;
  }
  $settings = variable_get('shurly_throttle', array());
  if (!empty($settings) && is_array($account->roles)) {
    $rids = array_keys($account->roles);
    $use_rid = array_shift($rids);

    // get list of roles with permission to create short URLs
    $creating_roles = user_roles(FALSE, 'Create short URLs');
    foreach ($account->roles as $rid => $name) {

      // check that this role has permission to create URLs, otherwise discard it
      if (array_key_exists($rid, $creating_roles)) {

        // find the lightest role... if roles are the same weight, use the next role
        $settings[$rid]['weight'] = isset($settings[$rid]['weight']) ? $settings[$rid]['weight'] : 0;
        $use_rid = $settings[$use_rid]['weight'] < $settings[$rid]['weight'] ? $use_rid : $rid;
      }
    }
  }
  if (!empty($settings) && is_numeric($settings[$use_rid]['rate']) && is_numeric($settings[$use_rid]['time'])) {

    // see if it's allowed
    $allowed = shurly_flood_is_allowed('shurly', $settings[$use_rid]['rate'], $settings[$use_rid]['time'] * 60);

    // increment the counter
    shurly_flood_register_event('shurly', $settings[$use_rid]['time'] * 60);
    $return = array(
      'allowed' => $allowed,
      'rate' => $settings[$use_rid]['rate'],
      'time' => $settings[$use_rid]['time'],
    );
  }
  else {

    // not set... don't do a flood check
    $return = array(
      'allowed' => TRUE,
    );
  }
  return $return;
}

/**
 * API function to save a URL
 * @arg $custom is a TRUE/FALSE 
 */
function shurly_save_url($long_url, $short_path, $account = NULL, $custom = NULL) {
  if (is_null($account)) {
    $account = $GLOBALS['user'];
  }
  $record = array();
  $record['destination'] = $long_url;
  $record['hash'] = md5($long_url);
  $record['custom'] = $custom ? 1 : 0;
  $record['created'] = time();
  $record['source'] = $short_path;
  $record['uid'] = $account->uid;
  $record['count'] = $record['last_used'] = 0;
  $record['active'] = 1;
  return drupal_write_record('shurly', $record);
}

/**
 * Activate or deactivate a link
 *  @arg $rid (int) the redirect id
 *  @arg $active (boolean) TRUE to make redirect active, FALSE to make it inactive
 */
function shurly_set_link_active($rid, $active) {
  $record = db_fetch_array(db_query('SELECT * FROM {shurly} WHERE rid = %d', $rid));
  if ($record) {
    $record['rid'] = $rid;
    $record['active'] = $active ? 1 : 0;
    return drupal_write_record('shurly', $record, 'rid');
  }
  else {
    return FALSE;
  }
}

/**
 * General function to validate long and short URLs
 * calls hook_shurly_validate(&$long, &$custom)
 */
function shurly_validate(&$long, &$custom = NULL) {
  $modules = module_implements('shurly_validate');
  $return = TRUE;
  foreach ($modules as $module) {
    $function = $module . '_shurly_validate';
    if (!$function($long, $custom)) {
      $return = FALSE;
      break;
    }
  }
  return $return;
}

/**
 * Validate custom short URL string
 *
 * @return TRUE if valid, FALSE if invalid
 */
function shurly_validate_custom($custom) {

  // check the length of the string
  if (strlen($custom) == 0) {
    return FALSE;
  }

  // disallow: #%&@*{}\:;<>?/+.,'"$|`^[] and space character
  return preg_match('/[\\/#%&\\@\\*\\{\\}\\:\\;<>\\?\\+ \\.\\,\'\\"\\$\\|`^\\[\\]]/u', $custom) ? FALSE : TRUE;
}

/**
 * Validate a long URL
 * 
 * Checks for:
 * - a valid URL
 * - it's not a link to an existing short URL
 * - it's not a link to itself
 *
 * @param
 * $long url - the long URL entered by user
 * $custom - custom short URL entered by user
 *
 * @return
 * BOOLEAN - TRUE if valid, FALSE if invalid
 */
function shurly_validate_long(&$long_url, $custom = NULL) {
  $return = TRUE;
  $match = FALSE;

  // if the person didn't remove the original http:// from the field, pull it out
  $long_url = preg_replace('!^http\\://(http\\://|https\\://)!i', '\\1', $long_url);
  $long_parse = parse_url($long_url);
  $base_parse = parse_url($GLOBALS['base_url']);
  $check_ip = variable_get('shurly_forbid_ips', FALSE);
  $check_localhost = variable_get('shurly_forbid_localhost', FALSE);
  $check_resolvability = variable_get('shurly_forbid_unresolvable_hosts', FALSE);
  $check_private_ip_ranges = variable_get('shurly_forbid_private_ips', FALSE);
  if ($long_parse === FALSE || !isset($long_parse['host'])) {

    // malformed URL
    // or no host in the URL
    shurly_set_error(t('Invalid URL.'), 'long_url');
    $return = FALSE;
  }
  elseif ($long_parse['scheme'] != 'http' && $long_parse['scheme'] != 'https') {
    shurly_set_error(t('Invalid URL. Only http:// and https:// URLs are allowed.'), 'long_url');
    $return = FALSE;
  }
  elseif ($check_ip && preg_match('/^\\d/', $long_parse['host'])) {

    // Host is given as IP address instead of a common hostname.
    $return = FALSE;

    // @todo Rework condition with respect to RFC 1123, which allows hostnames
    //       starting with a digit.
  }
  elseif ($check_localhost && shurly_host_is_local($long_parse['host'], TRUE)) {

    // Host seems to be the local host.
    $return = FALSE;
  }
  elseif ($check_resolvability && !shurly_host_is_resolveable($long_parse['host'], TRUE)) {

    // Host cannot be resolved (at least not by this server!).
    $return = FALSE;
  }
  elseif ($check_private_ip_ranges && shurly_host_is_private($long_parse['host'], TRUE)) {

    // Host refers to a private IP address.
    $return = FALSE;
  }
  else {
    if (variable_get('shurly_forbid_custom', FALSE)) {
      $custom_pattern = variable_get('shurly_custom_restriction', '');
      if (!empty($custom_pattern)) {
        if (preg_match($custom_pattern, $long_url)) {
          $return = FALSE;
        }
      }
    }
    $long_domain_parts = explode('.', $long_parse['host']);
    $base_domain_parts = explode('.', $base_parse['host']);

    // if last domain part of entered URL matches last part of this domain
    if (isset($base_domain_parts[count($long_domain_parts) - 1]) && $long_domain_parts[count($long_domain_parts) - 1] == $base_domain_parts[count($long_domain_parts) - 1]) {

      // and (if there's a 2nd to last)
      if (count($long_domain_parts) >= 2) {

        // check that 2nd to last matches
        if (isset($base_domain_parts[count($long_domain_parts) - 2]) && $long_domain_parts[count($long_domain_parts) - 2] == $base_domain_parts[count($long_domain_parts) - 2]) {

          // last 2 parts link to this domain
          $match = TRUE;
        }
      }
      else {

        // there's only one part, and it links here
        $match = TRUE;
      }

      // We only get down here if the long URL links to this domain
      // by the way, we're ignoring any subdomain...
      // so http://lbt.me/something and http://www.lbt.me/something are assumed to be the same
      if ($match) {

        // let's see if there's a $_GET['q'] in the long URL
        $query = $long_parse['query'];
        $query = html_entity_decode($query);
        $query_array = explode('&', $query);
        $queries = array();
        foreach ($query_array as $val) {
          $x = explode('=', $val);
          $queries[$x[0]] = $x[1];
        }
        if ($queries['q']) {

          // if there's a 'q' query, Drupal uses this instead of anything in the path
          $path = $queries['q'];
        }
        else {
          $path = $long_parse['path'];
        }

        // see if this is a link to an existing shortURL
        // remove the leading "/" from path, if it exists
        $path = explode('/', $path, 2);
        $path = array_pop($path);
        if ($path) {

          // get the base path of this Drupal install
          $base = explode('/', base_path(), 2);
          $base = array_pop($base);

          // remove the base from the path
          if ($base) {
            $path = preg_replace('!' . preg_quote($base, '!') . '!i', '', $path);
          }
          if (shurly_url_exists($path)) {

            // link to existing short URL
            shurly_set_error(t('Illegal URL'), 'long_url');
            $return = FALSE;
          }
          if ($custom && $custom == $path) {

            // they've created a link to itself
            shurly_set_error(t('Recursive URL'), 'short_url');
            $return = FALSE;
          }
        }
      }
    }
  }
  return $return;
}

/**
 * Generate a random short URL
 * Pretty much unused at this point
 * this method could take a LOOOONG time on a site with lots of URLs
 */
function shurly_generate_random($len = NULL) {
  if ($len == NULL) {
    $len = variable_get('shurly_length', 4);
  }
  $charset = "abcdefghijklmnopqrstuvwxyz123456789";
  $charlen = strlen($charset) - 1;
  do {
    $str = '';
    for ($i = 0; $i < $len; $i++) {
      $str .= $charset[mt_rand(0, $charlen)];
    }

    // check that this string hasn't been used already
    // check that the string is a valid (available) path
  } while (shurly_url_exists($str) || !shurly_path_available($str));
  return $str;
}

/**
 * Return next available short URL
 */
function shurly_next_url() {
  $count = variable_get('shurly_counter', 3249);

  // starts the URLs with 3 characters
  do {
    $count++;

    // counter is stored as base 10
    // $index is a-z, A-Z, 0-9, sorted randomly, with confusing characters (01lIO) removed - 57 characters
    // a custom index can be created as a variable override in settings.php
    $index = variable_get('shurly_index', 'kZ4oJ3Uwi5STqcpGNxfYgMQAdPWmsenh78XB26uLbEaRDzKrHVj9CyFtv');
    $str = shurly_dec2any($count, NULL, $index);

    // check that this string hasn't been used already
    // check that the string is a valid (available) path
  } while (shurly_url_exists($str) || !shurly_path_available($str));
  variable_set('shurly_counter', $count);
  return $str;
}

/**
 * Checks to see if there's a menu handler, path alias, or language prefix for a given path
 *
 * @return TRUE if there are no conflicts
 */
function shurly_path_available($path) {

  // check to see if path represents an enabled language
  $languages = language_list();
  if (array_key_exists($path, $languages)) {
    return FALSE;
  }
  $return = TRUE;

  // see if $path is an alias
  $source = drupal_lookup_path('source', $path);
  if ($source) {

    // if so, set alias source to $path
    $path = $source;
  }

  // check to see if $path has a menu callback
  if (menu_get_item($path)) {
    $return = FALSE;
  }
  return $return;
}

/**
 * Check to see if this short URL already exists
 */
function shurly_url_exists($short, $long = NULL) {
  $redirect = shurly_get_redirect($short);
  $return = FALSE;
  if ($redirect) {
    $return = 'found';
  }
  if ($long && $redirect->destination == $long) {
    $return = 'match';
  }
  return $return;
}

/**
 * Given the short URL, return the long one
 *  NOTE: Always check $redirect->active before using the result
 */
function shurly_get_redirect($short_url, $check_active = FALSE) {
  $query = "SELECT * FROM {shurly} WHERE source = '%s'";
  if ($check_active) {
    $query .= ' AND active = 1';
  }
  $redirect = db_fetch_object(db_query($query, $short_url));
  return $redirect;
}

/**
 * Get the latest generated short URL by a given user for a given long URL
 */
function shurly_get_latest_short($long, $uid) {
  $hash = md5($long);
  return db_result(db_query("SELECT source FROM {shurly} WHERE hash = '%s' AND uid = %d AND custom = 0 AND active = 1 ORDER BY rid DESC LIMIT 1", $hash, $uid));
}

/**
 * A heavily modified version of drupal_goto() (which hasn't been bootstrapped during hook_boot()
 */
function shurly_goto($row) {
  if (!$row || isset($_GET['redirect']) && $_GET['redirect'] == 'false') {
    return;
  }

  // Allow other modules to implement hook_shurly_redirect_before()
  // to add additional logging information to the database or perform other tasks
  // _before() is probably best to use for altering the $row->destination
  // Remember this is running during hook_boot(). Many Drupal functions are unavailable.
  module_invoke_all('shurly_redirect_before', $row);
  $url = $row->destination;

  // Remove newlines from the URL to avoid header injection attacks.
  $url = str_replace(array(
    "\n",
    "\r",
  ), '', $url);

  // We do not want this while running update.php.
  if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {

    // Allow modules to react to the end of the page request before redirecting.
    module_invoke_all('exit', $url);
  }

  // Even though session_write_close() is registered as a shutdown function, we
  // need all session data written to the database before redirecting.
  session_write_close();
  header('Location: ' . $url, TRUE, 301);

  // header has been sent... browser has been redirected
  // now we can do any expensive operations
  // update access information on this row
  db_query('UPDATE {shurly} SET count = count + 1, last_used = %d WHERE rid = %d', time(), $row->rid);

  // note: If possible, other modules should probably insert more information
  // in the database by using hook_db_rewrite_sql() on the above query
  // rather than creating a new db call
  // Allow other modules to implement hook_shurly_redirect_after()
  // _after() happens after the redirect has already been sent to browser.
  // It's probably best for slower operations like additional database logging
  // Remember this is running during hook_boot(). Many Drupal functions are unavailable.
  module_invoke_all('shurly_redirect_after', $row);

  // The "Location" header sends a redirect status code to the HTTP daemon. In
  // some cases this can be wrong, so we make sure none of the code below the
  // drupal_goto() call gets executed upon redirection.
  exit;
}

/**
 * Set an validation error
 * We don't call form_set_error() directly because we don't want to be 
 * calling drupal_set_message() during API calls
 */
function shurly_set_error($message = NULL, $field = NULL) {
  static $errors = array();
  if ($message) {
    $errors[] = array(
      'message' => $message,
      'field' => $field,
    );
  }
  return $errors;
}

/**
 * Get an array of errors (for delivering errors through web services)
 */
function shurly_get_errors() {
  $errors = shurly_set_error();
  $return = array();
  foreach ($errors as $error) {
    $return[] = $error['message'];
  }
  return $return;
}

/**
 * Set errors on form items (for use with form API)
 */
function shurly_errors_to_form() {
  $errors = shurly_set_error();
  foreach ($errors as $error) {
    form_set_error($error['field'], $error['message']);
  }
}

/**
 * Internal function to call url() without language prefixing or subdomain rewrites
 */
function _surl($path = NULL, $options = array()) {
  $options['language'] = _shurly_language_stub();
  return url($path, $options);
}

/**
 * Internal function to call l() without language prefixing or subdomain rewrites
 */
function _sl($text, $path, $options = array()) {
  $options['language'] = _shurly_language_stub();
  return l($text, $path, $options);
}

/**
 * Return default language object which will avoid redirects and subdomains
 * 
 * This is necessary because we always want our short URLs to be
 * the first item in the path, even if we've got another language enabled
 */
function _shurly_language_stub() {
  static $language;
  if (!isset($language)) {
    $language = language_default();
    $language->prefix = '';
    $language->domain = '';
  }
  return $language;
}

/**
 * Implementation of hook_filter().
 */
function shurly_filter($op, $delta = 0, $format = -1, $text = '') {
  switch ($op) {
    case 'list':
      return array(
        0 => t("Shorten all outgoing URL's"),
      );
    case 'description':
      return t('Shorten all outgoing URL\'s.');
    case 'settings':
      break;
    case 'no cache':
      break;
    case 'prepare':
      return $text;
    case 'process':
      return _shurly_filter_process($text);
    default:
      return $text;
  }
}

/**
 * Process callback for shurly filter.
 */
function _shurly_filter_process($text) {

  // Find all a tags containing a full URL.
  preg_match_all('/<a[^>]*href="(http[^"]*)"[^>]*>/i', $text, $links);
  if (!empty($links)) {
    $links = $links[1];
    foreach ($links as $key => $link) {
      $short_url = shurly_shorten($link);
      $text = str_replace('"' . $link . '"', '"' . $short_url['shortUrl'] . '"', $text);
    }
  }
  return $text;
}

/**
 * Implements hook_filter_tips().
 */
function shurly_filter_tips($delta, $filter, $format, $long = FALSE) {
  return t('All links starting with http or https will be replaced.');
}

/**
 * Wrapper function for PHP's `gethostbyname()`.
 *
 * This function should be used, when multiple encapsulated code parts need to
 * resolve a hostname.
 *
 * @staticvar array $resolved_hosts
 *   Array of `gethostbyname()` return values.
 *
 * @param string $hostname
 *   Hostname to resolve.
 *
 * @return string
 *   Resolved host address on success or the input $hostname on failure.
 */
function _shurly_gethostbyname($hostname) {
  static $resolved_hosts = array();
  if (!isset($resolved_hosts[$hostname])) {
    $resolved_hosts[$hostname] = gethostbyname($hostname);
  }
  return $resolved_hosts[$hostname];
}

/**
 * Check whether the given test string matches the pattern of an IP address.
 *
 * @param string $test_string
 *   Host address or whatever should be tested.
 *
 * @return bool
 *   TRUE if the $test_string matches an IP address pattern; otherwise FALSE.
 */
function _shurly_is_ip_address($test_string) {
  if (!!filter_var($test_string, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
    return TRUE;
  }
  if (!!filter_var($test_string, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Check whether the input $hostname can be resolved to a valid IP address.
 *
 * @param string $hostname
 *   Hostname to test.
 *
 * @return bool
 *   TRUE if the $hostname resolves to a valid IP address; otherwise FALSE.
 */
function shurly_host_is_resolveable($hostname) {
  if (_shurly_is_ip_address($hostname)) {
    return TRUE;
  }
  elseif (_shurly_is_ip_address(_shurly_gethostbyname($hostname))) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Check whether the given resolved host is the localhost.
 *
 * @param string $hostname
 *   Return value of a `gethostbyname()` call.
 *
 * @return bool
 *   TRUE if the resolved hostname matches an IPv4 or IPv6 localhost address;
 *   otherwise FLASE.
 */
function shurly_host_is_local($hostname) {
  $resolved_hostname = _shurly_gethostbyname($hostname);
  $local_ip_address_pattern = '/^127(?:\\.[0-9]+){0,2}\\.[0-9]+$|^\\[(?:0*\\:)*?:?0*1\\]$/';
  if (preg_match($local_ip_address_pattern, $resolved_hostname)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Check whether the given hostname matches a private IP address.
 *
 * @param string $hostname
 *   Hostname to check.
 *
 * @return bool
 *   TRUE if the given $hostname matches a private IP address; otherwise FALSE.
 */
function shurly_host_is_private($hostname) {
  $resolved_hostname = _shurly_gethostbyname($hostname);
  $private_ip_address_pattern = '/^(10\\.|172\\.(1[6-9]|2[0-9]|3[0-1])\\.|192\\.168\\.)/';
  if (preg_match($private_ip_address_pattern, $resolved_hostname)) {
    return TRUE;
  }
  return FALSE;
}

Functions

Namesort descending Description
shurly_block
shurly_boot Implementation of hook_boot()
shurly_confirm_delete_form Confirmation form to delete a link
shurly_confirm_delete_form_submit Submit handler for above form
shurly_create_form The main form to create new short URLs.
shurly_create_form_submit
shurly_create_form_validate
shurly_cron Implements hook_cron().
shurly_dec2any From http://www.php.net/manual/en/function.base-convert.php#52450
shurly_delete_access Access callback for deleting (deactivating) a URL
shurly_errors_to_form Set errors on form items (for use with form API)
shurly_expand Function to get the long url.
shurly_filter Implementation of hook_filter().
shurly_filter_tips Implements hook_filter_tips().
shurly_flood_is_allowed Function to check if the current user is in the flood table.
shurly_flood_register_event Function to store the flood event.
shurly_generate_random Generate a random short URL Pretty much unused at this point this method could take a LOOOONG time on a site with lots of URLs
shurly_get_errors Get an array of errors (for delivering errors through web services)
shurly_get_latest_short Get the latest generated short URL by a given user for a given long URL
shurly_get_redirect Given the short URL, return the long one NOTE: Always check $redirect->active before using the result
shurly_goto A heavily modified version of drupal_goto() (which hasn't been bootstrapped during hook_boot()
shurly_help Implementation of hook_help().
shurly_host_is_local Check whether the given resolved host is the localhost.
shurly_host_is_private Check whether the given hostname matches a private IP address.
shurly_host_is_resolveable Check whether the input $hostname can be resolved to a valid IP address.
shurly_menu Implementation of hook_menu()
shurly_next_url Return next available short URL
shurly_path_available Checks to see if there's a menu handler, path alias, or language prefix for a given path
shurly_perm Implementation of hook_perm()
shurly_rate_limit_allowed Check rate limit for this user return an array in the following format array( 'allowed' => TRUE/FALSE 'rate' => number of requests allowed 'time' => period of time in minutes )
shurly_save_url API function to save a URL @arg $custom is a TRUE/FALSE
shurly_set_error Set an validation error We don't call form_set_error() directly because we don't want to be calling drupal_set_message() during API calls
shurly_set_link_active Activate or deactivate a link @arg $rid (int) the redirect id @arg $active (boolean) TRUE to make redirect active, FALSE to make it inactive
shurly_shorten API function to shorten a URL @arg $long_url - the long URL to shorten @arg $custom - optional custom short URL, user_access('enter custom URLs') should be checked prior to calling shurly_shorten()
shurly_theme Implementation of hook_theme()
shurly_url_exists Check to see if this short URL already exists
shurly_validate General function to validate long and short URLs calls hook_shurly_validate(&$long, &$custom)
shurly_validate_custom Validate custom short URL string
shurly_validate_long Validate a long URL
shurly_views_api * Implementation of hook_views_api. * Notifies the Views module that we're compatible with a particular API revision.
theme_shurly_create_form
_shurly_filter_process Process callback for shurly filter.
_shurly_gethostbyname Wrapper function for PHP's `gethostbyname()`.
_shurly_is_ip_address Check whether the given test string matches the pattern of an IP address.
_shurly_language_stub Return default language object which will avoid redirects and subdomains
_sl Internal function to call l() without language prefixing or subdomain rewrites
_surl Internal function to call url() without language prefixing or subdomain rewrites