<?php
// $Id: admin_menu.module,v 1.26.2.33 2008/11/02 21:36:36 sun Exp $

/**
 * @file
 * Renders a menu tree for administrative purposes as dropdown menu at the top
 * of the window.
 *
 * Note: Most theme-functions in Administration Menu are not invoked via theme(),
 * because we try to keep this module as fast as possible and chances are very
 * small that someone wants to override those functions.
 */

/**
 * Implementation of hook_help().
 */
function admin_menu_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('Renders the administer menu tree as dropdown menu at the top of the window.');

    case 'admin/settings/admin_menu':
      return t('The Administration Menu will appear at the top of the page for users who have been given the "access administration menu" permission.  Customize appearance of the dropdown menu here.');

    case 'admin/help#admin_menu':
      return t(<<<EOT
<p>Drupal Administration Menu renders all administrative menu items below 'administer'
in a clean, attractive and purely CSS-based menu at the top of your website.  It contains
not only regular menu items - local tasks are also included, giving you extremely fast
access to any administrative resource and function your Drupal installation provides.</p>
<p>The menu also can display the number of anonymous and authenticated users,
<a href="http://drupal.org/project/devel">Devel module</a> links, and items generated by
other modules.</p>
<p>The <a href="!settings">settings page</a> is where administrators can choose
whether or not to allow the module to shift the page contents down to accommodate
the menu.  Depending on the design/layout of your theme, this may cause problems with
the appearance of the page.  Changing the '%margin-setting' setting may fix this
issue.</p>
<p>Please bear in mind that the displayed menu items in Drupal Administration Menu depend on the actual permissions of a user.  For example, if a user does not have the permission "@permission-access" and "@permission-users", the whole "@user-management" menu item will not be displayed.</p>
EOT
, array('!settings' => url('admin/settings/admin_menu'), '%margin-setting' => t('Apply margin-top to page body'), '@permission-access' => 'administer access control', '@permission-users' => 'administer users', '@user-management' => 'User management'));
  }
}

/**
 * Implementation of hook_perm().
 */
function admin_menu_perm() {
  return array('access administration menu', 'display drupal links');
}

/**
 * Implementation of hook_menu().
 *
 * We can't move this into admin_menu_footer(), because PHP-only based themes
 * like chameleon load and output scripts and stylesheets in front of
 * theme_closure(), so we ensure Admin menu's styles and scripts are loaded on
 * all pages via hook_menu().
 */
function admin_menu_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/admin_menu',
      'title' => t('Administration Menu'),
      'description' => t('Adjust settings for the dropdown Administration Menu.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array('admin_menu_theme_settings'),
      'access' => user_access('administer site configuration'),
    );
    $items[] = array(
      'path' => 'admin_menu/toggle-modules',
      'callback' => 'admin_menu_toggle_modules',
      'access' => user_access('administer site configuration'),
      'type' => MENU_CALLBACK,
    );
  }
  elseif (user_access('access administration menu')) {
    $path = drupal_get_path('module', 'admin_menu');
    drupal_add_css($path .'/admin_menu.css', 'module', 'all', FALSE);
    // Performance: Defer execution.
    drupal_add_js($path .'/admin_menu.js', 'module', 'header', TRUE);

    if ($setting = variable_get('admin_menu_margin_top', 1)) {
      drupal_add_js(array('admin_menu' => array('margin_top' => $setting)), 'setting');
    }
    if ($setting = variable_get('admin_menu_position_fixed', 0)) {
      drupal_add_js(array('admin_menu' => array('position_fixed' => $setting)), 'setting');
    }
    if ($setting = variable_get('admin_menu_tweak_tabs', 0)) {
      drupal_add_js(array('admin_menu' => array('tweak_tabs' => $setting)), 'setting');
    }
    if ($_GET['q'] == 'admin/build/menu' && variable_get('admin_menu_tweak_menu', 0)) {
      drupal_add_js($path .'/admin_menu.menu.js');
      drupal_add_js('misc/collapse.js');
    }
    else if ($_GET['q'] == 'admin/build/modules') {
      drupal_add_js(array('admin_menu' => array('tweak_modules' => variable_get('admin_menu_tweak_modules', 0))), 'setting');
    }
  }
  return $items;
}

/**
 * Form builder function for module settings.
 */
