diff --git a/masquerade.info b/masquerade.info --- a/masquerade.info Tue Jun 21 17:30:07 2011 +++ b/masquerade.info Tue Jun 21 17:30:15 2011 @@ -2,4 +2,5 @@ description = "This module allows permitted users to masquerade as other users." core = 7.x files[] = masquerade.test +files[] = views/masquerade_handler_field_user_link.inc configure = admin/config/people/masquerade diff --git a/masquerade.module b/masquerade.module --- a/masquerade.module Wed May 04 08:58:00 2011 +++ b/masquerade.module Tue Jun 21 17:34:26 2011 @@ -23,15 +22,23 @@ * @return array */ function masquerade_permission() { - return array( - 'masquerade as user' => array( - 'title' => t('Masquerade as user'), - 'description' => t('Masquerade as another user.'), - ), - 'masquerade as admin' => array( - 'title' => t('Masquerade as admin'), - 'description' => t('Masquerade as the site admin (UID 1).'), - ), + $roles = array(); + // load and build permissions for all roles in our site + // filter off the Anonymous role. + foreach (user_roles() as $rid => $role) { + $title = 'masquerade as '. $role; + $key = drupal_strtolower($title); + $description = t('Masquerade as a member of the @role role.', array('@role' => $role)); + if ($rid == DRUPAL_AUTHENTICATED_RID) { + $description = t('Grant this permission with caution. Any user signed in is given this role.'); + } + $roles[$title] = array( + 'title' => t($title), + 'description' => $description, + ); + } + + return $roles + array( 'administer masquerade' => array( 'title' => t('Administer Masquerade'), 'description' => t('Perform administration tasks and configure the Masquerade module.'), @@ -188,7 +194,7 @@ function masquerade_user_operations() { return array( 'masquerade' => array( - 'label' => t('Masquerade as user'), + 'label' => t('Masquerade as this user'), 'callback' => 'masquerade_user_operations_masquerade', ), ); @@ -223,7 +229,7 @@ case 'unswitch': return isset($_SESSION['masquerading']) || arg(2) == 'menu-customize' || arg(2) == 'menu'; case 'autocomplete': - return isset($_SESSION['masquerading']) || (user_access('masquerade as user') || user_access('masquerade as admin')); + return isset($_SESSION['masquerading']) || masquerade_has_any_masquerade_permission(); break; case 'user': global $user; @@ -232,7 +238,6 @@ case 'switch': $switch_to_account = FALSE; global $user; - if ($uid) { if (!is_numeric($uid)) { return FALSE; } @@ -241,8 +246,11 @@ ':uid_from' => $user->uid, ':uid_to' => $account->uid ))->fetchField(); + // ensure the user we want to switch to has only roles we're permitted to access. + if (!masquerade_check_user_roles($account)) { + return FALSE; } - return !isset($_SESSION['masquerading']) && (user_access('masquerade as user') || user_access('masquerade as admin') || $switch_to_account); + return !isset($_SESSION['masquerading']) || $switch_to_account; break; } } @@ -252,25 +260,6 @@ */ function masquerade_admin_settings() { // create a list of roles; all selected roles are considered administrative. - $roles = user_roles(); - $form['masquerade_admin_roles'] = array( - '#type' => 'checkboxes', - '#title' => t('Roles that are considered "administrators" for masquerading'), - '#options' => $roles, - '#default_value' => variable_get('masquerade_admin_roles', array()), - ); - - $test_name = _masquerade_user_load(variable_get('masquerade_test_user', '')); - - $form['masquerade_test_user'] = array( - '#type' => 'textfield', - '#title' => t('Menu Quick Switch user'), - '#autocomplete_path' => 'masquerade/autocomplete', - '#default_value' => isset($test_name->name) ? check_plain($test_name->name) : '', - '#description' => t('Enter the username of an account you wish to switch easily between via a menu item.'), - '#maxlength' => NULL, - ); - $quick_switch = user_load_multiple(variable_get('masquerade_quick_switches', array())); $quick_switch_users = array(); foreach ($quick_switch as $uid => $account) { @@ -296,15 +285,6 @@ } function masquerade_admin_settings_validate($form, &$form_state) { - if (!empty($form_state['values']['masquerade_test_user'])) { - $test_user = _masquerade_user_load($form_state['values']['masquerade_test_user']); - if (!$test_user) { - form_set_error('masquerade_test_user', t('%user does not exist. Please enter a valid username.', array('%user' => $form_state['values']['masquerade_test_user']))); - } - } - // Needs to rebuild menu in masquerade_admin_settings_submit(). - $form_state['masquerade_rebuild_menu'] = (variable_get('masquerade_test_user', '') != $form_state['values']['masquerade_test_user']); - // A comma-separated list of users. $masquerade_switches = drupal_explode_tags($form_state['values']['masquerade_quick_switches']); // Change user names to user ID's for system_settings_form_submit() to save. @@ -373,15 +353,16 @@ * Implements hook_user_view(). */ function masquerade_user_view($account, $view_mode, $langcode) { - // check if user qualifies as admin - $roles = array_keys(array_filter(variable_get('masquerade_admin_roles', array()))); - $perm = $account->uid == 1 || array_intersect(array_keys((array)$account->roles), $roles) ? - 'masquerade as admin' : - 'masquerade as user'; - global $user; + // make sure the account viewed isn't our own + // and that we have adequate permissions to masquerade as it + if ($user->uid == $account->uid || !masquerade_check_user_roles($account)) { + return; + } - if (user_access($perm) && empty($account->masquerading) && $user->uid != $account->uid) { + // Ensure we have access to: the user page, that we are not presently masquerading, + // that the current account isn't our own, and that the user we're looking at only has roles we can become. + if (empty($account->masquerading)) { $account->content['masquerade'] = array( '#markup' => l(t('Masquerade as !user', array('!user' => $account->name)), 'masquerade/switch/' . $account->uid, @@ -439,6 +420,9 @@ if (!_masquerade_user_load($username)) { form_set_error('masquerade_users', t('%user is not a valid user name.', array('%user' => $username))); } + if (!masquerade_check_user_roles($username)) { + form_set_error('masquerade_users', t('This account has been assigned roles you are not permitted to access.')); + } } } } @@ -527,8 +511,10 @@ $block = array(); switch ($delta) { case 'masquerade': + if (isset($_SESSION['masquerading']) || masquerade_has_any_masquerade_permission()) { $block['subject'] = t('Masquerade'); $block['content'] = drupal_get_form('masquerade_block_1'); + } break; } return $block; @@ -560,6 +546,10 @@ foreach ($masquerade_switches as $switch_user) { if (!isset($_SESSION['user']->uid) || $switch_user != $_SESSION['user']->uid) { $user_name = user_load($switch_user); + // make sure the users in the quick switch list only have roles we're allowed to become. + if (!masquerade_check_user_roles($user_name)) { + continue; + } $switch_link = 'masquerade/switch/' . $user_name->uid; if ($user_name->uid) { $quick_switch_links[] = l($user_name->name, $switch_link, array('query' => array('token' => drupal_get_token($switch_link)))); @@ -625,6 +615,9 @@ if (!$masq_user) { form_set_error('masquerade_user_field', t('User %masq_as does not exist. Please enter a valid username.', array('%masq_as' => $form_state['values']['masquerade_user_field']))); } + elseif (!masquerade_check_user_roles($masq_user)) { + form_set_error('masquerade_user_field', t('You cannot masquerade as %masq_as because this person has roles you are not permitted to switch to.', array('%masq_as' => $form_state['values']['masquerade_user_field']))); + } elseif ($masq_user->uid == $user->uid) { form_set_error('masquerade_user_field', t('You cannot masquerade as yourself. Please choose a different user to masquerade as.')); } @@ -757,23 +750,14 @@ $new_user = user_load($uid); - $roles = array_keys(array_filter(variable_get('masquerade_admin_roles', array()))); - $perm = $uid == 1 || array_intersect(array_keys($new_user->roles), $roles) ? - 'masquerade as admin' : - 'masquerade as user'; - - // Check to see if we need admin permission. - $results = db_query('SELECT TRUE FROM {masquerade_users} WHERE uid_from = :uid_from AND uid_to = :uid_to', array( - ':uid_from' => $user->uid, - ':uid_to' => $new_user->uid, - )); - if (!user_access($perm) && !isset($_SESSION['masquerading']) && !$results->fetchField()) { - watchdog('masquerade', 'This user requires administrative permissions to switch to the user %user.', array('%user' => $new_user->name), WATCHDOG_ERROR); + if ($user->uid == $uid || isset($user->masquerading)) { + watchdog('masquerade', 'This user is already %user.', array('%user' => $new_user->name), WATCHDOG_ERROR); return FALSE; } - if ($user->uid == $uid || isset($user->masquerading)) { - watchdog('masquerade', 'This user is already %user.', array('%user' => $new_user->name), WATCHDOG_ERROR); + // ensure the account we're switching to isn't our own and that we have permission to masquerade with these roles + if ($user->uid !== $new_user->uid && !masquerade_check_user_roles($new_user)) { + watchdog('masquerade', 'The user %user has roles %user2 does not have permission to obtain.', array('%user' => $new_user->name, '%user2' => $user->name), WATCHDOG_ERROR); return FALSE; } @@ -781,6 +765,7 @@ drupal_set_message(t('It is not possible to masquerade in off-line mode as %user does not have the %config-perm permission. Please set the site status to "online" to masquerade as %user.', array('%user' => $new_user->name, '%config-perm' => 'administer site configuration', '@site-maintenance' => url('admin/settings/site-maintenance')))); return FALSE; } + // hooray, we made it! $query = db_insert('masquerade'); $query->fields(array( @@ -836,4 +821,134 @@ $oldname = ($user->uid == 0 ? variable_get('anonymous', t('Anonymous')) : $user->name); $user = user_load($uid); watchdog('masquerade', 'User %user no longer masquerading as %masq_as.', array('%user' => $user->name, '%masq_as' => $oldname), WATCHDOG_INFO); +} + +/** + * Implements hook_views_api() + */ +function masquerade_views_api() { + return array( + 'api' => 2, + 'path' => drupal_get_path('module', 'masquerade') . '/views', + ); +} + +/** + * Tell ctools where to look for plugins + */ +function masquerade_ctools_plugin_directory($owner, $plugin_type) { + // Treat ctools requests similarly to the way they normally work. + if ($owner == 'ctools') { + return "ctools/$plugin_type"; + } + // we don't know what's being requsted + return ''; +} + +/** + * Adds a masquerading condition to the context module + */ +function masquerade_context_plugins() { + $plugins = array(); + $plugins['masquerade_is_masquerading'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'masquerade') .'/context/condition', + 'file' => 'masquerade_is_masquerading.inc', + 'class' => 'masquerade_is_masquerading_context_condition', + 'parent' => 'context_condition', + ), + ); + return $plugins; +} + +/** + * Tells the Context module to look for our custom plugins. + */ +function masquerade_context_registry() { + return array( + 'conditions' => array( + 'masquerade_is_masquerading' => array( + 'title' => t('User is Masquerading'), + 'description' => t('Sets this context when the current user is posing as someone else.'), + 'plugin' => 'masquerade_is_masquerading', + ), + ), + ); +} + +/** + * Triggers our custom context reactions. + * Why isn't this handled by the Context plugin manager?? + */ +function masquerade_context_page_reaction() { + if ($plugin = context_get_plugin('condition', 'masquerade_is_masquerading')) { + $plugin->execute(); + } +} + +/** + * Checks if the user has access to masquerade as a specific role. + * @return bool + * Whether or not the current user is allowed to masquerade as someone with this role. + */ +function masquerade_check_role_permission($role) { + $permission = stripos($role, 'masquerade as') ? $role : 'masquerade as ' . $role; + return user_access($permission); +} + +/** + * Checks an array of roles to see if we have access to them. + * @param $roles + * An array of roles to check + * @return bool + * Return FALSE if any role exists that the current user isn't permitted to become. + * Return TRUE otherwise. + */ +function masquerade_check_roles($roles = array()) { + foreach ($roles as $rid => $role) { + if ($rid == DRUPAL_AUTHENTICATED_RID) { + continue; + } + if (!masquerade_check_role_permission($role)) { + return FALSE; + } + } + return TRUE; +} + +/** + * Checks all roles of the user we're trying to become. + * @param mixed + * The user object to check roles. + * @return bool + * Return FALSE if any role exists that the current user isn't permitted to become. + * Return TRUE otherwise. + */ +function masquerade_check_user_roles($user) { + if (is_numeric($user)) { + $user = user_load($user); + } + elseif (is_string($user)) { + $user = user_load_by_name($user); + } + elseif (!is_object($user) || !isset($user->roles)) { + return FALSE; + } + return masquerade_check_roles($user->roles); +} + +/** + * Check if the user has ANY masquerade permission. + * @return bool + * Return TRUE if user has access to masquerade at all. + * Return FALSE if user can't masquerade. + */ +function masquerade_has_any_masquerade_permission() { + $return = FALSE; + foreach(user_roles() as $rid => $role) { + if (masquerade_check_role_permission($role)) { + $return = TRUE; + } + } + return $return; } diff --git a/context/condition/masquerade_is_masquerading.inc b/context/condition/masquerade_is_masquerading.inc --- a/context/condition/masquerade_is_masquerading.inc Tue Jun 21 17:28:50 2011 +++ b/context/condition/masquerade_is_masquerading.inc Fri May 27 10:27:20 2011 @@ -0,0 +1,27 @@ + t('Evaluate this context when the current user is posing as someone else.')); + } + + /** + * @note + * hook_context_reaction() is required of your module and will have to call $plugin->execute() + * or this plugin won't do anything. + */ + function execute() { + if (isset($_SESSION['masquerading']) && is_numeric($_SESSION['masquerading'])) { + foreach ($this->get_contexts() as $context) { + $this->condition_met($context, 'is_masquerading'); + } + } + } +} diff --git a/ctools/access/is_masquerading.inc b/ctools/access/is_masquerading.inc --- a/ctools/access/is_masquerading.inc Tue Jun 21 17:28:36 2011 +++ b/ctools/access/is_masquerading.inc Thu May 26 08:58:00 2011 @@ -0,0 +1,39 @@ + t('User: Is Masquerading'), + 'description' => t('Checks whether or not a user is masquerading as another.'), + 'callback' => 'masquerade_is_masquerading_access_check', + 'default' => array( + ), + 'summary' => 'masquerade_is_masquerading_access_summary', + 'required context' => array(), +); + +/** + * Checks if the user is currently masquerading as another. + * If no context is provided, this will automatically return FALSE. + * @return + * TRUE if the current user is masquerading, FALSE otherwise. + */ +function masquerade_is_masquerading_access_check($conf, $context) { + if (isset($_SESSION['masquerading']) && is_numeric($_SESSION['masquerading'])) { + return TRUE; + } + else { + return FALSE; + } +} + +/** + * Provides a simple access summary to administrators for this plugin. + * @return + * A string which states whether or not the current user is masquerading. + */ +function masquerade_is_masquerading_access_summary($conf, $context) { + return t('Current User: is masquerading as someone else.'); +} diff --git a/views/masquerade.views.inc b/views/masquerade.views.inc --- a/views/masquerade.views.inc Tue Jun 21 17:28:07 2011 +++ b/views/masquerade.views.inc Thu May 26 11:17:42 2011 @@ -0,0 +1,106 @@ + array( + 'left_table' => 'users', + 'left_field' => 'uid', + 'field' => 'uid_as', + ), + 'user_is' => array( + 'left_table' => 'users', + 'left_field' => 'uid', + 'field' => 'uid_from', + ), + ); + + // A faux field that should allow us to access any user ID + $data['users']['masquerade_as'] = array( + // tell views that 'masquerade_as' isn't a real field + // and to use 'uid' instead + 'real field' => 'uid', + 'title' => t('Masquerade Link'), + 'help' => t('The user you want to masquerade as.'), + 'field' => array( + 'handler' => 'masquerade_handler_field_user', + 'click sortable' => TRUE, + ), + 'relationship' => array( + 'handler' => 'views_handler_relationship', + 'base' => 'users', + 'base field' => 'uid', + 'field' => 'uid', + 'label' => t('user'), + ), + ); + + $data['masquerade']['sid'] = array( + 'title' => t('Session SID'), + 'help' => t('The session ID of the person masquerading.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + ); + + // Who is BEING masqueraded. + $data['masquerade']['uid_as'] = array( + 'title' => t('Masquerade As'), + 'help' => t('The user this person is posing as.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'relationship' => array( + 'handler' => 'views_handler_relationship', + 'base' => 'users', + 'base field' => 'uid', + 'field' => 'uid_as', + 'label' => t('Masquerading as'), + ), + ); + + // Who actually IS masquerading. + $data['masquerade']['uid_from'] = array( + 'title' => t('Masquerade From'), + 'help' => t('The user this person actually is.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'relationship' => array( + 'handler' => 'views_handler_relationship', + 'base' => 'users', + 'base field' => 'uid', + 'field' => 'uid_from', + 'label' => t('Who is masquerading'), + ), + ); + + return $data; +} + +/** + * Use hook_views_handlers() to avoid forcing our handler to be loaded + * on every page call with files[] in the .info file. + */ +function masquerade_views_handlers() { + return array( + 'info' => array( + 'path' => drupal_get_path('module', 'masquerade') . '/views', + ), + 'handlers' => array( + 'masquerade_handler_field_user' => array( + 'parent' => 'views_handler_field_user' + ), + ), + ); +} diff --git a/views/masquerade_handler_field_user_link.inc b/views/masquerade_handler_field_user_link.inc --- a/views/masquerade_handler_field_user_link.inc Tue Jun 21 17:28:14 2011 +++ b/views/masquerade_handler_field_user_link.inc Tue May 31 10:01:10 2011 @@ -0,0 +1,80 @@ + FALSE); + $options['custom_destination'] = array('default' => ''); + return $options; + } + + /** + * Adds additional options to the Field options form + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['provide_custom_destination'] = array( + '#title' => t('Provide a custom destination.'), + '#description' => t('This is helpful for providing a shortcut to a certain function of the site from one area or another.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['provide_custom_destination']) ? $this->options['provide_custom_destination'] : FALSE, + ); + + // provide the an optional redirect immediately after switching to another user + $form['custom_destination'] = array( + '#title' => t('Alternate destination'), + '#description' => t('Redirect the browser to this page immediately after switching to another user. There is a caveat, however. When you switch back, masquerade will not remember where you last were.'), + '#type' => 'textfield', + '#dependency' => array( 'edit-options-provide-custom-destination' => array(TRUE) ), + '#default_value' => !empty($this->options['custom_destination']) && valid_url($this->options['custom_destination']) ? $this->sanitize_value($this->options['custom_destination']) : '', + ); + + // override the normal link-to title and description + $form['link_to_user']['#title'] = t('Generate a link to Masquerade as this user.'); + $form['link_to_user']['#description'] = t('Be careful not to rewrite this field with tokens that generate links. This field will only display a link to users who have permission to masquerade.'); + } + + /** + * Generates a link + */ + function render_link($data, $values) { + if (isset($values->{$this->aliases['uid']}) && + masquerade_check_user_roles($values->{$this->aliases['uid']})) { + $data !== NULL && + $data !== '' && + // make sure any data being used to generate the link is clean + $switchto = $this->sanitize_value($values->{$this->aliases['uid']}); + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "masquerade/switch/" . $switchto; + + // let drupal know who we are before we switch users. + $this->options['alter']['query'] = array('token' => drupal_get_token($this->options['alter']['path'])); + + // if the user provided a custom destination (that isn't blank), tack it onto the link + if ($this->options['provide_custom_destination']== TRUE && + $this->options['custom_destination'] != '') { + $this->options['alter']['query'] += array('destination' => $this->options['custom_destination']); + } + + return $data; + } + return ''; + } + +}