shurly.module in ShURLy 6
Same filename and directory in other branches
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.moduleView 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"><--- ' . 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
Name | 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 |