function admin_menu_theme_settings() {
  $form['admin_menu_margin_top'] = array(
    '#type' => 'checkbox',
    '#title' => t('Apply margin-top to page body'),
    '#default_value' => variable_get('admin_menu_margin_top', 1),
    '#description' => t('If enabled, the output of this site will be shifted for approx. 20 pixels from the top of the viewport to make room for the Administration Menu. If this setting is disabled, some absolute or fixed positioned page elements at the top of the viewport may be covered by Administration Menu.'),
  );
  $form['admin_menu_position_fixed'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use fixed instead of absolute positioning'),
    '#default_value' => variable_get('admin_menu_position_fixed', 0),
    '#description' => t('If enabled, the administration menu will always stay at the top of the browser viewport, even if you scroll down the page.') .'<br />'. t('<strong>Note:</strong> In some browsers this setting might cause malformed page contents, a disappearing cursor and not editable select lists in forms. If such effects occur, this setting should be disabled.'),
  );
  $form['tweaks'] = array(
    '#type' => 'fieldset',
    '#title' => t('Drupal Administration Tweaks'),
  );
  $form['tweaks']['admin_menu_tweak_menu'] = array(
    '#type' => 'checkbox',
    '#title' => t('Collapse menus in menu administration'),
    '#default_value' => variable_get('admin_menu_tweak_menu', 0),
    '#description' => t('If enabled, menu containers on the <a href="!menu-url">Site building &raquo; Menus</a> page will be collapsed like fieldsets.', array('!menu-url' => url('admin/build/menu'))),
  );
  $form['tweaks']['admin_menu_tweak_modules'] = array(
    '#type' => 'checkbox',
    '#title' => t('Collapse fieldsets on modules page'),
    '#default_value' => variable_get('admin_menu_tweak_modules', 0),
    '#description' => t('If enabled, fieldsets on the <a href="!modules-url">Site building &raquo; Modules</a> page will be initially collapsed.', array('!modules-url' => url('admin/build/modules'))),
  );
  if (module_exists('util')) {
    $form['tweaks']['admin_menu_tweak_modules']['#description'] .= '<br /><strong>'. t('If you installed Utility module for this purpose, you can safely uninstall it now!') .'</strong>';
  }
  $form['tweaks']['admin_menu_tweak_tabs'] = array(
    '#type' => 'checkbox',
    '#title' => t('Move page tabs into administration menu'),
    '#default_value' => variable_get('admin_menu_tweak_tabs', 0),
    '#description' => t('If enabled, tabs of the current page (usually displayed next or below the page title) will be moved into the administration menu. This feature requires that the theme is using the CSS classes <code>tabs primary</code> and <code>tabs secondary</code> for tabs (like Garland).'),
  );
  return system_settings_form($form);
}

/**
 * Suppress display of administration menu.
 *
 * This function should be called from within another module's page callback
 * (preferably using module_invoke()) when the menu should not be displayed.
 * This is useful for modules that implement popup pages or other special
 * pages where the menu would be distracting or break the layout.
 *
 * @param $set
 *   Defaults to TRUE.  If called before hook_footer, the menu will not be
 *   displayed.  Calling with FALSE returns the suppression state.
 */
function admin_menu_suppress($set = TRUE) {
  static $suppress = FALSE;
  if (!empty($set)) {
    $suppress = TRUE;
  }
  return $suppress;
}

/**
 * Implementation of hook_footer().
 *
 * Admin menu was previously output via hook_block(), but suffered from
 * theme-specific stylesheets that may be applied to layout blocks. We now
 * output Admin menu in the footer to circumvent this.
 */
function admin_menu_footer($main = 0) {
  if (!user_access('access administration menu') || admin_menu_suppress(FALSE)) {
    return;
  }
  global $_menu;
  
  // Get item id of /q=admin, which we suppose to be the root for admin menu.
  $mid_admin = $_menu['path index']['admin'];
  $_admin_menu =& admin_menu_get_menu($mid_admin);
  
  // Allow other modules to integrate with admin_menu (uncached).
  foreach (module_implements('admin_menu') as $module) {
    $function = $module .'_admin_menu';
    $function($_admin_menu, FALSE);
  }
  
  $content  = '<div id="admin-menu">';
  $content .= theme_admin_menu_tree($mid_admin);
  $content .= '</div>';
  return $content;
}

