You are here

ack_menu.module in Access Control Kit 7

The ACK menu module.

File

ack_menu/ack_menu.module
View source
<?php

/**
 * @file
 * The ACK menu module.
 */

/**
 * Implements hook_access_info().
 */
function ack_menu_access_info() {

  // Declare support for controlling access to menu links.
  $info['menu_link'] = array(
    'label' => t('Menu link'),
  );
  return $info;
}

/**
 * Implements hook_access_handler_info().
 */
function ack_menu_access_handler_info() {

  // To keep URLs sane on management pages, only allow integer-based schemes.
  // Integers also make link-to-realm mapping much easier and more searchable.
  $integer_schemes = array();
  foreach (access_scheme_info() as $scheme_info) {
    if ($scheme_info['data_type'] == 'integer') {
      $integer_schemes[] = $scheme_info['type'];
    }
  }
  $info = array();

  // Handler to map menu links to realms.
  $info['AckMenuMap'] = array(
    'label' => t('Administratively assigned'),
    'scheme types' => $integer_schemes,
    'object types' => array(
      'menu_link',
    ),
  );

  // @todo Support access to a menu item based on the node it links to?
  return $info;
}

/**
 * Implements hook_permission().
 */
function ack_menu_permission() {
  $menus = implode(', ', _ack_menu_managed_menus());
  $schemes = implode(', ', access_object_schemes('menu_link', TRUE));
  return array(
    'administer ack_menu' => array(
      'title' => t('Administer menus for all schemes'),
      'description' => t('Manage the links in the following access scheme-controlled menus: %menus.', array(
        '%menus' => $menus,
      )),
    ),
    'ack manage menu links' => array(
      'title' => t('Manage menu links in assigned access realms'),
      'description' => t('This permission only applies to the following access schemes: %schemes.', array(
        '%schemes' => $schemes,
      )),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function ack_menu_menu() {
  $items['ack_menu'] = array(
    'title' => 'Manage menu links',
    'description' => 'Add, edit, and rearrange links for menu trees that you are permitted to manage.',
    'page callback' => 'ack_menu_overview_page',
    'access callback' => 'ack_menu_access',
    'menu_name' => 'navigation',
    'file' => 'ack_menu.pages.inc',
  );
  $items['ack_menu/list'] = array(
    'title' => 'List manageable links',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['ack_menu/manage/%access_scheme_machine_name/%'] = array(
    'title' => 'Manage realm menu links',
    'title callback' => 'ack_menu_overview_title',
    'title arguments' => array(
      2,
      3,
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'ack_menu_overview_form',
      2,
      3,
    ),
    'access callback' => 'ack_menu_realm_access',
    'access arguments' => array(
      2,
      3,
    ),
    'file' => 'ack_menu.pages.inc',
  );
  $items['ack_menu/manage/%access_scheme_machine_name/%/list'] = array(
    'title' => 'List links',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['ack_menu/manage/%access_scheme_machine_name/%/add'] = array(
    'title' => 'Add realm menu link',
    'title callback' => 'ack_menu_link_add_title',
    'title arguments' => array(
      2,
      3,
    ),
    'page callback' => 'ack_menu_link_add',
    'page arguments' => array(
      2,
      3,
    ),
    'access callback' => 'ack_menu_realm_access',
    'access arguments' => array(
      2,
      3,
    ),
    'type' => MENU_LOCAL_ACTION,
    'file' => 'ack_menu.pages.inc',
  );
  return $items;
}

/**
 * Implements hook_menu_alter().
 */
function ack_menu_menu_alter(&$items) {

  // Override the menu admin overview page.
  $items['admin/structure/menu']['page callback'] = 'ack_menu_admin_page';
  $items['admin/structure/menu']['access callback'] = 'ack_menu_admin_access';
  $items['admin/structure/menu']['file'] = 'ack_menu.pages.inc';
  $items['admin/structure/menu']['file path'] = drupal_get_path('module', 'ack_menu');
  unset($items['admin/structure/menu']['access arguments']);

  // Override access to the menu admin list and add pages.
  $paths = array(
    'admin/structure/menu/manage/%menu',
    'admin/structure/menu/manage/%menu/add',
  );
  foreach ($paths as $path) {
    $items[$path]['access callback'] = 'ack_menu_menu_access';
    $items[$path]['access arguments'] = array(
      4,
    );
  }

  // Override access to the menu link edit and delete operations.
  foreach (array(
    'edit',
    'delete',
  ) as $op) {
    $items['admin/structure/menu/item/%menu_link/' . $op]['access callback'] = 'ack_menu_link_access';
    $items['admin/structure/menu/item/%menu_link/' . $op]['access arguments'] = array(
      4,
    );
  }
}

/**
 * Implements hook_admin_paths().
 */
function ack_menu_admin_paths() {
  $paths = array(
    'ack_menu' => TRUE,
    'ack_menu/*' => TRUE,
  );
  return $paths;
}

/**
 * Menu title callback for the "manage realm links" page.
 *
 * @param object $scheme
 *   An access scheme.
 * @param int $realm
 *   A realm value.
 *
 * @return string
 *   The translated page title.
 */
function ack_menu_overview_title($scheme, $realm) {
  return t('Manage @realm menu links', array(
    '@realm' => $scheme->realms[$realm],
  ));
}

/**
 * Menu title callback for the "add realm link" page.
 *
 * @param object $scheme
 *   An access scheme.
 * @param int $realm
 *   A realm value.
 *
 * @return string
 *   The translated page title.
 */
function ack_menu_link_add_title($scheme, $realm) {
  return t('Add @realm menu link', array(
    '@realm' => $scheme->realms[$realm],
  ));
}

/**
 * Access callback for the realm menu manager.
 *
 * @param object $account
 *   (optional) A user account. Defaults to the currently logged-in user.
 *
 * @return bool
 *   TRUE if the user account has access to the realm menu manager.
 */
function ack_menu_access($account = NULL) {
  return user_access('ack manage menu links', $account) || user_access('administer ack_menu', $account) || user_access('administer menu', $account);
}

/**
 * Access callback for the menu admin pages.
 *
 * @param object $account
 *   (optional) A user account. Defaults to the currently logged-in user.
 *
 * @return bool
 *   TRUE if the user account has access to the menu admin pages.
 */
function ack_menu_admin_access($account = NULL) {
  return user_access('administer menu', $account) || user_access('administer ack_menu', $account);
}

/**
 * Access callback for managing a menu.
 *
 * @param array $menu
 *   An array defining a menu.
 * @param object $account
 *   (optional) A user account. Defaults to the currently logged-in user.
 *
 * @return bool
 *   TRUE if the user account has access to manage the menu.
 */
function ack_menu_menu_access($menu, $account = NULL) {

  // Global menu administrators have access to all menus.
  if (user_access('administer menu', $account)) {
    return TRUE;
  }

  // Scheme menu administrators have access to menus managed by ACK.
  if (user_access('administer ack_menu', $account)) {
    $menu_name = $menu['menu_name'];
    $managed = _ack_menu_managed_menus();
    return isset($managed[$menu_name]);
  }
  return FALSE;
}

/**
 * Access callback for managing realm links.
 *
 * @param object $scheme
 *   An access scheme.
 * @param int $realm
 *   A realm value.
 * @param object $account
 *   (optional) A user account. Defaults to the currently logged-in user.
 *
 * @return bool
 *   TRUE if the user account has access to manage links in the given realm.
 */
function ack_menu_realm_access($scheme, $realm, $account = NULL) {

  // Validate the scheme.
  if (isset($scheme->handlers['menu_link'])) {

    // Validate the realm value.
    if (is_numeric($realm) && isset($scheme->realms[$realm])) {

      // Admin access trumps any realm-level access.
      if (ack_menu_admin_access($account)) {
        return TRUE;
      }

      // Check realm access and that manageable links exist.
      if (ack_menu_access($account)) {
        $allowed_realms = access_user_permission_realms('ack manage menu links', $account, array(
          $scheme,
        ));
        if (in_array($realm, $allowed_realms[$scheme->machine_name])) {
          $links = ack_menu_realm_links($scheme, $realm);
          return !empty($links);
        }
      }
    }
  }
  return FALSE;
}

/**
 * Access callback for editing or deleting a menu link.
 *
 * @param array $menu_link
 *   The menu link.
 * @param object $account
 *   (optional) A user account. Defaults to the currently logged-in user.
 *
 * @return bool
 *   TRUE if the user account has access to edit or delete the link.
 */
function ack_menu_link_access($menu_link, $account = NULL) {

  // Check for overall access to the link's menu.
  $menu = menu_load($menu_link['menu_name']);
  if (ack_menu_menu_access($menu, $account)) {
    return TRUE;
  }

  // Check access grants.
  return access_user_object_access('ack manage menu links', 'menu_link', $menu_link, $account);
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Alters the menu item add/edit form.
 */
function ack_menu_form_menu_edit_item_alter(&$form, &$form_state, $form_id) {
  $item = $form['original_item']['#value'];
  _ack_menu_form_alter($form, $form_state, $form_id, $item);
  $form['#validate'][] = 'ack_menu_form_menu_edit_item_validate';
  $form['#submit'][] = 'ack_menu_form_menu_edit_item_submit';
  if (isset($form['actions']['delete'])) {
    $form['actions']['delete']['#submit'] = array(
      'ack_menu_item_delete_submit',
    );
  }
}

/**
 * Validation handler for the altered menu item form.
 *
 * @see ack_menu_form_menu_edit_item_alter()
 */
function ack_menu_form_menu_edit_item_validate($form, &$form_state) {
  $item = $form_state['values'];
  foreach ($form_state['ack_menu']['schemes'] as $scheme) {
    $scheme->handlers['menu_link']
      ->objectFormValidate('menu_link', $item, $form, $form_state);
  }
}

/**
 * Submit handler for the altered menu item form.
 *
 * @see ack_menu_form_menu_edit_item_alter()
 */
function ack_menu_form_menu_edit_item_submit($form, &$form_state) {
  $item = $form_state['values'];
  foreach ($form_state['ack_menu']['schemes'] as $scheme) {
    $scheme->handlers['menu_link']
      ->objectFormSubmit('menu_link', $item, $form, $form_state);
  }
  if (isset($form_state['ack_menu']['destination'])) {
    $form_state['redirect'] = $form_state['ack_menu']['destination'];
  }
}

/**
 * Submit handler for the delete button on the altered menu item form.
 *
 * @see ack_menu_form_menu_edit_item_alter()
 * @see menu_item_delete_submit()
 */
function ack_menu_item_delete_submit($form, &$form_state) {
  $destination = array();
  if (isset($_GET['destination'])) {
    $destination = drupal_get_destination();
    unset($_GET['destination']);
  }
  $form_state['redirect'] = array(
    'admin/structure/menu/item/' . $form_state['values']['mlid'] . '/delete',
    array(
      'query' => $destination,
    ),
  );
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 *
 * Alters the menu options on the node form.
 */
function ack_menu_form_node_form_alter(&$form, &$form_state, $form_id) {
  if (!empty($form['menu'])) {
    $link = $form['#node']->menu;
    $menus = variable_get('menu_options_' . $form['#node']->type, array());
    $access =& $form['menu']['#access'];

    // If the link exists, see if the user is allowed to manage it.
    if (!empty($link['mlid'])) {
      $access = ack_menu_link_access($link);
    }
    elseif (ack_menu_admin_access()) {
      while (!$access && ($menu_name = array_pop($menus))) {
        $menu = menu_load($menu_name);
        if (!empty($menu)) {
          $access = ack_menu_menu_access($menu);
        }
      }
    }
    elseif (ack_menu_access()) {
      $realms = _ack_menu_user_realms();
      while (!$access && (list($scheme_machine_name, $scheme_realms) = each($realms))) {
        $scheme = access_scheme_machine_name_load($scheme_machine_name);
        while (!$access && ($realm = array_pop($scheme_realms))) {
          $realm_menus = ack_menu_realm_menus($scheme, $realm);
          $allowed = array_intersect($menus, $realm_menus);
          $access = !empty($allowed);
        }
      }
    }

    // If we granted access above, filter the parent link options.
    if ($access) {
      _ack_menu_form_alter($form['menu']['link'], $form_state, $form_id, $link);
    }
  }
}

/**
 * Helper function for altering the menu item and node forms.
 *
 * @param array &$form
 *   A form structure representing a menu item. Must have 'parent' and 'weight'
 *   element children.
 * @param array &$form_state
 *   The form state. If 'ack_menu' is already defined in the array, it must
 *   contain 'schemes' and 'realms' values, defining (respectively) the schemes
 *   that can manage the link and the realms that the user can assign to it.
 * @param string $form_id
 *   The form identifier.
 * @param array $link
 *   The menu link represented by the form.
 *
 * @see ack_menu_form_menu_edit_item_alter()
 * @see ack_menu_form_node_form_alter()
 */
function _ack_menu_form_alter(&$form, &$form_state, $form_id, $link) {

  // Place ack_menu values in the form state for use by the handlers.
  if (!isset($form_state['ack_menu'])) {
    $form_state['ack_menu'] = array(
      'schemes' => access_object_schemes('menu_link'),
      'realms' => _ack_menu_user_realms(),
    );
  }
  $form_state['ack_menu']['admin'] = user_access('administer ack_menu');
  $form_state['ack_menu']['global admin'] = user_access('administer menu');
  $schemes = $form_state['ack_menu']['schemes'];
  $realms = $form_state['ack_menu']['realms'];

  // Apply AckMenuHandlerInterface::objectFormAlter() for all schemes.
  foreach ($schemes as $scheme) {

    // A new link can be added to any allowed realm, and admins can change the
    // realm assignments of existing links.
    if (empty($link['mlid']) || $form_state['ack_menu']['admin']) {
      $scheme_realms = isset($realms[$scheme->machine_name]) ? $realms[$scheme->machine_name] : array();
    }
    else {

      // For non-admins, an existing link must stay in its current realm.
      $scheme_realms = NULL;
    }
    $scheme->handlers['menu_link']
      ->objectFormAlter('menu_link', $link, $form, $form_state, $form_id, $scheme_realms);
  }

  // If this link's current parent is not manageable by the user, then the user
  // should not be able to change this link's position, since that would
  // effectively be rearranging the parent's menu tree.
  if (!empty($link['mlid']) && !empty($link['plid'])) {
    $parent = menu_link_load($link['plid']);
    if (!empty($parent)) {
      $form['weight']['#disabled'] = !ack_menu_link_access($parent);
    }
  }
}

/**
 * Implements hook_menu_link_delete().
 */
function ack_menu_menu_link_delete($link) {
  db_delete('ack_menu_map')
    ->condition('mlid', $link['mlid'])
    ->execute();
}

/**
 * Implements hook_access_scheme_delete().
 */
function ack_menu_access_scheme_delete($scheme) {
  db_delete('ack_menu_map')
    ->condition('scheme', $scheme->machine_name)
    ->execute();
}

/**
 * Loads the menu links for a realm.
 *
 * @param object $scheme
 *   An access scheme.
 * @param int $realm
 *   A realm value.
 * @param string $menu_name
 *   (optional) Limit the returned links to the given menu. If omitted, links
 *   from all menus will be included. Defaults to NULL.
 * @param bool $reset
 *   (optional) Whether to reset the internal cache. Defaults to FALSE.
 *
 * @return array
 *   The realm's menu links, sorted by menu and depth. Each value is an array
 *   defining a link, similar to what would be returned by menu_link_load().
 */
function ack_menu_realm_links($scheme, $realm, $menu_name = NULL, $reset = FALSE) {
  $links =& drupal_static(__FUNCTION__, array());
  if (!isset($links[$scheme->sid][$realm]) || $reset) {
    $realm_links = array();

    // Validate the scheme.
    if (isset($scheme->handlers['menu_link'])) {

      // Validate the realm value.
      if (is_numeric($realm) && isset($scheme->realms[$realm])) {
        $managed = $scheme->handlers['menu_link']
          ->managedMenus();
        ksort($managed);
        foreach (array_keys($managed) as $menu) {
          $realm_links[$menu] = $scheme->handlers['menu_link']
            ->realmLinks($realm, $menu);
        }
      }
    }
    $links[$scheme->sid][$realm] = $realm_links;
  }
  $list = array();
  if (isset($menu_name)) {
    if (isset($links[$scheme->sid][$realm][$menu_name])) {
      $list = $links[$scheme->sid][$realm][$menu_name];
    }
  }
  else {
    foreach ($links[$scheme->sid][$realm] as $menu_links) {
      if (!empty($menu_links)) {
        $list = array_merge($list, $menu_links);
      }
    }
  }
  return $list;
}

/**
 * Returns a list of menus that contain links for a realm.
 *
 * @param object $scheme
 *   An access scheme.
 * @param int $realm
 *   A realm value.
 * @param bool $reset
 *   (optional) Whether to reset the internal cache. Defaults to FALSE.
 *
 * @return array()
 *   An array of menu names.
 */
function ack_menu_realm_menus($scheme, $realm, $reset = FALSE) {
  $menus =& drupal_static(__FUNCTION__, array());
  if (!isset($menus[$scheme->sid][$realm]) || $reset) {
    $realm_menus = array();
    if (isset($scheme->handlers['menu_link'])) {

      // If we're resetting the cache, clear the realm links cache once here, so
      // we don't do it repeatedly in the foreach statement below.
      if ($reset) {
        ack_menu_realm_links($scheme, $realm, NULL, TRUE);
      }

      // Find all managed menus that contain links.
      $managed = $scheme->handlers['menu_link']
        ->managedMenus();
      foreach (array_keys($managed) as $menu_name) {
        $menu_links = ack_menu_realm_links($scheme, $realm, $menu_name);
        if (!empty($menu_links)) {
          $realm_menus[] = $menu_name;
        }
      }
    }
    $menus[$scheme->sid][$realm] = $realm_menus;
  }
  return $menus[$scheme->sid][$realm];
}

/**
 * Helper function to find the list of menus that are managed by access schemes.
 *
 * @return array
 *   An array with the machine-readable names as the keys, and human-readable
 *   titles as the values.
 */
function _ack_menu_managed_menus() {
  $managed = array();
  foreach (access_object_schemes('menu_link') as $scheme) {
    $managed += $scheme->handlers['menu_link']
      ->managedMenus();
  }
  return $managed;
}

/**
 * Helper function to find the realms where the user is allowed to manage links.
 *
 * @return array
 *   An array indexed by scheme machine name where the values are arrays of
 *   realm values manageable by the currently logged-in user.
 */
function _ack_menu_user_realms() {
  $schemes = access_object_schemes('menu_link');
  $realms = array();

  // Admins can access all realm menus.
  if (ack_menu_admin_access()) {
    foreach ($schemes as $scheme) {
      $realms[$scheme->machine_name] = array_keys($scheme->realms);
    }
  }
  else {

    // Otherwise, find the realms where the user has been granted access.
    foreach ($schemes as $scheme) {
      foreach (array_keys($scheme->realms) as $realm) {
        if (ack_menu_realm_access($scheme, $realm)) {
          $realms[$scheme->machine_name][] = $realm;
        }
      }
    }
  }
  return $realms;
}

Functions

Namesort descending Description
ack_menu_access Access callback for the realm menu manager.
ack_menu_access_handler_info Implements hook_access_handler_info().
ack_menu_access_info Implements hook_access_info().
ack_menu_access_scheme_delete Implements hook_access_scheme_delete().
ack_menu_admin_access Access callback for the menu admin pages.
ack_menu_admin_paths Implements hook_admin_paths().
ack_menu_form_menu_edit_item_alter Implements hook_form_FORM_ID_alter().
ack_menu_form_menu_edit_item_submit Submit handler for the altered menu item form.
ack_menu_form_menu_edit_item_validate Validation handler for the altered menu item form.
ack_menu_form_node_form_alter Implements hook_form_BASE_FORM_ID_alter().
ack_menu_item_delete_submit Submit handler for the delete button on the altered menu item form.
ack_menu_link_access Access callback for editing or deleting a menu link.
ack_menu_link_add_title Menu title callback for the "add realm link" page.
ack_menu_menu Implements hook_menu().
ack_menu_menu_access Access callback for managing a menu.
ack_menu_menu_alter Implements hook_menu_alter().
ack_menu_menu_link_delete Implements hook_menu_link_delete().
ack_menu_overview_title Menu title callback for the "manage realm links" page.
ack_menu_permission Implements hook_permission().
ack_menu_realm_access Access callback for managing realm links.
ack_menu_realm_links Loads the menu links for a realm.
ack_menu_realm_menus Returns a list of menus that contain links for a realm.
_ack_menu_form_alter Helper function for altering the menu item and node forms.
_ack_menu_managed_menus Helper function to find the list of menus that are managed by access schemes.
_ack_menu_user_realms Helper function to find the realms where the user is allowed to manage links.