array( 'arguments' => array('form' => NULL), ), ); } /** * Implementation of hook_menu(). */ function purl_menu() { $items = array(); $items['admin/settings/purl'] = array( 'type' => MENU_NORMAL_ITEM, 'title' => t('Persistent URL'), 'description' => t('Settings for persistent url.'), 'page callback' => 'drupal_get_form', 'page arguments' => array('purl_settings_form'), 'access callback' => 'user_access', 'access arguments' => array('administer site configuration'), 'weight' => 10, ); $items['admin/settings/purl/settings'] = array( 'type' => MENU_DEFAULT_LOCAL_TASK, 'title' => t('Settings'), 'page callback' => 'drupal_get_form', 'page arguments' => array('purl_settings_form'), 'access callback' => 'user_access', 'access arguments' => array('administer site configuration'), 'weight' => 0, ); $items['admin/settings/purl/list'] = array( 'type' => MENU_LOCAL_TASK, 'title' => t('Modifiers'), 'page callback' => 'purl_admin', 'access callback' => 'user_access', 'access arguments' => array('administer site configuration'), 'weight' => 10, ); return $items; } /** * Implementation of hook_init() * Checks for any valid persistent urls in request string and fire callback appropriately */ function purl_init() { static $once; if (!$once) { _purl_init(PURL_PATH); _purl_init(PURL_DOMAIN); _purl_init(PURL_PAIR); $once = true; } } /** * Helper function to initialize. */ function _purl_init($method = PURL_PATH) { switch ($method) { case PURL_PATH: $q = isset($_REQUEST["q"]) ? trim($_REQUEST["q"], "/") : ''; $parsed = purl_parse(PURL_PATH, $q); break; case PURL_PAIR: $q = isset($_REQUEST["q"]) ? trim($_REQUEST["q"], "/") : ''; $parsed = purl_parse(PURL_PAIR, $q); break; case PURL_DOMAIN: $host = $_SERVER['HTTP_HOST']; // We handle sub.domain.com, and nothing more (no sub1.sub2.domain.com). $q = str_replace('http://','',$host); $parsed = purl_parse(PURL_DOMAIN, $q); break; } // Initialize a few things so that we can use them without warnings. if (!isset($_GET['q'])) { $_GET['q'] = ''; } if (!isset($_REQUEST['q'])) { $_REQUEST['q'] = ''; } // if $_GET and $_REQUEST are different, the path has NOT been // aliased. We may need to rewrite the path. if (in_array($method, array(PURL_PATH, PURL_PAIR)) && ($_GET['q'] == trim($_REQUEST['q'], '/'))) { $q = purl_remove_modifiers($q, $method); // there is nothing beyond the path value -- treat as frontpage if ($q == '') { $_GET['q'] = variable_get('site_frontpage', 'node'); } // pass the rest of the path onto Drupal cleanly else { $_REQUEST['q'] = $_GET['q'] = _purl_get_normal_path($q); } } if (is_array($parsed)) { foreach ($parsed as $value => $info) { purl_set($method, $value, $info); } } } /** * Check that no one else has implemented the custom_url_rewrite function -- * if available, use PURL. */ if (!function_exists('custom_url_rewrite_outbound')) { function custom_url_rewrite_outbound(&$path, &$options, $original) { return purl_url_rewrite($path, $options, $original); } } /** * Rewrites path with the current modifier and removes the modifier if searching for source path. */ function purl_url_rewrite(&$path, &$options, $original) { $working_path = $path; // preserve original path /** * TODO: we need to abstract this base_url dissection into a * handler, and in there, we'll abstract out for * protocol handling, and handling the site's base_url like www. * * TODO: we need to make sure that other valued contexts don't * get dropped. e.g. if you are on 'en/node/43' and you * purl_goto('spaces', 'mygroup'), you should end up * at 'en/mygroup', not 'mygroup' * */ // Check to see whether url rewriting has been disabled. if (empty($options['purl']['disabled']) || !purl_disable()) { $args = $active_path_values = array(); // The current url has requested a specific PURL modifier use it only if (!empty($options['purl']['provider']) && !empty($options['purl']['id'])) { $method = variable_get('purl_method_'. $options['purl']['provider'], PURL_PATH); $modifiers = purl_modifiers($method); switch ($method) { case PURL_PATH: foreach ($modifiers as $key => $modifier) { if ($modifier['id'] == $options['purl']['id'] && $modifier['provider'] == $options['purl']['provider']) { $active_path_values[] = $key; break; } } break; case PURL_PAIR: // @TODO: this ($values[0]) is really sketchy -- does it work? $modifiers = array_keys($modifiers); $active_path_values[] = "{$modifiers[0]}/{$options['purl']['id']}"; break; case PURL_DOMAIN: foreach ($modifiers as $key => $info) { if ($modifier['id'] == $options['purl']['id'] && $modifier['provider'] == $options['purl']['provider']) { $options['absolute'] = TRUE; $active_path_values = array(); $path = "http://{$key}/{$path}"; break; } } break; } } else { // Retrieve the path values for the current page that were // "stripped out" and write them back into url paths. foreach (purl_get(PURL_PAIR) as $modifier) { $active_path_values[] = "{$modifier['value']}/{$modifier['id']}"; } foreach (purl_get(PURL_PATH) as $modifier) { $active_path_values[] = $modifier['value']; } } if (count($active_path_values)) { $parsed = purl_parse(PURL_PATH, $working_path) + purl_parse(PURL_PAIR, $working_path); // A "normal" url was requested -- value the path if (!$options['alias'] && !strpos($path, '://') && !count($parsed) && count($active_path_values)) { $args = $args + $active_path_values; } } if (!empty($working_path)) { $args[] = $working_path; } $path = is_array($args) ? implode('/', $args) : ''; } else { // Handle domains -- need to force domain onto the path and push through as absolute url $options['absolute'] = TRUE; if ($path == '') { $path = variable_get('site_frontpage', 'node'); } if ($domain = variable_get('purl_base_domain', '')) { $path = $domain .'/'. $path; // REPLACE BASE_URL with the hub domain. } } } /** * Queries the database & modules for valid values based on modifing method. * * Modules that wish to provide in-code values should implement the * hook_purl_modifiers(). Which should return an array of values by * by provider. * * For example: * * return array( * 'my_module => array( * array('value' => 'foo', 'id' => 1), * array('value' => 'bar', 'id' => 2), * ), * ); */ function purl_modifiers($requested_method = PURL_PATH) { static $values; if (!isset($values)) { $values = array(); // Invoke purl_modifiers() and gather all values // provided "in code" (or stored by their respective modules) $providers = module_invoke_all('purl_modifiers'); foreach ($providers as $provider => $items) { // Store providers for use when retrieving db values $method = variable_get('purl_method_'. $provider, PURL_PATH); // If using a value pair we don't need to cache the valid values. if ($method == PURL_PAIR) { $value = variable_get('purl_method_'. $provider .'_key', false); if ($value != false) { $values[$method][$value] = array( 'provider' => $provider, 'id' => null, ); } } else { foreach ($items as $item) { if ($item['value'] && $item['id']) { $values[$method][$item['value']] = array( 'provider' => $provider, 'id' => $item['id'], ); } } } } // Gather database values -- we exclude providers that we have // already collected values for through code. $providers = array_diff_key(purl_providers(), $providers); foreach ($providers as $provider => $info) { $method = variable_get('purl_method_'. $provider, PURL_PATH); // Don't load all data base values for keyed pairs. if ($method == PURL_PAIR) { $value = variable_get('purl_method_'. $provider .'_key', false); if ($value != false) { $values[$method][$value] = array( 'provider' => $provider, 'id' => null, ); } } else { $result = db_query("SELECT * FROM {purl} WHERE provider = '%s'", $provider); while ($row = db_fetch_object($result)) { $values[$method][$row->value] = array( 'provider' => $row->provider, 'id' => $row->id, ); } } } } return (isset($values[$requested_method]) ? $values[$requested_method] : array()); } /** * Parses a query string of various types (url, domain, etc.) and * returns an array of any found values and their respective * providers/id values. */ function purl_parse($method = PURL_PATH, $q) { static $cache; if (!isset($cache)) { $cache = new purl_cache(); } if ($cache->get($method, $q) === false) { $valid_values = purl_modifiers($method); // Parse the provided query string and provide an array of any values found switch ($method) { case PURL_PATH: case PURL_PAIR: $parsed = array(); $args = explode('/', $q); $arg = $args[0]; while (isset($valid_values[$arg])) { $parsed[$arg] = $valid_values[$arg]; array_shift($args); if ($method == PURL_PAIR) { $parsed[$arg]['id'] = array_shift($args); } $arg = $args[0]; if (in_array($arg, $parsed)) { break; } } $cache->add($method, array($q => $parsed)); break; case PURL_DOMAIN: $parsed = array(); if (isset($valid_values[$q])) { $parsed[$q] = $valid_values[$q]; } $cache->add($method, array($q => $parsed)); break; } } return $cache->get($method, $q); } /** * Removes any modifiers from a query string. For path values only. */ function purl_remove_modifiers($q, $method, $providers = array()) { $parsed = purl_parse($method, $q); if (is_array($providers) && count($providers)) { foreach ($parsed as $value => $info) { if (!in_array($info['provider'], $providers)) { unset($parsed[$value]); } } } $parsed = array_keys($parsed); $args = explode('/', $q); switch ($method) { case PURL_PATH: // Remove the first occurrence of each value value from the query string foreach ($parsed as $value) { $k = array_search($value, $args); if ($k !== FALSE) { unset($args[$k]); } } break; case PURL_PAIR: foreach ($parsed as $v) { array_splice($args, array_search($v, $args), 2); } break; } return implode('/', $args); } /** * Invokes hook_purl_provider() to gather all providers. * * Modules that implement hook_purl_provider need to return an * array of value definitions. Each definition should have the following * keys: * - name * - description * - callback * - example * * See the spaces module for an usage example. */ function purl_providers($by_method = FALSE) { static $providers; if (!is_array($providers)) { $providers = array(); $providers = module_invoke_all('purl_provider'); } if ($by_method) { static $methods; if (!isset($methods)) { $methods = new purl_cache(); foreach ($providers AS $id => $provider) { $methods->add(variable_get('purl_method_'. $id, PURL_PATH), array($id => $provider)); } } return $methods->get(); } else { return $providers; } } /** * Taken from i18n */ function _purl_get_normal_path($path) { // If bootstrap, drupal_lookup_path is not defined if (!function_exists('drupal_get_headers')) { return $path; } // Check alias without lang elseif ($alias = drupal_lookup_path('source', $path)) { return $alias; } else { return $path; } } /** * Static cache function for setting + storing any valued modifiers * that are present on this page's request. */ function _purl_set($op = 'set', $type = PURL_PATH, $value = '', $info = array()) { static $used; if (!$used) { $used = new purl_cache(); } switch ($op) { case 'set': // Store value for url rewriting later on in the stack $info['value'] = $value; $used->add($type, $info, false); // Fire the provider callback if ($info['provider'] && $info['id']) { $providers = purl_providers(); $callback = $providers[$info['provider']]['callback']; $args = isset($providers[$info['provider']]['callback arguments']) ? $providers[$info['provider']]['callback arguments'] : array(); $args[] = $info['id']; if (function_exists($callback)) { call_user_func_array($callback, $args); } } break; case 'get': if ($type === 'all') { return $used->get(); } else { return $used->get($type); } } } /** * Set wrapper for _purl_set() */ function purl_set($type = PURL_PATH, $value = '', $info = array()) { return _purl_set('set', $type, $value, $info); } /** * Get wrapper for _purl_set() */ function purl_get($type = PURL_PATH) { return _purl_set('get', $type); } /** * PAGE CALLBACKS ===================================================== */ /** * Page callback for the purl administration page. */ function purl_admin() { global $pager_page_array, $pager_total, $pager_total_items; $page = isset($_GET['page']) ? $_GET['page'] : 0; $element = 0; $limit = 20; $providers = purl_providers(); // Convert $page to an array, used by other functions. $pager_page_array = array($page); $methods = _purl_options(); $merged = array(); foreach(array_keys($methods) as $method) { foreach(purl_modifiers($method) as $value => $info) { $info['value'] = $value; $merged[] = $info; } } $rows = array(); for ($i = $page * $limit; $i < ($page+1) * $limit && $i < count($merged); $i++) { $rows[] = array( $providers[$merged[$i]['provider']]['name'], $merged[$i]['value'], $merged[$i]['id'], $methods[variable_get('purl_method_'. $merged[$i]['provider'], PURL_PATH)], ); } // We calculate the total of pages as ceil(items / limit). $pager_total_items[$element] = count($merged); $pager_total[$element] = ceil($pager_total_items[$element] / $limit); $pager_page_array[$element] = max(0, min((int)$pager_page_array[$element], ((int)$pager_total[$element]) - 1)); if ($rows) { $output = theme('table', array(t('Provider'), t('Modifier'), t('ID'), t('Method')), $rows); $output .= theme('pager'); } else { $output = "

". t('No persistent urls have been registered.') ."

"; } return $output; } /** * Settings form for choosing the operating mode of purl */ function purl_settings_form() { global $base_url; $form = array(); $options = _purl_options(); foreach (purl_providers() as $id => $provider) { // Check to see whether provider has limited the available valueing methods if (isset($provider['methods']) && count($provider['methods'])) { $provider_options = array(); foreach ($provider['methods'] as $method) { $provider_options[$method] = $options[$method]; } } else { $provider_options = $options; } $form[$id] = array( '#fieldset' => true, '#provider' => true, '#title' => $provider['name'], '#description' => $provider['description'], ); $form[$id]['purl_method_'. $id] = array( '#title' => t('Method'), '#type' => 'select', '#options' => $provider_options, '#default_value' => variable_get('purl_method_'. $id, PURL_PATH), ); $form[$id]['purl_method_'. $id .'_key'] = array( '#title' => t('Key'), '#type' => 'textfield', '#size' => 12, '#default_value' => variable_get('purl_method_'. $id .'_key', ''), ); } $form['purl_location'] = array( '#type' => 'fieldset', // '#title' => t(''), ); $form['purl_location']['purl_base_domain'] = array( '#type' => 'textfield', '#title' => t('Default domain'), '#description' => t('Enter the default domain if you are using domain modifiers.'), '#required' => FALSE, '#default_value' => variable_get('purl_base_domain', $base_url), ); $form = system_settings_form($form); $form['#theme'] = 'purl_settings_form'; return $form; } /** * Theme function for purl_settings_form() */ function theme_purl_settings_form($form) { $output = ''; $rows = array(); foreach (element_children($form) as $id) { $row = array(); if (isset($form[$id]['#provider'])) { $name = $form[$id]['#title']; $description = $form[$id]['#description']; unset($form[$id]['#title']); unset($form[$id]['#description']); $row[] = "$name
$description
"; $cell = ''; foreach (element_children($form[$id]) as $item) { unset($form[$id][$item]['#title']); $cell .= drupal_render($form[$id][$item]); } $row[] = $cell; } $rows[] = $row; } $output .= theme('table', array(t('Provider'), t('Modifier type')), $rows); $output .= drupal_render($form); drupal_add_js(drupal_get_path("module", "purl") ."/purl.admin.js"); return $output; } /** * API Functions ====================================================== */ /** * Load a modifier from the database by provider or value. */ function purl_load($modifier) { if (isset($modifier['provider'])) { if ($modifier['id']) { $modifier = db_fetch_array(db_query("SELECT * FROM {purl} WHERE id = '%s' AND provider = '%s'", $modifier['id'], $modifier['provider'])); if ($modifier) { return $modifier; } } else if ($modifier['value']) { $modifier = db_fetch_array(db_query("SELECT * FROM {purl} WHERE value = '%s' AND provider = '%s'", $modifier['value'], $modifier['provider'])); if ($modifier) { return $modifier; } } } return false; } /** * Validation for modifiers. */ function purl_validate($modifier) { // Check that the value is valid if (check_plain($modifier['provider']) && !empty($modifier['value']) && preg_match('!^[a-z0-9_-]+$!', $modifier['value']) && !menu_get_item($modifier['value'])) { $id = db_result(db_query("SELECT id FROM {purl} WHERE value = '%s'", $modifier['value'])); if (!$id) { return true; } else if (isset($modifier['id']) && ($id == $modifier['id'])) { return true; } } return false; } /** * Save modifier to database. Will insert new entry if no ID is provided and update an existing one otherwise. */ function purl_save($modifier) { if (purl_validate($modifier)) { $id = db_result(db_query("SELECT id FROM {purl} WHERE id = '%s'", $modifier['id'])); if (!empty($id)) { $status = drupal_write_record('purl', $modifier, array('provider', 'id')); } else { $status = drupal_write_record('purl', $modifier); } return $status; } return false; } /** * Delete a modifier entry from the database. */ function purl_delete($modifier) { if ($modifier['value']) { $param = 'value'; $where = $modifier['value']; } else if ($modifier['id']) { $param = 'id'; $where = $modifier['id']; } $check = db_result(db_query("SELECT id FROM {purl} WHERE provider = '%s' AND $param = '%s'", $modifier['provider'], $where)); if ($check) { $status = db_query("DELETE FROM {purl} WHERE provider = '%s' AND $param = '%s'", $modifier['provider'], $where); return $status; } return false; } /** * An alternative implementation of drupal_goto() that allows PURL modifiers to * be added or removed from the destination URL. You provide a drupal path ('node/43') * and a persistent url modifier (provider/id pair) and purl_goto will determine the * correct location to use. The 'disable' flag may also be used to drop any * purl modifiers from the redirect. * * The code below is nearly identical to drupal_goto(), except that it passes an * $options array to url(). */ function purl_goto($path = '', $options = array(), $http_response_code = 302) { $options = !is_array($options) ? array() : $options; $options['absolute'] = TRUE; if (isset($_REQUEST['destination'])) { extract(parse_url(urldecode($_REQUEST['destination']))); } else if (isset($_REQUEST['edit']['destination'])) { extract(parse_url(urldecode($_REQUEST['edit']['destination']))); } $url = url($path, $options); // Remove newlines from the URL to avoid header injection attacks. $url = str_replace(array("\n", "\r"), '', $url); // Allow modules to react to the end of the page request before redirecting. // We do not want this while running update.php. if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { 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, $http_response_code); // 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(); } /** * Returns whether the current l/url call should use context rewriting or not */ function purl_disable($set = FALSE) { static $drop; if (!isset($drop)) { $drop = FALSE; } if ($set) { $drop = TRUE; } return $drop; } /** * Generates a persistent url form element that can be dropped into a * FormAPI form array. Includes validation, but nsert/update must be * handled by the implementing submit handler. */ function purl_form($provider, $id, $value = '') { switch (variable_get('purl_method_'. $provider, PURL_PATH)) { case PURL_PATH: case PURL_PAIR: $description = t('Choose a value path. May contain only lowercase letters, numbers, dashes and underscores. e.g. "my-value"'); break; case PURL_SUBDOMAIN: $description = t('Enter a domain registered for this context, such as "mygroup". Do not include http://'); break; case PURL_DOMAIN: $description = t('Enter a domain registered for this context, such as "www.example.com". Do not include http://'); break; } $form = array( '#tree' => TRUE, '#element_validate' => array('purl_form_validate'), ); $form['value'] = array( '#title' => t('Path value'), '#type' => 'textfield', '#description' => $description, '#maxlength' => 255, '#required' => true, '#default_value' => $value, ); $form['provider'] = array( '#type' => 'value', '#value' => $provider, ); $form['id'] = array( '#type' => 'value', '#value' => $id, ); return $form; } /** * Validation handler for purl_form(). */ function purl_form_validate($form) { $modifier = array( 'provider' => $form['provider']['#value'], 'value' => $form['value']['#value'], 'id' => $form['id']['#value'], ); if (!purl_validate($modifier)) { form_set_error($form['#parents'][0], t('There was an error registering the value "@value". It is either invalid or is already taken. Please choose another.', array('@value' => $form['value']['#value']))); return false; } else { return true; } } /** * Specialized cache for storing modifier information. */ class purl_cache { protected $cache = array(); function __construct() { $this->cache[PURL_PATH] = array(); $this->cache[PURL_PAIR] = array(); $this->cache[PURL_SUBDOMAIN] = array(); $this->cache[PURL_DOMAIN] = array(); } /** * @param $method * The method to add to the cache for * @param $item * Either a integer|string, or keyed array to add * @param $merge * Preserve keys and merge into cache for method. */ public function add($method, $item, $merge = true) { if (is_array($item) && $merge) { // Need to preserve keys so we use the '+' array operator. $this->cache[$method] = $this->cache[$method] + $item; } else { $this->cache[$method][] = $item; } } /** * @param $method * The method to retrieve from the cache for. * @param $item * Optionally and key of the required info. * * @return the desired info or false if an id doesn't exist. */ public function get($method = false, $id = false) { if ($method !== false && $id !== false) { return (isset($this->cache[$method][$id]) ? $this->cache[$method][$id] : false); } elseif ($method !== false) { return $this->cache[$method]; } else { return $this->cache; } } } /** * Helper function, returns form options for modifier types. */ function _purl_options() { return array( PURL_PATH => t('Path'), PURL_PAIR => t('Keyed pair'), PURL_DOMAIN => t('Full domain'), // PURL_SUBDOMAIN => t('Subdomain'), ); }