/**
 * Return Administration Menu from cache or rebuild it.
 *
 * @param int $mid_admin
 *   The menu item id to use for the administration menu.
 *
 * @return array
 *   An array containing a complete menu structure of all cached administration
 *   menu items.
 */
function &admin_menu_get_menu($mid_admin = 5) {
  static $_admin_menu;
  
  if (isset($_admin_menu)) {
    return $_admin_menu;
  }
  global $user, $locale;
  
  $cid = $user->uid .':'. $locale .':admin_menu';
  $cache = cache_get($cid, 'cache_menu');
  // Check if cache is an array needed to distinguish between v5.x-1.2 and later
  // versions.
  if ($cache && substr($cache->data, 0, 1) == 'a') {
    $_admin_menu = unserialize($cache->data);
  }
  else {
    require_once drupal_get_path('module', 'admin_menu') .'/admin_menu.inc';
    $_admin_menu = admin_menu_build($mid_admin);
    cache_set($cid, 'cache_menu', serialize($_admin_menu), time() + (60 * 60 * 24));
  }
  
  return $_admin_menu;
}

/**
 * Generate the HTML for a menu tree.
 *
 * @param int $pid
 *   The menu item id to use for the administration menu.
 *
 * @return string
 *   The complete, rendered administration menu.
 */
function theme_admin_menu_tree($pid = 1) {
  $_admin_menu = admin_menu_get_menu();
  $output = '';
  
  if (isset($_admin_menu[$pid]) && $_admin_menu[$pid]['children']) {
    // Since we allow other modules to add items to admin menu, we need to sort
    // all items (again).
    usort($_admin_menu[$pid]['children'], '_admin_menu_sort');
    foreach ($_admin_menu[$pid]['children'] as $mid) {
      $children = isset($_admin_menu[$mid]['children']) ? $_admin_menu[$mid]['children'] : NULL;
      $output .= theme_admin_menu_item($mid, theme_admin_menu_tree($mid), count($children) == 0);
    }
  }
  return $output ? "\n<ul>". $output .'</ul>' : '';
}

/**
 * Generate the HTML output for a single menu item.
 *
 * @param int $mid
 *   The menu id of the item.
 * @param string $children
 *   A string containing any rendered child items of this menu.
 * @param bool $leaf
 *   A boolean indicating whether this menu item is a leaf.
 */
function theme_admin_menu_item($mid, $children = '', $leaf = TRUE) {
  static $display_option;

  $_admin_menu = admin_menu_get_menu();
  $item = $_admin_menu[$mid];
  
  if (!isset($display_option)) {
    $display_option = variable_get('admin_menu_display', 0);
  }
  // Display extra information about menu items if enabled (devel).
  if ($display_option) {
    if ($display_option == 'mid') {
      $item['title'] = $item['title'] .' ('. $mid .')';
    }
    else if (isset($item[$display_option])) {
      $item['title'] = $item['title'] .' ('. $item[$display_option] .')';
    }
  }
  
  $class = array();
  if (!$leaf) {
    $class[] = 'expandable';
  }
  if (isset($item['class'])) {
    $class[] = $item['class'];
  }
  $output  = '<li'. (!empty($class) ? ' class="'. implode(' ', $class) .'"' : '') .'>';
  $output .= '<a href="'. check_url($item['path']) .'"'. drupal_attributes($item['attributes']) .'>'. filter_xss_admin($item['title']) .'</a>'. $children .'</li>';
  return $output;
}

/**
 * Comparator routine for use in sorting menu items.
 */
function _admin_menu_sort($a, $b) {
  $_admin_menu = admin_menu_get_menu();
  
  $a = $_admin_menu[$a];
  $b = $_admin_menu[$b];
  
  if ($a['weight'] < $b['weight']) {
    return -1;
  }
  elseif ($a['weight'] > $b['weight']) {
    return 1;
  }
  elseif (isset($a['title']) && isset($b['title'])) {
    return strnatcasecmp($a['title'], $b['title']);
  }
  else {
    return 1;
  }
}

/**
 * Adjust the menu item path.
 *
 * Adjust the path of local tasks and let them point to their parent item.
 * Finally build the url. These functions have been moved here to be able
 * to cache the final results.
 *
 * @param &$item
 *   An admin menu item.
 */
