=== modified file 'includes/bootstrap.inc' --- includes/bootstrap.inc 2009-01-31 16:50:56 +0000 +++ includes/bootstrap.inc 2009-02-18 19:20:00 +0000 @@ -1615,3 +1615,156 @@ function registry_rebuild() { /** * @} End of "ingroup registry". */ + +/** + * @ingroup handlers + * @{ + */ + +/** + * Request a handler object. + * + * @param $slot_id + * The name of the pluggable subsystem the handler belongs to. + * @param $target + * The target inside the subsytem the handler belongs to. + * @param $reset + * If this is set to TRUE, the internal caches are flushed. Internal use + * only. + * + * @return + * The associated handler object. + */ +function handler($slot_id, $target = 'default', $reset = FALSE) { + // $handler_mappings is an array, keyed by $slot_id and $target and the + // values are arrays containing class names and a boolean indicating + // reusability. handler_objects keeps the objects themselves. + // $default_only[$slot_id] is TRUE if the given slot only uses defaults. + static $handler_mappings, $handler_objects, $default_only; + + // We have to reset the target cache after a new handler has been attached + // or detached. + if ($reset) { + $handler_mappings = array(); + $handler_objects = array(); + $default_only = array(); + return; + } + + // If we already have the appropriate handler cached, just use that. + if (!empty($handler_objects[$slot_id][$target])) { + return $handler_objects[$slot_id][$target]; + } + + // While the objects need to be stored per target, the classnames can be per + // slot, if the only attached handler is attached to default. That includes + // any slot that does not make use of targets. We cache the handler + // information for these slots in the variable system to reduce database + // lookups. Because the variable system doesn't auto-initialize in + // variable_get(), any handler lookup that runs before the variable system + // has been initialized will simply fail this check and use the database + // anyway. + $mapping_target = isset($default_only[$slot_id]) ? 'default' : $target; + // Look up the class for the requested slot/target. + if (empty($handler_mappings[$slot_id][$mapping_target])) { + // If the only attached handler is attached to default, + if ($record = variable_get('handler_default_' . $slot_id, array())) { + $default_only[$slot_id] = TRUE; + $mapping_target = 'default'; + } + else { + // 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)->fetchAssoc(); + + // 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)->fetchAssoc(); + } + } + // Statically cache the lookup information so that we don't need to check + // for it again. + $handler_mappings[$slot_id][$mapping_target] = $record; + } + + // Create a new handler object. + $handler = new $handler_mappings[$slot_id][$mapping_target]['class']($target); + + // Cache the handler object for later use, if flagged to do so. + if ($handler_mappings[$slot_id][$mapping_target]['reuse']) { + $handler_objects[$slot_id][$target] = $handler; + } + return $handler; +} + +/** + * Rebuild the handler and slot registries. + */ +function handlers_rebuild() { + require_once DRUPAL_ROOT . '/includes/handlers.inc'; + handler_slot_rebuild(); + handler_handler_rebuild(); + 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 provides 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". + */ === modified file 'includes/common.inc' --- includes/common.inc 2009-02-13 04:43:00 +0000 +++ includes/common.inc 2009-02-18 19:43:46 +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'])) { @@ -4186,6 +4186,7 @@ function drupal_flush_all_caches() { _drupal_flush_css_js(); registry_rebuild(); + handlers_rebuild(); drupal_clear_css_cache(); drupal_clear_js_cache(); @@ -4228,3 +4229,129 @@ function _drupal_flush_css_js() { } variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19)); } + +/** + * @ingroup handlers + * @{ + */ + +/** + * Associate a handler with a specific target for the given slot. + * + * @param $slot_id + * The internal ID of the slot. + * @param $handler_id + * The internal ID of the handler. + * @param $target + * The handler will be associated with this target. + */ +function handler_attach($slot_id, $handler_id, $target = 'default') { + + // Lazy-load the utility function. + if (drupal_function_exists('handler_load')) { + + $handler = handler_load($slot_id, $handler_id); + $slot = slot_load($slot_id); + + if ($handler) { + // We store the class name in the database in addition to the handler ID + // so that we can immediately instantiate the class upon lookup. + db_merge('handler_attachments') + ->key(array( + 'slot' => $slot_id, + 'target' => $target, + )) + ->fields(array( + 'handler' => $handler->handler, + 'class' => $handler->class, + 'reuse' => (int)$slot->reuse, + )) + ->execute(); + } + + // If we've associated something other than the default target, disable the + // extra variable system caching of handler attachments. + if ($target != 'default') { + variable_del('handler_default_' . $slot_id); + } + + // Flush the target cache. + handler(NULL, NULL, TRUE); + } +} + +/** + * 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'); + } + } + + // If there are no attachments for this slot other than 'default', + // cache that to the variable system for faster lookups. + $attachments = db_query("SELECT target, class, reuse FROM {handler_attachments} WHERE slot = :slot", array(':slot' => $slot_id))->fetchAllAssoc('target', PDO::FETCH_ASSOC); + if (count($attachments) == 1 && key($attachments) == 'default') { + $attachments = current($attachments); + unset($attachments['target']); + variable_set('handler_default_' . $slot_id, $attachments); + } + + // Flush the target cache. + handler(NULL, NULL, TRUE); +} + +/** + * 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 + * An array of handlers that have been defined for this slot. + */ +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". + */ === added file 'includes/handlers.inc' --- includes/handlers.inc 1970-01-01 00:00:00 +0000 +++ includes/handlers.inc 2009-02-18 19:21:16 +0000 @@ -0,0 +1,295 @@ +get(...); + * @endcode + * + * If no handler has been attached to that slot and target, the handler attached + * to the target 'default' is used instead. This allows arbitrarily defined + * targets -- handler can ask for any target and will get a meaningful answer + * every time. If no target is specified, the slot's currently assigned default + * handler is used, allowing the target argument to be ommitted for subsystems + * with only a single target. + * + * A slot/target may also be reset: + * + * @code + * handler_detach('cache', 'filter'); + * @endcode + * + * After detach, as explained above, subsequent calls for this target will + * return the handler attached to the target 'default'. + * + * If the target argument is omitted, both handler_attach and handler_detach + * operate on the 'default' target. + */ + +/** + * Rebuild the handler slot registry. + * + * @return + * The array of slots just declared. + */ +function handler_slot_rebuild() { + $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 registry. + * + * @return + * The array of handlers just defined. + */ +function handler_handler_rebuild() { + $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(); + + // If there are no attachments for this slot other than the default, cache + // that to the variable system for faster lookups. + $attachments = db_query("SELECT target, class, reuse FROM {handler_attachments} WHERE slot = :slot", array(':slot' => $record->slot))->fetchAllAssoc('target', PDO::FETCH_ASSOC); + if (count($attachments) == 1 && key($attachments) == 'default') { + $attachments = current($attachments); + unset($attachments['target']); + variable_set('handler_default_' . $record->slot, $attachments); + } + } +} + +/** + * 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 objects. + * + * @param $slot_id + * The internal slot ID. + * @param $refresh + * If this is set to TRUE, the internal slot cache is 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 handlers cache is 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". + */ === modified file 'includes/path.inc' --- includes/path.inc 2009-01-04 20:04:32 +0000 +++ includes/path.inc 2009-02-18 19:14:25 +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,18 @@ 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; + + // This function may be called many times in one page, so static cache the + // handler so that we don't waste cycles on the handler lookup each time. + // The handler should never change during the course of a page request so + // that doesn't cause a problem. Note that because objects pass by handle + // this will be the same handler object as in drupal_get_normal_path(). + if (empty($handler)) { + $handler = handler('path_lookup'); } - return $result; + + return $handler->lookupAlias($path, $path_language); } /** @@ -127,16 +56,26 @@ 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; + + // This function may be called many times in one page, so static cache the + // handler so that we don't waste cycles on the handler lookup each time. + // The handler should never change during the course of a page request so + // that doesn't cause a problem. Note that because objects pass by handle + // this will be the same handler object as in drupal_get_path_alias(). + 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); === modified file 'includes/registry.inc' --- includes/registry.inc 2008-12-20 18:24:32 +0000 +++ includes/registry.inc 2009-02-18 01:50:03 +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. === modified file 'modules/path/path.test' --- modules/path/path.test 2009-01-09 07:44:00 +0000 +++ modules/path/path.test 2009-02-18 01:50:03 +0000 @@ -186,7 +186,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. === added file 'modules/simpletest/tests/handlers.test' --- modules/simpletest/tests/handlers.test 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/handlers.test 2009-02-18 19:17:31 +0000 @@ -0,0 +1,202 @@ + t('Handlers, Nonreusable'), + 'description' => t('Test the handler routing system'), + 'group' => t('Handlers'), + ); + } + + function setUp() { + parent::setUp('handlers_test'); + $this->sampleString = $this->randomName(); + + // 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 simulating 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->fail(get_class($handler)); + + $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 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.')); + } +} === added file 'modules/simpletest/tests/handlers_test.info' --- modules/simpletest/tests/handlers_test.info 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/handlers_test.info 2009-02-18 01:50:03 +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 === added file 'modules/simpletest/tests/handlers_test.module' --- modules/simpletest/tests/handlers_test.module 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/handlers_test.module 2009-02-18 19:15:15 +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; + } +} === modified file 'modules/system/system.admin.inc' --- modules/system/system.admin.inc 2009-02-11 05:33:18 +0000 +++ modules/system/system.admin.inc 2009-02-18 01:50:03 +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. * === modified file 'modules/system/system.api.php' --- modules/system/system.api.php 2009-02-09 15:42:52 +0000 +++ modules/system/system.api.php 2009-02-18 19:42:02 +0000 @@ -1635,6 +1635,144 @@ function hook_disable() { mymodule_cache_rebuild(); } + +/** + * 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". */ === modified file 'modules/system/system.info' --- modules/system/system.info 2008-10-12 01:23:01 +0000 +++ modules/system/system.info 2009-02-18 01:50:03 +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 === modified file 'modules/system/system.install' --- modules/system/system.install 2009-02-03 12:30:14 +0000 +++ modules/system/system.install 2009-02-18 19:43:08 +0000 @@ -278,7 +278,7 @@ function system_requirements($phase) { 'title' => $t('HTTP request status'), 'value' => $t('Fails'), 'severity' => REQUIREMENT_ERROR, - 'description' => $t('Your system or network configuration does not allow Drupal to access web pages, resulting in reduced functionality. This could be due to your webserver configuration or PHP settings, and should be resolved in order to download information about available updates, fetch aggregator feeds, sign in via OpenID, or use other network-dependent services. If you are certain that Drupal can access web pages but you are still seeing this message, you may add $conf[\'drupal_http_request_fails\'] = FALSE; to the bottom of your settings.php file.'), + 'description' => $t('Your system or network configuration does not allow Drupal to access web pages, resulting in reduced functionality. This could be due to your webserver configuration or PHP settings, and should be resolved in order to download information about available updates, fetch aggregator feeds, sign in via OpenID, or use other network-dependent services. If you are certain that Drupal can access web pages but you are still seeing this message, you may add $conf[\'drupal_http_request_fails\'] = FALSE; to the bottom of your settings.php file.'), ); } } @@ -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. */ === modified file 'modules/system/system.module' --- modules/system/system.module 2009-02-11 05:33:18 +0000 +++ modules/system/system.module 2009-02-18 19:42:28 +0000 @@ -711,6 +711,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( @@ -2290,3 +2296,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; + } +} === added file 'modules/system/system.path_lazy.inc' --- modules/system/system.path_lazy.inc 1970-01-01 00:00:00 +0000 +++ modules/system/system.path_lazy.inc 2009-02-18 19:29:10 +0000 @@ -0,0 +1,76 @@ +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 + // 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, 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; + } + } +} === added file 'modules/system/system.path_precache.inc' --- modules/system/system.path_precache.inc 1970-01-01 00:00:00 +0000 +++ modules/system/system.path_precache.inc 2009-02-18 01:50:03 +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; + } + } +}