You are here

crumbs.module in Crumbs, the Breadcrumbs suite 7

Same filename and directory in other branches
  1. 6.2 crumbs.module
  2. 6 crumbs.module
  3. 7.2 crumbs.module

Provides an API for building breadcrumbs.

File

crumbs.module
View source
<?php

/**
 * @file
 *   Provides an API for building breadcrumbs.
 */

// Register the module-provided autoloader if xautoload is missing.
if (!module_exists('xautoload')) {
  spl_autoload_register('_crumbs_autoload');
}

/**
 * Implements hook_permission().
 */
function crumbs_permission() {
  return array(
    'administer crumbs' => array(
      'title' => t('Administer Crumbs'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function crumbs_menu() {
  $items = array();
  $items['admin/structure/crumbs'] = array(
    'title' => 'Crumbs',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'crumbs_admin_form',
    ),
    'access arguments' => array(
      'administer crumbs',
    ),
    'file' => 'admin/crumbs.admin.inc',
  );
  $items['admin/structure/crumbs/settings'] = array(
    'title' => 'Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 0,
  );
  $items['admin/structure/crumbs/node-parent'] = array(
    'title' => 'Node parent',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'crumbs_admin_node_parent_form',
    ),
    'access arguments' => array(
      'administer crumbs',
    ),
    'file' => 'admin/crumbs.node_parent.inc',
    'weight' => 1,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/structure/crumbs/debug'] = array(
    'title' => 'Debug',
    'page callback' => 'crumbs_debug_page',
    'access arguments' => array(
      'administer crumbs',
    ),
    'file' => 'admin/crumbs.debug.inc',
    'weight' => 2,
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implements hook_theme().
 */
function crumbs_theme($existing, $type, $theme, $path) {
  return array(
    'crumbs_breadcrumb_link' => array(
      'item' => NULL,
    ),
    'crumbs_breadcrumb_current_page' => array(
      'item' => NULL,
    ),
  );
}

/**
 * Default theme implementation for theme('crumbs_breadcrumb_link').
 */
function theme_crumbs_breadcrumb_link(array $item) {
  $options = isset($item['localized_options']) ? $item['localized_options'] : array();
  return l($item['title'], $item['href'], $options);
}

/**
 * Default theme implementation for theme('crumbs_breadcrumb_current_page').
 */
function theme_crumbs_breadcrumb_current_page(array $item) {
  return check_plain($item['title']);
}

/**
 * Implements hook_block_info()
 */
function crumbs_block_info() {
  return array(
    'breadcrumb' => array(
      'info' => t('Breadcrumb (Crumbs)'),
      'cache' => DRUPAL_NO_CACHE,
    ),
  );
}

/**
 * Implements hook_block_view()
 */
function crumbs_block_view($delta = '') {
  $block = array();
  switch ($delta) {
    case 'breadcrumb':
      $breadcrumb_data = crumbs_get_breadcrumb_data();
      if ($breadcrumb_data) {
        $block['content'] = $breadcrumb_data['html'];
      }
      break;
  }
  return $block;
}

/**
 * Implements hook_preprocess_page().
 */
function crumbs_preprocess_page(&$vars) {
  $vars['crumbs_trail'] = array();
  $vars['breadcrumb'] = '';
  $breadcrumb_data = crumbs_get_breadcrumb_data();
  if ($breadcrumb_data) {
    $vars['crumbs_trail'] = $breadcrumb_data['trail'];
    $vars['breadcrumb'] = $breadcrumb_data['html'];
  }
}

/**
 * Implements hook_themekey_properties().
 */
function crumbs_themekey_properties() {
  $attributes = array();
  $attributes['crumbs:trail_paths'] = array(
    'description' => t('Crumbs trail paths'),
    'validator' => '',
    'page cache' => THEMEKEY_PAGECACHE_SUPPORTED,
  );
  $maps = array();
  $maps[] = array(
    'src' => 'drupal:get_q',
    'dst' => 'crumbs:trail_paths',
    'callback' => '_crumbs_themekey_path2trailpaths',
  );
  return array(
    'attributes' => $attributes,
    'maps' => $maps,
  );
}

/**
 * Callback for themekey integration.
 */
function _crumbs_themekey_path2trailpaths($path) {
  $trail = crumbs_get_trail($path);
  $paths = array();
  foreach ($trail as $k => $item) {
    $paths[$item['alias']] = TRUE;
    $paths[$item['route']] = TRUE;
    $paths[$k] = TRUE;
  }
  return array_keys($paths);
}

/**
 * Returns the breadcrumb data for the current page.
 *
 * Gets the menu trail for the current page, and then uses it to build the
 * breadcrumb. Each link is themed separately, and then the links are passed
 * to theme('breadcrumb'), which returns the final rendered breadcrumb.
 *
 * Note: If the existing Drupal-provided breadcrumb is empty, then Crumbs
 * makes no effort to calculate its own, since it means that a module has
 * intentionally removed it.
 *
 * Breadcrumbs with one item are also ignored, to prevent the breadcrumb
 * from being shown on the frontpage.
 *
 * @return
 *   An associative array with the following keys:
 *   - trail: An array containing the menu trail of the current page.
 *   - items: An array containing the built breadcrumb.
 *   - html: The rendered breadcrumb received from theme('breadcrumb').
 *   or FALSE if the breadcrumb could not be determined.
 */
function crumbs_get_breadcrumb_data() {
  static $data;
  if (!isset($data)) {
    $existing_breadcrumb = drupal_get_breadcrumb();

    // If the existing breadcrumb is empty, that means a module has
    // intentionally removed it. Honor that, and stop here.
    if (empty($existing_breadcrumb)) {
      $data = FALSE;
      return $data;
    }
    $show_current_page = variable_get('crumbs_show_current_page', FALSE);
    $trail = crumbs_get_trail();
    if ($show_current_page) {

      // Do not show the breadcrumb if it contains only 1 item. This prevents
      // the breadcrumb from showing up on the frontpage.
      if (count($trail) == 1) {
        $data = FALSE;
        return $data;
      }
    }
    else {

      // The current page should not be shown, remove it from the trail so that
      // the plugins don't need to waste time loading its title.
      array_pop($trail);
    }
    $breadcrumb_items = crumbs_build_breadcrumb($trail);
    $router_item = crumbs_get_router_item($_GET['q']);

    // Allow modules to alter the breadcrumb, if possible, as that is much
    // faster than rebuilding an entirely new active trail.
    drupal_alter('menu_breadcrumb', $breadcrumb_items, $router_item);
    $links = array();
    $last_item = end($breadcrumb_items);
    foreach ($breadcrumb_items as $i => $item) {
      if ($show_current_page && $item == $last_item) {

        // The current page should be styled differently (no link).
        $links[$i] = theme('crumbs_breadcrumb_current_page', $item);
      }
      else {
        $links[$i] = theme('crumbs_breadcrumb_link', $item);
      }
    }
    $html = theme('breadcrumb', array(
      'breadcrumb' => $links,
      'crumbs_breadcrumb_items' => $breadcrumb_items,
      'crumbs_trail' => $trail,
    ));
    $data = array(
      'trail' => $trail,
      'items' => $breadcrumb_items,
      'html' => $html,
    );
  }
  return $data;
}

/**
 * Returns the trail for the provided path.
 *
 * @param $path
 *   The path for which the trail is built. If NULL, the url of the
 *   current page is assumed.
 *
 * @return
 *   An associative array containing the trail, with the paths as the keys, and
 *   the router items (as received from crumbs_get_router_item()) as the values.
 *
 * @see crumbs_TrailFinder
 */
function crumbs_get_trail($path = NULL) {
  static $trails = array();
  if (!isset($path)) {
    $path = $_GET['q'];
  }
  $path = drupal_get_normal_path($path);
  if (!isset($trails[$path])) {
    $finder = crumbs_get_trail_finder();
    $trails[$path] = $finder
      ->buildTrail($path);
  }
  return $trails[$path];
}

/**
 * Builds the breadcrumb based on the provided trail.
 *
 * Each breadcrumb item is a router item taken from the trail, with
 * two additional/updated keys:
 * - title: The title of the breadcrumb item as received from a plugin.
 * - localized_options: An array of options passed to l() if needed.
 *
 * @param $trail
 *   The trail provided by crumbs_get_trail().
 *
 * @return
 *   An array of breadcrumb items.
 *
 * @see crumbs_BreadcrumbBuilder
 */
function crumbs_build_breadcrumb(array $trail) {
  $plugin_engine = crumbs_get_plugin_engine();
  $breadcrumb_builder = new crumbs_BreadcrumbBuilder($plugin_engine);
  return $breadcrumb_builder
    ->buildBreadcrumb($trail);
}

/**
 * Loads and instantiates the trail finder.
 *
 * @return
 *   An instance of crumbs_TrailFinder.
 */
function crumbs_get_trail_finder() {
  static $trail_finder;
  if (!isset($trail_finder)) {
    $parent_finder = crumbs_get_parent_finder();
    $trail_finder = new crumbs_TrailFinder($parent_finder);
  }
  return $trail_finder;
}

/**
 * Loads and instantiates the parent finder.
 *
 * @return
 *   An instance of crumbs_ParentFinder.
 */
function crumbs_get_parent_finder() {
  static $parent_finder;
  if (!isset($parent_finder)) {
    $plugin_engine = crumbs_get_plugin_engine();
    $parent_finder = new crumbs_ParentFinder($plugin_engine);
  }
  return $parent_finder;
}

/**
 * Loads and instantiates the plugin engine.
 *
 * @return
 *   An instance of crumbs_PluginEngine.
 */
function crumbs_get_plugin_engine() {
  static $plugin_engine;
  if (!isset($plugin_engine)) {
    list($plugins, $disabled_keys) = crumbs_get_plugins();
    $weights = crumbs_get_weights();
    foreach ($disabled_keys as $key => $disabled) {
      if (!isset($weights[$key])) {
        $weights[$key] = FALSE;
      }
    }
    $plugin_engine = new crumbs_PluginEngine($plugins, $weights);
  }
  return $plugin_engine;
}

/**
 * Returns an array of loaded plugins and disabled plugin keys.
 *
 * @return
 *   An array containing two arrays:
 *   - An array of loaded plugins keyed by plugin key.
 *   - An array of disabled plugin keys.
 */
function crumbs_get_plugins() {
  static $plugins, $disabled_keys;
  if (!isset($plugins)) {
    $modules = array(
      'blog',
      'comment',
      'crumbs',
      'entityreference',
      'menu',
      'path',
      'taxonomy',
      'forum',
      'entityreference_prepopulate',
    );

    // Include Crumbs-provided plugins.
    foreach ($modules as $module) {
      if (module_exists($module)) {
        module_load_include('inc', 'crumbs', 'plugins/crumbs.' . $module);
      }
    }

    // Organic groups is a special case,
    // because 7.x-2.x behaves different from 7.x-1.x.
    if (module_exists('og')) {
      if (function_exists('og_get_group')) {

        // We are using the og-7.x-1.x branch.
        module_load_include('inc', 'crumbs', 'plugins/crumbs.og');
      }
      else {

        // We are using the og-7.x-2.x branch.
        module_load_include('inc', 'crumbs', 'plugins/crumbs.og.2');
      }
    }
    $plugins = array();
    $disabled_keys = array();
    $api = new crumbs_InjectedAPI_hookCrumbsPlugins($plugins, $disabled_keys);
    foreach (module_implements('crumbs_plugins') as $module) {
      $function = $module . '_crumbs_plugins';
      $api
        ->setModule($module);
      $function($api);
    }
  }
  return array(
    $plugins,
    $disabled_keys,
  );
}

/**
 * Returns an array of plugin keys and their weights.
 *
 * Make sure to distinguish the weight 0 from FALSE, which denotes a disabled
 * plugin.
 *
 * The "*" plugin key is a wildcard that triggers inheritance (runs all
 * implicitly enabled plugins that aren't in the provided list).
 * It always has the biggest weight.
 *
 * @return
 *   An array of plugin keys and their weights, sorted by weight.
 */
function crumbs_get_weights() {
  $weights = variable_get('crumbs_weights', array(
    'crumbs.home_title' => 0,
  ));
  asort($weights);
  if (!isset($weights['*'])) {
    $weights['*'] = count($weights);
  }
  return $weights;
}

/**
 * Returns a router item.
 *
 * This is a wrapper around menu_get_item() that sets additional keys
 * (route, link_path, alias, fragments).
 *
 * @param $path
 *   The path for which the corresponding router item is returned.
 *   For example, node/5.
 *
 * @return
 *   The router item.
 */
function crumbs_get_router_item($path) {
  $normalpath = drupal_get_normal_path($path);
  try {
    $item = menu_get_item($normalpath);
  } catch (Exception $e) {

    // Some modules throw an exception, if a path has unloadable arguments.
    // We don't care, because we don't actually load this page.
    return NULL;
  }

  // Some additional keys.
  if (!empty($item) && is_array($item)) {

    // 'route' is a less ambiguous name for a router path than 'path'.
    $item['route'] = $item['path'];

    // 'href' sounds more like it had already run through url().
    $item['link_path'] = $normalpath;
    $item['alias'] = drupal_get_path_alias($normalpath);
    $item['fragments'] = explode('/', $normalpath);
    if (!isset($item['localized_options'])) {
      $item['localized_options'] = array();
    }
    return $item;
  }
}

/**
 * Chop off path fragments until we find a valid path.
 *
 * @param $path
 *   Starting path or alias
 * @param $depth
 *   Max number of fragments we try to chop off. -1 means no limit.
 */
function crumbs_reduce_path($path, $depth = -1) {
  $fragments = explode('/', $path);
  while (count($fragments) > 1 && $depth !== 0) {
    array_pop($fragments);
    $parent_path = implode('/', $fragments);
    $parent_item = crumbs_get_router_item($parent_path);
    if ($parent_item && $parent_item['href'] === $parent_item['link_path']) {
      return $parent_item['link_path'];
    }
    --$depth;
  }
}

/**
 * This function has exactly one possible input value for
 * each possible return value, except the return value FALSE.
 *
 * @param $router_path :string
 *   The router path can contain any character, but will typically
 *   have a format like "node/%/edit".
 * @return :string or FALSE
 *   A string that can be used as a method suffix,
 *   or FALSE, where that is not possible.
 *   The route "node/%/edit" will resolve as "node_x_edit".
 */
function crumbs_build_method_suffix($router_path) {
  $method_suffix = strtolower($router_path);
  $method_suffix = preg_replace('#[^a-z0-9\\%]#', '_', $method_suffix);
  $method_suffix = strtr($method_suffix, array(
    '%' => 'x',
  ));
  $reverse = strtr($method_suffix, array(
    '_' => '/',
  ));
  $reverse = preg_replace(array(
    '#/x/#',
    '#/x$#',
  ), array(
    '/%/',
    '/%',
  ), $reverse);

  // we need to do this two time to catch things like "/x/x/x/x".
  $reverse = strtr($reverse, array(
    '/x/' => '/%/',
  ));
  if ($reverse === $router_path) {
    return $method_suffix;
  }
  return FALSE;
}

/**
 * Measures time between two runs and displays it in a human-readable format.
 *
 * @param $label
 *   String used to identify the measured time.
 *
 * @param
 *   A string with the measurement, or NULL if the function was run for the
 *   first time.
 */
function crumbs_benchmark($label = NULL) {
  static $previous_time;
  $current_time = microtime(TRUE);
  if (isset($previous_time) && isset($label)) {
    $str = 'Duration: ' . number_format(1000 * ($current_time - $previous_time), 3) . ' ms for ' . $label;
  }
  $previous_time = $current_time;
  return isset($str) ? $str : NULL;
}

/**
 * Crumbs autoloader.
 *
 * Takes the class name, strips the "crumbs_" prefix, converts underscores
 * to directory separators.
 *
 * For example, crumbs_InjectedAPI_describeMonoPlugin will be loaded
 * from lib/InjectedAPI/describeMonoPlugin.php.
 *
 * @param $class
 *   The name of the class to load.
 */
function _crumbs_autoload($class) {
  if (preg_match('#^crumbs_(.*)$#', $class, $m)) {
    $path = strtr($m[1], '_', '/');
    module_load_include('php', 'crumbs', "lib/{$path}");
  }
}

/**
 * Clean tokens so they are URL friendly.
 * Taken directly from pathauto_clean_token_values()
 *
 * @param $replacements
 *   An array of token replacements that need to be "cleaned" for use in the URL.
 * @param $data
 *   An array of objects used to generate the replacements.
 * @param $options
 *   An array of options used to generate the replacements.
 */
function crumbs_clean_token_values(&$replacements, $data = array(), $options = array()) {
  foreach ($replacements as $token => &$value) {

    // Only clean non-path tokens.
    if (!preg_match('/(path|alias|url|url-brief)\\]$/', $token)) {

      // We use a simple regex instead of pathauto_cleanstring().
      $value = preg_replace('#[^a-z0-9/]+#', '-', strtolower($value));
    }
  }
}

// ============================================ interfaces =====================

/**
 * Interface to be used internally by the plugin engine.
 */
interface crumbs_PluginOperationInterface_find {
  function invoke($plugin, $plugin_key, $weight_keeper);

}

/**
 * Interface to be used internally by the plugin engine.
 */
interface crumbs_PluginOperationInterface_alter {
  function invoke($plugin, $plugin_key);

}

// -----------------------------------------------------------------------------

/**
 * Interface for plugin objects registered with hook_crumbs_plugins().
 */
interface crumbs_MonoPlugin {

  /**
   * @param $api :crumbs_InjectedAPI_describeMonoPlugin
   *   Injected API object, with methods that allows the plugin to further
   *   describe itself.
   *
   * @return
   *   As an alternative to the API object's methods, the plugin can simply
   *   return a string label.
   */
  function describe($api);

}

// -----------------------------------------------------------------------------

/**
 * Interface for plugin objects registered with hook_crumbs_plugins().
 *
 */
interface crumbs_MultiPlugin {

  /**
   * @param $api :crumbs_InjectedAPI_describeMultiPlugin
   *   Injected API object, with methods that allow the plugin to further
   *   describe itself.
   *   The plugin is supposed to tell Crumbs about all possible rule keys, and
   *   can give a label and a description for each.
   *
   * @return
   *   As an alternative to the API object's methods, the plugin can simply
   *   return a key-value array, where the keys are the available rules, and the
   *   values are their respective labels.
   */
  function describe($api);

}

Functions

Namesort descending Description
crumbs_benchmark Measures time between two runs and displays it in a human-readable format.
crumbs_block_info Implements hook_block_info()
crumbs_block_view Implements hook_block_view()
crumbs_build_breadcrumb Builds the breadcrumb based on the provided trail.
crumbs_build_method_suffix This function has exactly one possible input value for each possible return value, except the return value FALSE.
crumbs_clean_token_values Clean tokens so they are URL friendly. Taken directly from pathauto_clean_token_values()
crumbs_get_breadcrumb_data Returns the breadcrumb data for the current page.
crumbs_get_parent_finder Loads and instantiates the parent finder.
crumbs_get_plugins Returns an array of loaded plugins and disabled plugin keys.
crumbs_get_plugin_engine Loads and instantiates the plugin engine.
crumbs_get_router_item Returns a router item.
crumbs_get_trail Returns the trail for the provided path.
crumbs_get_trail_finder Loads and instantiates the trail finder.
crumbs_get_weights Returns an array of plugin keys and their weights.
crumbs_menu Implements hook_menu().
crumbs_permission Implements hook_permission().
crumbs_preprocess_page Implements hook_preprocess_page().
crumbs_reduce_path Chop off path fragments until we find a valid path.
crumbs_theme Implements hook_theme().
crumbs_themekey_properties Implements hook_themekey_properties().
theme_crumbs_breadcrumb_current_page Default theme implementation for theme('crumbs_breadcrumb_current_page').
theme_crumbs_breadcrumb_link Default theme implementation for theme('crumbs_breadcrumb_link').
_crumbs_autoload Crumbs autoloader.
_crumbs_themekey_path2trailpaths Callback for themekey integration.

Interfaces

Namesort descending Description
crumbs_MonoPlugin Interface for plugin objects registered with hook_crumbs_plugins().
crumbs_MultiPlugin Interface for plugin objects registered with hook_crumbs_plugins().
crumbs_PluginOperationInterface_alter Interface to be used internally by the plugin engine.
crumbs_PluginOperationInterface_find Interface to be used internally by the plugin engine.