function admin_menu_item_url(&$_admin_menu, $mid) {
  $link_item = $mid;
  while ($_admin_menu[$link_item]['type'] & MENU_LINKS_TO_PARENT) {
    $link_item = $_admin_menu[$link_item]['pid'];
  }

  if (!isset($_admin_menu[$link_item]['processed'])) {
    $_admin_menu[$mid]['path'] = url($_admin_menu[$link_item]['path'], isset($_admin_menu[$mid]['query']) ? $_admin_menu[$mid]['query'] : NULL);
    $_admin_menu[$link_item]['processed'] = TRUE;
  }
  else {
    // Copy the already processed path of the parent item to the
    // default local task.
    $_admin_menu[$mid]['path'] = $_admin_menu[$link_item]['path'];
  }
}

/**
 * Add a custom menu item.
 *
 * @param array $_admin_menu
 *   An array containing the complete administration menu structure, passed by
 *   reference.
 * @param int $pid
 *   The parent menu item id.
 * @param array $item
 *   An menu item array for the menu system. May contain the key 'weight' to
 *   adjust the item's weight. You can use Devel module to display additional
 *   information about menu items.
 *
 * @return int
 *   The id of the new menu item.
 */
function admin_menu_add_item(&$_admin_menu, $pid, $item) {
  if (empty($item['path'])) {
    return FALSE;
  }
  $item['pid'] = $pid;
  $item['children'] = array();
  $id = max(array_keys($_admin_menu)) + 1;
  $_admin_menu[$id] = $item;
  $_admin_menu[$pid]['children'][] = $id;
  $_admin_menu['index'][$item['path']] = $id;
  admin_menu_item_url($_admin_menu, $id);

  return $id;
}

/**
 * Implementation of hook_admin_menu().
 *
 * @param array $admin_menu
 *   An array containing the complete administration menu structure, passed by
 *   reference.
 * @param bool $may_cache
 *   Whether changes will be cached. If new menu items contain dynamic
 *   information, such as query strings or user-related data, these should be
 *   added on each page request ($may_cache = FALSE).
 */
function admin_menu_admin_menu(&$admin_menu, $may_cache) {
  if (!$may_cache) {
    // Add count of active anonymous/authenticated users.
    // @see user_block(), user.module
    $interval = time() - variable_get('user_block_seconds_online', 900);
    $count_anon = sess_count($interval);
    $count_auth = db_result(db_query("SELECT COUNT(DISTINCT uid) FROM {sessions} WHERE uid > 0 AND timestamp >= %d", $interval));

    $mid_admin  = $admin_menu['index']['admin'];
    $title = t('Current anonymous / authenticated users');
    $icon_users = '<img src="'. base_path() . drupal_get_path('module', 'admin_menu') .'/images/icon_users.png" width="16" height="15" alt="'. $title .'" title="'. $title .'" />';
    admin_menu_add_item($admin_menu, $mid_admin, array(
      'title' => $count_anon .' / '. $count_auth .' '. $icon_users,
      'path' => user_access('administer users') ? 'admin/user/user' : drupal_get_normal_path(variable_get('site_frontpage', 'node')),
      'weight' => -90,
      'class' => 'admin-menu-action admin-menu-icon admin-menu-users',
    ));
  }
}

/**
 * Implementation of hook_form_alter().
 *
 * Extends Devel module with Administration Menu developer settings.
 */
