Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.269 diff -u -p -r1.269 bootstrap.inc --- includes/bootstrap.inc 31 Jan 2009 16:50:56 -0000 1.269 +++ includes/bootstrap.inc 9 Feb 2009 02:08:22 -0000 @@ -1615,3 +1615,118 @@ function registry_rebuild() { /** * @} End of "ingroup registry". */ + +/** + * @ingroup handlers + * @{ + */ + +/** + * Request a handler object. + * + * @param $slot_id + * The internal ID of the slot for which we want to retrieve a handler. + * @param $target + * The target for which we want the attached handler. + * @return + * The associated handler object. + */ +function handler($slot_id, $target = 'default') { + static $handlers; + + if (empty($handlers[$slot_id][$target])) { + // Try to get the associated handler. If the first query finds an associated + // handler, we use that. If not, the second query will always find the + // default handler. By UNIONing them together we get the fallback default + // behavior without having to issue a second request to the database. + $record = db_query_range("SELECT class, reuse FROM {handler_attachments} WHERE slot = :slot_1 AND target = :target + UNION SELECT class, reuse FROM {handler_attachments} WHERE slot = :slot_2 AND target = 'default'", array( + ':slot_1' => $slot_id, + ':slot_2' => $slot_id, + ':target' => $target, + ), 0, 1)->fetchObject(); + + // It's possible that this function will be called before the handler system + // has first been initialized. That's especially the case for early-running + // systems such as cache or path, as they operate during the bootstrap phase. + // If we have no record at all, we first rebuild the registry and then try + // again. That should at least always give us the slot-defined default + // and allow the system to proceed. + if (!$record) { + registry_rebuild(); + handlers_rebuild(); + $record = db_query_range("SELECT class, reuse FROM {handler_attachments} WHERE slot = :slot_1 AND target = :target + UNION SELECT class, reuse FROM {handler_attachments} WHERE slot = :slot_2 AND target = 'default'", array( + ':slot_1' => $slot_id, + ':slot_2' => $slot_id, + ':target' => $target, + ), 0, 1)->fetchObject(); + } + + // Create the handler. + $handler = new $record->class($target); + + // Cache the handler for later requests only if this slot is defined to do so. + if ($record->reuse) { + $handlers[$slot_id][$target] = $handler; + } + } + else { + $handler = $handlers[$slot_id][$target]; + } + + return $handler; +} + +/** + * Rebuild the handler and slot registries. + */ +function handlers_rebuild() { + require_once DRUPAL_ROOT . '/includes/handlers.inc'; + handler_slot_build(); + handler_handler_build(); + handler_ensure_defaults(); +} + +/** + * Interface for all Handler classes for any slot. + * + * This is a very thin interface, but does serve to help standardize handler + * behavior and a way to type-check a handler object. Interfaces for specific + * slots should extend this interface. + */ +interface HandlerInterface { + + /** + * Constructor + * + * @param $target + * The target for which this handler is being called. Some handlers may + * require this information in order to route commands properly. + */ + function __construct($target = 'default'); +} + +/** + * Base implementation of a handler object. + * + * Simple handler objects may choose to inherit from a base class in order to + * not reimplement routine functionality. Alternatively they may simply implement + * the appropriate interface and implement their own version of common + * functionality. Both methods are acceptable depending on the use case. + */ +abstract class HandlerBase implements HandlerInterface { + + /** + * The target for which this handler is active. + */ + protected $target; + + function __construct($target = 'default') { + $this->target = $target; + } +} + +/** + * @} End of "ingroup handlers". + */ Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.864 diff -u -p -r1.864 common.inc --- includes/common.inc 5 Feb 2009 01:21:16 -0000 1.864 +++ includes/common.inc 9 Feb 2009 02:08:22 -0000 @@ -151,7 +151,7 @@ function drupal_get_html_head() { * Reset the static variable which holds the aliases mapped for this request. */ function drupal_clear_path_cache() { - drupal_lookup_path('wipe'); + handler('path_lookup')->clearCache(); } /** @@ -3254,7 +3254,7 @@ function drupal_render_page($page) { * * Recursively iterates over each of the array elements, generating HTML code. * - * HTML generation is controlled by two properties containing theme functions, + * HTML generation is controlled by two properties containing theme functions, * #theme and #theme_wrapper. * * #theme is the theme function called first. If it is set and the element has any @@ -3265,13 +3265,13 @@ function drupal_render_page($page) { * * The theme function in #theme_wrapper will be called after #theme has run. It * can be used to add further markup around the rendered children, e.g. fieldsets - * add the required markup for a fieldset around their rendered child elements. + * add the required markup for a fieldset around their rendered child elements. * A wrapper theme function always has to include the element's #children property - * in its output, as this contains the rendered children. + * in its output, as this contains the rendered children. * * For example, for the form element type, by default only the #theme_wrapper * property is set, which adds the form markup around the rendered child elements - * of the form. This allows you to set the #theme property on a specific form to + * of the form. This allows you to set the #theme property on a specific form to * a custom theme function, giving you complete control over the placement of the * form's children while not at all having to deal with the form markup itself. * @@ -3305,7 +3305,7 @@ function drupal_render(&$elements) { else { $elements += element_basic_defaults(); } - + // If #markup is not empty and no theme function is set, use theme_markup. // This allows to specify just #markup on an element without setting the #type. if (!empty($elements['#markup']) && empty($elements['#theme'])) { @@ -3479,7 +3479,7 @@ function element_child($key) { function element_children($element) { $keys = array(); foreach(array_keys($element) as $key) { - if ($key[0] !== '#') { + if ($key[0] !== '#') { $keys[] = $key; } } @@ -4161,6 +4161,7 @@ function drupal_flush_all_caches() { _drupal_flush_css_js(); registry_rebuild(); + handlers_rebuild(); drupal_clear_css_cache(); drupal_clear_js_cache(); @@ -4203,3 +4204,107 @@ function _drupal_flush_css_js() { } variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19)); } + +/** + * @ingroup handlers + * @{ + */ + +/** + * Associate a given handler to a give slot for the specified target. + * + * @param $slot_id + * The internal ID of the slot. + * @param $handler_id + * The internal ID of the hand + * @param $target + * The target for which to associate this handler to this slot. + */ +function handler_attach($slot_id, $handler_id, $target = 'default') { + + // Lazy-load the utility function. + drupal_function_exists('handler_load'); + + $handler = handler_load($slot_id, $handler_id); + $slot = slot_load($slot_id); + + if ($handler) { + // We store the class in the database rather than the handler ID so that + // we can, on lookup, get back the class to instantiate and immediately + // do so. + db_merge('handler_attachments') + ->key(array( + 'slot' => $slot_id, + 'target' => $target, + )) + ->fields(array( + 'handler' => $handler->handler, + 'class' => $handler->class, + 'reuse' => (int)$slot->reuse, + )) + ->execute(); + } +} + +/** + * Revert a given slot and target to the default handler. + * + * @param $slot_id + * The slot to reset to the default handler. + * @param $target + * The target to reset to the default handler. + */ +function handler_detach($slot_id, $target = 'default') { + + // Delete the handler attachment. + db_delete('handler_attachments') + ->condition('slot', $slot_id) + ->condition('target', $target) + ->execute(); + + // If we just deleted the default target for this slot, reattach the + // slot-defined default handler. That ensures that we always have at least + // one handler configured. + if ($target = 'default') { + if (drupal_function_exists('slot_load')) { + $slot = slot_load($slot_id); + handler_attach($slot_id, $slot->default_handler, 'default'); + } + } +} + +/** + * Retrieve a list of all available handlers for a given slot. + * + * @param $slot_id + * The slot for which we want a list of available handlers. + * @return unknown_type + */ +function handler_get_available_handlers($slot_id) { + return db_query('SELECT handler, title, description, class FROM {handler_info} WHERE slot = :slot ORDER BY title', array(':slot' => $slot_id))->fetchAllAssoc('handler'); +} + +/** + * Get the handler object for the handler associated to this slot/target. + * + * @param $slot_id + * The slot for which we want to look up a handler. + * @param $target + * The target for which we want to look up a handler. + * @return + * A loaded handler object. + */ +function handler_get_attached_handler($slot_id, $target = 'default') { + $handler_id = db_query_range("SELECT handler FROM {handler_attachments} WHERE slot = :slot_1 AND target = :target + UNION SELECT handler FROM {handler_attachments} WHERE slot = :slot_2 AND target = 'default'", array( + ':slot_1' => $slot_id, + ':slot_2' => $slot_id, + ':target' => $target, + ), 0, 1)->fetchField(); + + return handler_load($slot_id, $handler_id); +} + +/** + * @} End of "ingroup handlers". + */ Index: includes/handlers.inc =================================================================== RCS file: includes/handlers.inc diff -N includes/handlers.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/handlers.inc 9 Feb 2009 02:08:22 -0000 @@ -0,0 +1,274 @@ +someMethod(); + * @endcode + * + * If no handler has been attached to that slot and target, the handler attached + * to the target "default" is used instead. A slot/target may also be "reset" + * to the default handler: + * + * @code + * handler_detach('slot_name', 'target'); + * @endcode + * + * If no target is specified, "default" is assumed. That allows for a given + * slot to not make use of the system's multi-target capabilities by simply + * omitting the target whenever it is called, which results in a target of + * "default" being used in all cases. + */ + +/** + * Rebuild the handler slot info registry. + * + * @return + * The array of slots just declared. + */ +function handler_slot_build() { + $slots = module_invoke_all('slot_info'); + + // Set default values, to avoid NULL issues if nothing else. + foreach ($slots as $slot => $info) { + $slots[$slot] += handler_slot_defaults(); + } + + // Let other modules alter the handler target registry if needed. + drupal_alter('slot_info', $slots); + + if ($slots) { + // Build the target data. + $insert = db_insert('handler_slot_info')->fields(array('slot', 'title', 'interface', 'reuse', 'default_handler', 'description')); + + foreach ($slots as $slot => $info) { + $insert->values(array( + 'slot' => $slot, + 'title' => $info['title'], + 'interface' => $info['interface'], + 'reuse' => (int)$info['reuse'], + 'default_handler' => $info['default_handler'], + 'description' => $info['description'], + )); + } + + // Don't do the deletion until after we've gotten the new data prepared. That + // reduces the potential race condition window. + db_delete('handler_slot_info')->execute(); + $insert->execute(); + } + + return $slots; +} + +/** + * Rebuild the handler info registry. + * + * @return + * The array of handlers just defined. + */ +function handler_handler_build() { + $handlers = module_invoke_all('handler_info'); + + // Set default values, to avoid NULL issues if nothing else. + foreach ($handlers as $slot => $handler_info) { + foreach ($handler_info as $handler => $info) { + $handlers[$slot][$handler] += handler_handler_defaults(); + } + } + + // Let other modules alter the handler registry if needed. + drupal_alter('handler_info', $handlers); + + if ($handlers) { + // Build the handler data. + $insert = db_insert('handler_info')->fields(array('handler', 'slot', 'title', 'class', 'description')); + foreach ($handlers as $slot => $handler_info) { + foreach ($handler_info as $handler => $info) { + $insert->values(array( + 'handler' => $handler, + 'slot' => $slot, + 'title' => $info['title'], + 'class' => $info['class'], + 'description' => $info['description'], + )); + } + } + + // Don't do the deletion until after we've gotten the new data. That reduces + // the potential race condition window. + db_delete('handler_info')->execute(); + $insert->execute(); + } + + return $handlers; +} + +/** + * Ensure that every slot has a handler attached for the default target. + * + * For every slot, attach whatever handler is declared the default handler + * to the default target unless one has already been defined. That ensures + * that the attachment table always has at least that default record in it for + * each slot, which greatly simplifies the lookup logic. + */ +function handler_ensure_defaults() { + $result = db_query("SELECT hsi.slot, reuse, class, handler + FROM {handler_slot_info} hsi + INNER JOIN {handler_info} hi ON hsi.slot = hi.slot AND hsi.default_handler = hi.handler"); + + foreach ($result as $record) { + // We can't exclude all fields or the query breaks. However, setting + // reuse to itself has no effect on the table so it's safe to do. + db_merge('handler_attachments') + ->key(array( + 'slot' => $record->slot, + 'target' => 'default', + )) + ->fields(array( + 'handler' => $record->handler, + 'class' => $record->class, + 'reuse' => $record->reuse, + )) + ->updateExcept('class', 'handler') + ->execute(); + } +} + +/** + * Define various default values for handler slots. + * + * @return + * The default "empty" slot definition. + */ +function handler_slot_defaults() { + return array( + 'slot' => '', + 'title' => '', + 'interface' => 'HandlerInterface', + 'reuse' => 1, + 'default_handler' => 'default', + 'description' => '', + ); +} + +/** + * Define various default values for handlers. + * + * @return + * The default "empty" handler definition. + */ +function handler_handler_defaults() { + return array( + 'handler' => '', + 'slot' => '', + 'title' => '', + 'class' => '', + 'description' => '', + ); +} + +/** + * Load function for slot info objects. + * + * @param $slot_id + * The internal slot ID. + * @param $refresh + * If this is set to TRUE, the internal slot cache will be flushed. + * @return + * The slot object or NULL if it is not defined. + */ +function slot_load($slot_id, $refresh = FALSE) { + static $slots; + + if ($refresh) { + $slots = array(); + } + + if (empty($slots[$slot_id])) { + $slots[$slot_id] = db_query("SELECT slot, title, interface, reuse, default_handler, description FROM {handler_slot_info} WHERE slot = :slot_id", array(':slot_id' => $slot_id))->fetchObject(); + } + + return $slots[$slot_id]; +} + +/** + * Load function for handler info objects. + * + * @param $slot_id + * The internal slot ID. All handlers are namespaced within their slot. + * @param $handler_id + * The internal handler ID. + * @param $refresh + * If this is set to TRUE, the internal handler cache will be flushed. + * @return + * The handler object or NULL if not defined. + */ +function handler_load($slot_id, $handler_id, $refresh = FALSE) { + static $handlers; + + if ($refresh) { + $handlers = array(); + } + + if (empty($handlers[$slot_id][$handler_id])) { + $handlers[$slot_id][$handler_id] = db_query("SELECT handler, slot, title, class, description FROM {handler_info} WHERE slot = :slot_id AND handler = :handler_id", array( + ':slot_id' => $slot_id, + ':handler_id' => $handler_id, + ))->fetchObject(); + } + + return $handlers[$slot_id][$handler_id]; +} + +/** + * @} End of "ingroup handlers". + */ Index: includes/path.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/path.inc,v retrieving revision 1.32 diff -u -p -r1.32 path.inc --- includes/path.inc 4 Jan 2009 20:04:32 -0000 1.32 +++ includes/path.inc 9 Feb 2009 02:08:23 -0000 @@ -23,84 +23,6 @@ function drupal_init_path() { } /** - * Given an alias, return its Drupal system URL if one exists. Given a Drupal - * system URL return one of its aliases if such a one exists. Otherwise, - * return FALSE. - * - * @param $action - * One of the following values: - * - wipe: delete the alias cache. - * - alias: return an alias for a given Drupal system path (if one exists). - * - source: return the Drupal system URL for a path alias (if one exists). - * @param $path - * The path to investigate for corresponding aliases or system URLs. - * @param $path_language - * Optional language code to search the path with. Defaults to the page language. - * If there's no path defined for that language it will search paths without - * language. - * - * @return - * Either a Drupal system path, an aliased path, or FALSE if no path was - * found. - */ -function drupal_lookup_path($action, $path = '', $path_language = '') { - global $language; - // $map is an array with language keys, holding arrays of Drupal paths to alias relations - static $map = array(), $no_src = array(), $count; - - $path_language = $path_language ? $path_language : $language->language; - - // Use $count to avoid looking up paths in subsequent calls if there simply are no aliases - if (!isset($count)) { - $count = db_query('SELECT COUNT(pid) FROM {url_alias}')->fetchField(); - } - - if ($action == 'wipe') { - $map = array(); - $no_src = array(); - $count = NULL; - } - elseif ($count > 0 && $path != '') { - if ($action == 'alias') { - if (isset($map[$path_language][$path])) { - return $map[$path_language][$path]; - } - // Get the most fitting result falling back with alias without language - $alias = db_query("SELECT dst FROM {url_alias} WHERE src = :src AND language IN(:language, '') ORDER BY language DESC", array( - ':src' => $path, - ':language' => $path_language)) - ->fetchField(); - $map[$path_language][$path] = $alias; - return $alias; - } - // Check $no_src for this $path in case we've already determined that there - // isn't a path that has this alias - elseif ($action == 'source' && !isset($no_src[$path_language][$path])) { - // Look for the value $path within the cached $map - $src = ''; - if (!isset($map[$path_language]) || !($src = array_search($path, $map[$path_language]))) { - // Get the most fitting result falling back with alias without language - if ($src = db_query("SELECT src FROM {url_alias} WHERE dst = :dst AND language IN(:language, '') ORDER BY language DESC", array( - ':dst' => $path, - ':language' => $path_language)) - ->fetchField()) { - $map[$path_language][$src] = $path; - } - else { - // We can't record anything into $map because we do not have a valid - // index and there is no need because we have not learned anything - // about any Drupal path. Thus cache to $no_src. - $no_src[$path_language][$path] = TRUE; - } - } - return $src; - } - } - - return FALSE; -} - -/** * Given an internal Drupal path, return the alias set by the administrator. * * @param $path @@ -113,11 +35,15 @@ function drupal_lookup_path($action, $pa * found. */ function drupal_get_path_alias($path, $path_language = '') { - $result = $path; - if ($alias = drupal_lookup_path('alias', $path, $path_language)) { - $result = $alias; + static $handler; + + // Static cache the handler so that we avoid the lookup costs on + // subsequent calls. + if (empty($handler)) { + $handler = handler('path_lookup'); } - return $result; + + return $handler->lookupAlias($path, $path_language); } /** @@ -127,16 +53,23 @@ function drupal_get_path_alias($path, $p * A Drupal path alias. * @param $path_language * An optional language code to look up the path in. - * * @return * The internal path represented by the alias, or the original alias if no * internal path was found. */ function drupal_get_normal_path($path, $path_language = '') { - $result = $path; - if ($src = drupal_lookup_path('source', $path, $path_language)) { - $result = $src; + static $handler; + + // Static cache the handler so that we avoid the lookup costs on + // subsequent calls. + if (empty($handler)) { + $handler = handler('path_lookup'); } + + $result = $handler->lookupPath($path, $path_language); + + // @todo: Remove this once we confirm that it's not needed, as alternate + // handler implementations can probably handle it. if (function_exists('custom_url_rewrite_inbound')) { // Modules may alter the inbound request path by reference. custom_url_rewrite_inbound($result, $path, $path_language); Index: includes/registry.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/registry.inc,v retrieving revision 1.11 diff -u -p -r1.11 registry.inc --- includes/registry.inc 20 Dec 2008 18:24:33 -0000 1.11 +++ includes/registry.inc 9 Feb 2009 02:08:23 -0000 @@ -37,6 +37,11 @@ function _registry_rebuild() { require_once DRUPAL_ROOT . '/includes/database/select.inc'; require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/query.inc'; + // If called before bootstrap completes, these files may not be available yet. + require_once DRUPAL_ROOT . '/includes/common.inc'; + require_once DRUPAL_ROOT . '/includes/file.inc'; + require_once DRUPAL_ROOT . '/modules/system/system.module'; + // Reset the resources cache. _registry_get_resource_name(); // Get the list of files we are going to parse. Index: modules/path/path.test =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.test,v retrieving revision 1.6 diff -u -p -r1.6 path.test --- modules/path/path.test 9 Jan 2009 07:44:00 -0000 1.6 +++ modules/path/path.test 9 Feb 2009 02:08:23 -0000 @@ -174,6 +174,7 @@ class PathLanguageTestCase extends Drupa // Confirm that the alias works. $this->drupalGet($edit['path']); + $this->assertText($english_node->title, 'Alias works.'); // Translate the node into French. @@ -186,7 +187,7 @@ class PathLanguageTestCase extends Drupa $this->drupalPost(NULL, $edit, t('Save')); // Clear the path lookup cache. - drupal_lookup_path('wipe'); + drupal_clear_path_cache(); // Ensure the node was created. // Check to make sure the node was created. Index: modules/simpletest/tests/handlers.test =================================================================== RCS file: modules/simpletest/tests/handlers.test diff -N modules/simpletest/tests/handlers.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/handlers.test 9 Feb 2009 02:08:23 -0000 @@ -0,0 +1,204 @@ + t('Handlers, Nonreusable'), + 'description' => t('Test the Handler routing system'), + 'group' => t('Handlers'), + ); + } + + function setUp() { + parent::setUp('handlers_test'); + + // We need to rebuild the index at least once, because the module_enable() + // routine called by the parent method doesn't call drupal_flush_all_caches(). + handlers_rebuild(); + } + + /** + * Test that we can retrieve the default handler if no target is defined. + */ + function testDefaultHandler() { + // 'default' has not been attached to anything, so should return the + // default implementation. + $handler = handler('fancystring', 'default'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringDefault, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual($this->sampleString, $mangled, t('Default handler returned correct string.')); + } + } + + /** + * Test that we can specify a handler for a target and it loads correctly. + */ + function testSpecifiedHandler() { + + // Specify a specific handler for a given target. + handler_attach('fancystring', 'rot13', 'foo'); + + // Now request the handler. + $handler = handler('fancystring', 'foo'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringRot13, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual(str_rot13($this->sampleString), $mangled, t('Specified handler returned correct string.')); + } + } + + /** + * Test that we can skip using target on a slot and everything still works. + */ + function testUndefinedHandler() { + // An undefined target should always give us the default handler defined by the slot. + $handler = handler('fancystring'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringDefault, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual($this->sampleString, $mangled, t('Default handler returned correct string.')); + } + } + + /** + * Test a context-sensitive handler. + */ + function testContextSensitiveHandler() { + + // This is a poor way of mocking up the variable system, but it will have + // to do for now. + $map = array( + 'Hello' => 'Goodbye', + 'World' => 'Cruel World', + ); + $GLOBALS['conf']['fancystring_translate'] = $map; + + // Specify a specific handler for a given target. + handler_attach('fancystring', 'custom_translate', 'bar'); + + // Now request the handler. + $handler = handler('fancystring', 'bar'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringCustom, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual(strtr($this->sampleString, $map), $mangled, t('Specified handler returned correct string.')); + } + + unset ($GLOBALS['conf']['fancystring_translate']); + } + + /** + * Test that we can successfully unset a handler attachment. + */ + function testUnsettingHandler() { + + // Specify a specific handler for a given target. + handler_attach('fancystring', 'rot13', 'foo'); + + // Now unset that attachment. + handler_detach('fancystring', 'foo'); + + // Now request the handler. We should get back the default. + $handler = handler('fancystring', 'foo'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringDefault, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual($this->sampleString, $mangled, t('Unspecified handler returned correct string.')); + } + } + + /** + * Test that a non-reusable slot returns a new object eacn time. + */ + function testHandlerReuse() { + + // Specify a specific handler for a given target. + handler_attach('fancystring', 'rot13', 'foo'); + + // Now request the handler. + $handler_1 = handler('fancystring', 'foo'); + + $this->assertTrue($handler_1 instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler_1 instanceof FancystringRot13, t('Correct handler object returned.')); + + $handler_2 = handler('fancystring', 'foo'); + + $this->assertNotIdentical($handler_1, $handler_2, t('A new object was returned.')); + } +} + +/** + * Test class for the Handlers system, part 2. + */ +class HandlersReusableTestCase extends DrupalWebTestCase { + + protected $sampleString = 'Hello World'; + + function getInfo() { + return array( + 'name' => t('Handlers, Reusable'), + 'description' => t('Test the Handler routing system for reusable handlers.'), + 'group' => t('Handlers'), + ); + } + + function setUp() { + parent::setUp('handlers_test'); + + // We need to rebuild the index at least once, because the module_enable() + // routine called by the parent method doesn't call drupal_flush_all_caches(). + handlers_rebuild(); + } + + /** + * Test that a reusable slot returns the exact same object each time. + */ + function testHandlerReuse() { + + // Specify a specific handler for a given target. + handler_attach('thingie', 'default', 'foo'); + + // Now request the handler. + $handler_1 = handler('thingie', 'foo'); + + $this->assertTrue($handler_1 instanceof ThingieInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler_1 instanceof ThingieDefault, t('Correct handler object returned.')); + + $handler_2 = handler('thingie', 'foo'); + + $this->assertIdentical($handler_1, $handler_2, t('The same handler object was returned.')); + } +} Index: modules/simpletest/tests/handlers_test.info =================================================================== RCS file: modules/simpletest/tests/handlers_test.info diff -N modules/simpletest/tests/handlers_test.info --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/handlers_test.info 9 Feb 2009 02:08:23 -0000 @@ -0,0 +1,8 @@ +; $Id$ +name = "Handlers Test" +description = "Support module for Handlers tests." +core = 7.x +package = Testing +files[] = handlers_test.module +version = VERSION +;hidden = TRUE Index: modules/simpletest/tests/handlers_test.module =================================================================== RCS file: modules/simpletest/tests/handlers_test.module diff -N modules/simpletest/tests/handlers_test.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/handlers_test.module 9 Feb 2009 02:08:23 -0000 @@ -0,0 +1,167 @@ + array( + 'title' => 'Fancy string', + 'description' => 'Do fancy stuff to strings', + 'interface' => 'FancystringInterface', + 'default_handler' => 'default', + 'reuse' => FALSE, + ), + 'thingie' => array( + 'title' => 'Other slot', + 'description' => 'This slot does nothing. It\'s just to test lookup behavior of reusable slots.', + 'interface' => 'ThingieInterface', + ), + ); +} + +/** + * Implementation of hook_handler_info(). + */ +function handlers_test_handler_info() { + return array( + 'fancystring' => array( + 'default' => array( + // Translate on load, not define, like hook_menu(). + 'title' => 'No string mutation', + 'class' => 'FancystringDefault', + 'description' => "This handler doesn't do anything to the string. It passes through unaltered.", + ), + 'rot13' => array( + 'title' => 'Rot13 translation', + 'class' => 'FancystringRot13', + 'description' => 'This handler ROT13 encrypts a string.', + ), + 'custom_translate' => array( + 'title' => 'Custom mapping', + 'class' => 'FancystringCustom', + 'description' => 'This handler uses a user-specified mapping array.', + ), + ), + 'thingie' => array( + 'default' => array( + 'title' => 'Do nothing handler', + 'description' => 'This handler does nothing', + 'class' => 'ThingieDefault', + ) + ), + ); +} + +/** + * Interface for the example string mangling handler. + */ +interface FancystringInterface extends HandlerInterface { + +/** + * Set the string for this object to mangle. + * @param $string + * The string to mangle. + */ + public function setString($string); + + /** + * Get the string back, mangled according to this handler. + * @return + * The mangled string. + */ + public function getString(); +} + +/** + * Base implementation of Fancystring handler. + */ +abstract class FancystringBase implements FancystringInterface { + + protected $target; + + protected $string = ''; + + function __construct($target = 'default') { + $this->target = $target; + } + + public function setString($string) { + $this->string = $string; + } +} + +/** + * Default implementation of Fancystring handler. + */ +class FancystringDefault extends FancystringBase { + + public function getString() { + return $this->string; + } +} + +/** + * Rot13 implementation of Fancystring handler. + */ +class FancystringRot13 extends FancystringBase { + + public function getString() { + return str_rot13($this->string); + } +} + +/** + * Custom replacement implementation of Fancystring handler. + * + * Note that we are implementing the FancystringInterface here directly + * rather than inheriting from the base class, mostly to show that we can. + * There's no obligation that handlers do anything other than support the + * appropriate interface. How they do so is entirely up to them. + * + */ +class FancystringCustom implements FancystringInterface { + + protected $string = ''; + + protected $target; + + function __construct($target = 'default') { + $this->target = $target; + } + + public function setString($string) { + $this->string = $string; + } + + public function getString() { + $map = variable_get('fancystring_translate', array()); + + return strtr($this->string, $map); + } +} + +/** + * Interface for the do-nothing test handler. + */ +interface ThingieInterface extends HandlerInterface { + + /** + * Do nothing. + */ + function noop(); +} + +class ThingieDefault implements ThingieInterface { + + protected $target; + + function __construct($target = 'default') { + $this->target = $target; + } + + function noop() { + return; + } +} Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.124 diff -u -p -r1.124 system.admin.inc --- modules/system/system.admin.inc 3 Feb 2009 18:55:31 -0000 1.124 +++ modules/system/system.admin.inc 9 Feb 2009 02:08:23 -0000 @@ -1417,6 +1417,46 @@ function system_clear_cache_submit(&$for drupal_set_message(t('Caches cleared.')); } + +/** + * Form builder; Configure the path alias lookup engine. + * + * @ingroup forms + */ +function system_path_lookup_form() { + $form = array(); + + $options = array(); + foreach (handler_get_available_handlers('path_lookup') as $handler => $info) { + $options[$handler] = t('@title (%description)', array( + '@title' => t($info->title), + '%description' => t($info->description), + )); + } + + $handler = handler_get_attached_handler('path_lookup'); + + $form['path_lookup'] = array( + '#title' => 'Path lookup mechanism', + '#type' => 'radios', + '#options' => $options, + '#default_value' => $handler->handler, + ); + + $form['buttons']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration') ); + + return $form; +} + +/** + * Submit callback; save the path lookup handler selection. + * + * @ingroup forms + */ +function system_path_lookup_form_submit($form, &$form_state) { + handler_attach('path_lookup', $form_state['values']['path_lookup']); +} + /** * Form builder; Configure the site file handling. * Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.17 diff -u -p -r1.17 system.api.php --- modules/system/system.api.php 31 Jan 2009 16:50:57 -0000 1.17 +++ modules/system/system.api.php 9 Feb 2009 02:08:23 -0000 @@ -212,7 +212,7 @@ function hook_js_alter(&$javascript) { * $page['content']['nodes'][$nid]['#node'] * // The results pager. * $page['content']['pager'] - * @code + * @code * * Blocks may be referenced by their module/delta pair within a region: * @code @@ -1636,5 +1636,142 @@ function hook_disable() { } /** + * Define one or more slots for the handlers system. + * + * This hook should return a nested array of slot definitions. Each key + * in the array is the machine-readable slot name, and its value is an array + * of values that define the slot. + * + * title + * This is the human-readable name of the slot. Note that this value + * should not be run through t() as it will be stored in the database. It + * should be translated when displayed to the user. + * description + * A human-readable description of what the slot is for. Note that this value + * should not be run through t() as it will be stored in the database. It + * should be translated when displayed to the user. + * interface + * The PHP interface that defines this slot. The interface must extend + * HandlerInterface, but otherwise may define whatever methods it wants. + * It may also be placed in any file, depending on what would provide the + * most performance for autoloading. + * default_handler (optional) + * The machine-readable name of the default handler for this slot, as defined + * in hook_handler_info(). All slots must have at least one handler + * available. If not specified, "default" is used. + * reuse (optional) + * By default, if a given target on a slot is requested multiple times in + * the same page request the same object will be returned each time. The + * object will be statically cached. To disable that behavior and return + * a newly instantiated object for every request, set this value to FALSE. + * + * This is a registry-style function. It should normally be placed in the + * file $module.registry.inc. + * + * @return + * A slot definition array. + */ +function hook_slot_info() { + return array( + 'fancystring' => array( + 'title' => 'Fancy string', + 'description' => 'Do fancy stuff to strings', + 'interface' => 'FancystringInterface', + 'default_handler' => 'default', + 'reuse' => FALSE, + ), + 'thingie' => array( + 'title' => 'Other slot', + 'description' => 'This slot does nothing. It\'s just to test lookup behavior of reusable slots.', + 'interface' => 'ThingieInterface', + ), + ); +} + +/** + * Alter the slot definitions of another module. + * + * @param $info + * The complete slot definition array from hook_slot_info(). + */ +function hook_slot_info_alter(&$info) { + // Change the default handler. + $info['fancystring']['default_handler'] = 'string_mangler'; +} + +/** + * Define one or more handlers in the system. + * + * This hook should return a doubly-nested array of handler definitions. The + * first key is an existing slot. Its value is an associative array of handler + * machine-readable names and handler definitions. + * + * Each handler definition is an associative array of values that define the + * handler. + * + * title + * This is the human-readable name of the handler. Note that this value + * should not be run through t() as it will be stored in the database. It + * should be translated when displayed to the user. + * description + * A human-readable description of the handler. Note that this value + * should not be run through t() as it will be stored in the database. It + * should be translated when displayed to the user. + * class + * The PHP class that defines this handler. The class must implement the + * interface defined for this slot, but otherwise may be defined in any way + * desired, including implementing the interface directly or indirectly by + * subclassing another class that implements that interface. It may also be + * placed in any file, depending on what would provide the most performance + * for autoloading. + * + * This is a registry-style function. It should normally be placed in the + * file $module.registry.inc. + * + * @return + * A handler definition array. + */ +function hook_handler_info() { + return array( + 'fancystring' => array( + 'default' => array( + // Translate on load, not define, like hook_menu(). + 'title' => 'No string mutation', + 'class' => 'FancystringDefault', + 'description' => "This handler doesn't do anything to the string. It passes through unaltered.", + ), + 'rot13' => array( + 'title' => 'Rot13 translation', + 'class' => 'FancystringRot13', + 'description' => 'This handler ROT13 encrypts a string.', + ), + 'custom_translate' => array( + 'title' => 'Custom mapping', + 'class' => 'FancystringCustom', + 'description' => 'This handler uses a user-specified mapping array.', + ), + ), + 'thingie' => array( + 'default' => array( + 'title' => 'Do nothing handler', + 'description' => 'This handler does nothing', + 'class' => 'ThingieDefault', + ) + ), + ); +} + +/** + * Alter the handler definitions of another module. + * + * @param $info + * The complete handler definition array from hook_handler_info(). + */ +function hook_handler_info_alter(&$info) { + // change the class used by the rot13 handler. + $info['fancystring']['rot13']['class'] = 'FancystringRot13Alternate'; +} + +/** * @} End of "addtogroup hooks". */ Index: modules/system/system.info =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.info,v retrieving revision 1.11 diff -u -p -r1.11 system.info --- modules/system/system.info 12 Oct 2008 01:23:06 -0000 1.11 +++ modules/system/system.info 9 Feb 2009 02:08:23 -0000 @@ -8,4 +8,6 @@ files[] = system.module files[] = system.admin.inc files[] = image.gd.inc files[] = system.install +files[] = system.path_lazy.inc +files[] = system.path_precache.inc required = TRUE Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.306 diff -u -p -r1.306 system.install --- modules/system/system.install 3 Feb 2009 12:30:14 -0000 1.306 +++ modules/system/system.install 9 Feb 2009 02:08:24 -0000 @@ -726,6 +726,122 @@ function system_schema() { 'nid' => array('nid'), ), ); + + $schema['handler_attachments'] = array( + 'description' => 'Maps slots and targets to their configured handler.', + 'fields' => array( + 'slot' => array( + 'description' => 'The machine-readable name of the slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'target' => array( + 'description' => 'The target value.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'handler' => array( + 'description' => 'The handler attached to this slot/target.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'class' => array( + 'description' => 'The PHP class for the handler attached to this slot/target. It is stored in this table to make lookups faster.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'reuse' => array( + 'description' => 'Whether or not this handler may be reused. It is stored in this table to make lookups faster.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'primary key' => array('slot', 'target'), + ); + + $schema['handler_info'] = array( + 'description' => 'Tracks all available handlers in the system.', + 'fields' => array( + 'handler' => array( + 'description' => 'The machine-readable name of the handler.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'slot' => array( + 'description' => 'The machine-readable name of the slot this handler is for.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'title' => array( + 'description' => 'The human-readable name of the handler.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'class' => array( + 'description' => 'The PHP class that defines this handler.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A human-readable description of this handler.', + 'type' => 'text', + 'not null' => TRUE, + ) + ), + 'primary key' => array('handler', 'slot'), + ); + + $schema['handler_slot_info'] = array( + 'description' => 'Tracks all registered slots in the system that handlers can fulfill.', + 'fields' => array( + 'slot' => array( + 'description' => 'The machine-readable name of the slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'title' => array( + 'description' => 'The human-readable name of the slot.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'interface' => array( + 'description' => 'The PHP interface that defines this slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'reuse' => array( + 'description' => 'Whether or not to reuse handler objects on this slot when requested multiple times.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + 'default_handler' => array( + 'description' => 'The default handler id for this slot if one is not otherwise configured.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A human-readable description of this slot.', + 'type' => 'text', + 'not null' => FALSE, + ), + ), + 'primary key' => array('slot'), + ); + $schema['menu_router'] = array( 'description' => 'Maps paths to various callbacks (access, page and title)', 'fields' => array( @@ -3213,6 +3329,134 @@ function system_update_7018() { } /** + * Add the new tables to support handlers. + */ +function system_update_7019() { + $ret = array(); + + $schema['handler_attachments'] = array( + 'description' => 'Maps slots and targets to their configured handler.', + 'fields' => array( + 'slot' => array( + 'description' => 'The machine-readable name of the slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'target' => array( + 'description' => 'The target value.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'handler' => array( + 'description' => 'The handler attached to this slot/target.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'class' => array( + 'description' => 'The PHP class for the handler attached to this slot/target. It is stored in this table to make lookups faster.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'reuse' => array( + 'description' => 'Whether or not this handler may be reused. It is stored in this table to make lookups faster.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'primary key' => array('slot', 'target'), + ); + + $schema['handler_info'] = array( + 'description' => 'Tracks all available handlers in the system.', + 'fields' => array( + 'handler' => array( + 'description' => 'The machine-readable name of the handler.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'slot' => array( + 'description' => 'The machine-readable name of the slot this handler is for.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'title' => array( + 'description' => 'The human-readable name of the handler.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'class' => array( + 'description' => 'The PHP class that defines this handler.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A human-readable description of this handler.', + 'type' => 'text', + 'not null' => TRUE, + ) + ), + 'primary key' => array('handler', 'slot'), + ); + + $schema['handler_slot_info'] = array( + 'description' => 'Tracks all registered slots in the system that handlers can fulfill.', + 'fields' => array( + 'slot' => array( + 'description' => 'The machine-readable name of the slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'title' => array( + 'description' => 'The human-readable name of the slot.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'interface' => array( + 'description' => 'The PHP interface that defines this slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'reuse' => array( + 'description' => 'Whether or not to reuse handler objects on this slot when requested multiple times.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + 'default_handler' => array( + 'description' => 'The default handler id for this slot if one is not otherwise configured.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A human-readable description of this slot.', + 'type' => 'text', + 'not null' => FALSE, + ), + ), + 'primary key' => array('slot'), + ); + + foreach ($schema as $table => $info) { + db_create_table($ret, $table, $info); + } + + return $ret; +} + +/** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. */ Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.666 diff -u -p -r1.666 system.module --- modules/system/system.module 3 Feb 2009 18:55:31 -0000 1.666 +++ modules/system/system.module 9 Feb 2009 02:08:24 -0000 @@ -720,6 +720,12 @@ function system_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['admin/settings/path-lookup'] = array( + 'title' => 'Path lookup', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('system_path_lookup_form'), + 'access arguments' => array('administer site configuration'), + ); // Reports: $items['admin/reports'] = array( @@ -2312,3 +2318,102 @@ function theme_meta_generator_header($ve function system_image_toolkits() { return array('gd'); } + +/** + * Implementation of hook_slot_info(). + */ +function system_slot_info() { + $slots['path_lookup'] = array( + 'title' => 'Path aliases', + 'description' => 'Lookup engine for path aliases', + 'interface' => 'PathAliasInterface', + 'default_handler' => 'lazy', + ); + + return $slots; +} + +/** + * Implementation of hook_handler_info(). + */ +function system_handler_info() { + $handlers['path_lookup']['lazy'] = array( + 'title' => 'Lazy lookup', + 'description' => 'Aliases are looked up one at a time as they are requested. Good for sites with a very large number of path aliases.', + 'class' => 'PathAliasLazy', + ); + $handlers['path_lookup']['precache'] = array( + 'title' => 'Pre-caching', + 'description' => 'All aliases are loaded from the database at once and then looked up in memory. Good for sites with a much smaller number of path aliases than pages.', + 'class' => 'PathAliasPrecache', + ); + + return $handlers; +} + +/** + * Interface for path alias lookup handlers. + */ +interface PathAliasInterface extends HandlerInterface { + + /** + * Given a system path, get the appropriate alias if any. + * + * @param $path + * The system path for which we want the appropriate alias. + * @param path_language + * Optional language code to search the path with. Defaults to the page language. + * If there's no path defined for that language it will search paths without + * language. + * @return + * An aliased path if one was found, or the original path if no alias was + * found. + */ + public function lookupAlias($path = '', $path_language = ''); + + /** + * Given an aliased path, get the corresponding system path, if any. + * + * @param $dest + * The alias for which we want the Drupal system path. + * @param path_language + * Optional language code to search the path with. Defaults to the page language. + * If there's no path defined for that language it will search paths without + * language. + * @return + * The internal path represented by the alias, or the original alias if no + * internal path was found. + */ + function lookupPath($dest = '', $path_language = ''); + + /** + * Clear the cache of looked up aliases so far this request. + */ + public function clearCache(); + +} + +abstract class PathAliasBase extends HandlerBase implements PathAliasInterface { + + /** + * An array with language keys, holding arrays of Drupal paths to alias relations. + * + * @var array + */ + protected $map; + + /** + * Keeps track of the number of paths in the system. + * + * If this value is 0, we know to not bother looking up any paths. + * + * @var int + */ + protected $count; + + public function clearCache() { + $this->map = array(); + $this->noSrc = array(); + $this->count = NULL; + } +} Index: modules/system/system.path_lazy.inc =================================================================== RCS file: modules/system/system.path_lazy.inc diff -N modules/system/system.path_lazy.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/system/system.path_lazy.inc 9 Feb 2009 02:08:24 -0000 @@ -0,0 +1,74 @@ +count)) { + $this->count = db_query('SELECT COUNT(pid) FROM {url_alias}')->fetchField(); + } + + if (!$this->count) { + return $path; + } + + $path_language = $path_language ? $path_language : $language->language; + + if (empty($this->map[$path_language][$path])) { + // Get the most fitting result falling back with alias without language + $alias = db_query("SELECT dst FROM {url_alias} WHERE src = :src AND language IN(:language, '') ORDER BY language DESC", array( + ':src' => $path, + ':language' => $path_language, + ))->fetchField(); + + // If there was no alias, the path translates back to itself. + $this->map[$path_language][$path] = $alias ? $alias : $path; + } + + return $this->map[$path_language][$path]; + } + + public function lookupPath($dest = '', $path_language = '') { + global $language; + + // Use $count to avoid looking up paths in subsequent calls if there simply are no aliases + if (empty($this->count)) { + $this->count = db_query('SELECT COUNT(pid) FROM {url_alias}')->fetchField(); + } + + if (!$this->count) { + return $dest; + } + + $path_language = $path_language ? $path_language : $language->language; + + // If we already know that there is no path, simply return now. + if (isset($this->noSrc[$path_language][$dest])) { + return $dest; + } + + // Reverse-lookup the path from the provided alias. + $src = db_query("SELECT src FROM {url_alias} WHERE dst = :dst AND language IN(:language, '') ORDER BY language DESC", array( + ':dst' => $dest, + ':language' => $path_language, + ))->fetchField(); + + if (!$src) { + $this->noSrc[$path_language][$dest] = TRUE; + return $dest; + } + else { + // Now that we know the path, we can also cache it for forward lookups. + $this->map[$path_language][$src] = $dest; + + // Return the path we found. + return $src; + } + } +} Index: modules/system/system.path_precache.inc =================================================================== RCS file: modules/system/system.path_precache.inc diff -N modules/system/system.path_precache.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/system/system.path_precache.inc 9 Feb 2009 02:08:24 -0000 @@ -0,0 +1,74 @@ +count)) { + $this->count = db_query('SELECT COUNT(pid) FROM {url_alias}')->fetchField(); + } + + if (!$this->count) { + return $path; + } + + $path_language = $path_language ? $path_language : $language->language; + + // Pre-fetch the entire path lookup table so that we never need to check + // the database again. + // @todo: This could probably leverage the cache system for even more + // speed, and then we override clearCache() as well. + if (empty($this->map)) { + $result = db_query("SELECT src, dst, language FROM {url_alias}"); + foreach ($result as $record) { + $this->map[$record->language][$record->src] = $record->dst; + } + } + + return isset($this->map[$path_language][$path]) ? $this->map[$path_language][$path] : $path; + } + + public function lookupPath($dest = '', $path_language = '') { + global $language; + + // Use $count to avoid looking up paths in subsequent calls if there simply are no aliases + if (empty($this->count)) { + $this->count = db_query('SELECT COUNT(pid) FROM {url_alias}')->fetchField(); + } + + if (!$this->count) { + return $dest; + } + + $path_language = $path_language ? $path_language : $language->language; + + // If we already know that there is no path, simply return now. + if (isset($this->noSrc[$path_language][$dest])) { + return $dest; + } + + // Reverse-lookup the path from the provided alias. + $src = db_query("SELECT src FROM {url_alias} WHERE dst = :dst AND language IN(:language, '') ORDER BY language DESC", array( + ':dst' => $dest, + ':language' => $path_language, + ))->fetchField(); + + if (!$src) { + $this->noSrc[$path_language][$dest] = TRUE; + return $dest; + } + else { + // Now that we know the path, we can also cache it for forward lookups. + $this->map[$path_language][$src] = $dest; + + // Return the path we found. + return $src; + } + } +}