function admin_menu_form_alter($form_id, &$form) {
  if ($form_id == 'devel_admin_settings') {
    // Shift system_settings_form buttons.
    $weight = $form['buttons']['#weight'];
    $form['buttons']['#weight'] = $weight + 1;
    
    $form['admin_menu'] = array(
      '#type' => 'fieldset',
      '#title' => t('Administration Menu settings'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $display_options = array('mid', 'weight', 'pid');
    $display_options = array(0 => t('None'), 'mid' => t('Id'), 'weight' => t('Weight'), 'pid' => t('Parent id'));
    $form['admin_menu']['admin_menu_display'] = array(
      '#type' => 'radios',
      '#title' => t('Display extra information for menu items in Drupal Administration Menu'),
      '#default_value' => variable_get('admin_menu_display', 0),
      '#options' => $display_options,
      '#description' => t('If enabled, the chosen information will appear next to each menu item link.'),
    );
    $form['admin_menu']['admin_menu_show_all'] = array(
      '#type' => 'checkbox',
      '#title' => t('Display all menu items'),
      '#default_value' => variable_get('admin_menu_show_all', 0),
      '#description' => t('Enable this option to disable user access checks for menu items, i.e. every menu item in the visible menu tree will be displayed to every user regardless of access permissions.'),
    );
  }
}

/**
 * Implementation of hook_admin_menu() for Devel module (temporary).
 *
 * - Adds most used functions 'empty cache' and 'variable editor' to the menu in
 *   Administration Menu's icon.
 * - Adds links to switch to a different user to the logout button.
 */
if (module_exists('devel') && !function_exists('devel_admin_menu')) {
  function devel_admin_menu(&$admin_menu, $may_cache) {
    $access_devel = user_access('access devel information');
    $access_switch = user_access('switch users');
    if (!$access_devel && !$access_switch) {
      return;
    }
    
    $mid_icon = $admin_menu['index']['admin_menu_icon'];
    if ($may_cache) {
      // Add variable editor.
      if ($access_devel) {
        admin_menu_add_item($admin_menu, $mid_icon, array('title' => t('Variable editor'), 'path' => 'devel/variable', 'weight' => 20));
      }
    }
    else {
      // Add clear-cache.
      if ($access_devel) {
        admin_menu_add_item($admin_menu, $mid_icon, array('title' => t('Empty cache'), 'path' => 'devel/cache/clear', 'weight' => 20, 'query' => drupal_get_destination()));
      }
      // Add switch_user items.
      if ($access_switch && $devel_user_links = module_invoke('devel', 'switch_user_list')) {
        $mid_logout = $admin_menu['index']['logout'];
        foreach ($devel_user_links as $link) {
          if (preg_match('!href="'. base_path() .'([^\?]+)\?([^"]+)" title="([^"]+)">((<em>)?[^<]+(</em>)?)!', $link, $match)) {
            admin_menu_add_item($admin_menu, $mid_logout, array('title' => $match[4], 'description' => $match[3], 'path' => urldecode($match[1]), 'query' => $match[2]));
          }
        }
      }
    }
  }
}

/**
 * Clear the cached admin menu tree.
 */
function admin_menu_clear_cache() {
  // cache_clear_all() does not support a leading wildcard.
  db_query("DELETE FROM {cache_menu} WHERE cid LIKE '%%:admin_menu'");
}

/**
 * Implementation of hook_panels_cache().
 *
 * We're misusing the panels_cache hook here: whenever called, check if the
 * current request has a POST payload, and clear the cached admin menu.
 * This can easily be seen as a hack, as it will happen way too often when
 * working on the panels admin pages.
 */
function admin_menu_panels_cache() {
  if (!empty($_POST)) {
    admin_menu_clear_cache();
  }
}

/**
 * Menu callback to enable/disable developer modules.
 *
 * This saves up to 150ms on each uncached page request. Not much, but
 * on larger Drupal sites this is actually a 10% performance increase.
 */
function admin_menu_toggle_modules() {
  $saved_state = variable_get('admin_menu_devel_modules_enabled', NULL);
  if (isset($saved_state)) {
    // Re-enable modules that were enabled before.
    module_enable($saved_state);
    variable_del('admin_menu_devel_modules_enabled');
    drupal_set_message(t('Re-enabled these modules: !module-list.', array('!module-list' => implode(', ', $saved_state))));
  }
  else {
    // Allow site admins to override this variable via settings.php.
    $devel_modules = variable_get('admin_menu_devel_modules', array('cache_disable', 'coder', 'content_copy', 'debug', 'delete_all', 'demo', 'devel', 'devel_node_access', 'devel_themer', 'macro', 'form_controller', 'imagecache_ui', 'journal', 'trace', 'upgrade_status', 'user_display_ui', 'util', 'views_ui', 'views_theme_wizard'));
    // Store currently enabled modules in a variable.
    $devel_modules = array_intersect(module_list(FALSE, FALSE), $devel_modules);
    if (!empty($devel_modules)) {
      variable_set('admin_menu_devel_modules_enabled', $devel_modules);
      // Disable developer modules.
      module_disable($devel_modules);
      drupal_set_message(t('Disabled these modules: !module-list.', array('!module-list' => implode(', ', $devel_modules))));
    }
    else {
      drupal_set_message(t('No developer modules are enabled.'));
    }
  }
  drupal_goto(referer_uri());
}

