Error message

Deprecated function: implode(): Passing glue string after array is deprecated. Swap the parameters in drupal_get_feeds() (line 394 of /home1/tylerfra/public_html/includes/common.inc).

Drupal Webform Submission Results User Access Control

Category: 

Have you ever wanted to grant access to a certain list of users so they could access a Drupal Webform's submission results? The default permissions that come with Webform don't provide much granularity in this scenario, so a custom solution needs to be implemented.

Luckily Webform has a few hooks to make our lives easier.

  • hook_webform_results_access
  • hook_webform_submission_access

By adding an unlimited value user reference field to the Webform content type (e.g. field_webform_results_access) and implementing the two hooks above, this can be accomplished. The user reference field will allow you to attach users to a webform node, then if that user is listed in that field on the webform node, then they can access the webform submission results. We just have to write a little code to accomplish this, for example:

Draupl 6

/**
 * Implementation of hook_webform_results_access().
 */
function my_module_webform_results_access($node, $account = NULL) {
  return my_module_webform_access($node, $account);
}

/**
 * Implementation of hook_webform_submission_access().
 */
function my_module_webform_submission_access($node, $submission, $op = 'view', $account = NULL) {
  return my_module_webform_access($node, $account);
}

/**
 * Returns true if account is a value in the webform results user
 * reference field on the webform content type.
 *
 * @param object $node
 *   The webform node.
 * @param object $account
 *   The user account, optional. Defaults to current user.
 *
 * @return bool
 *   Returns true if user is listed, false otherwise.
 */
function my_module_webform_access ($node, $account = NULL) {
  global $user;
  $account = isset($account) ? $account : $user;
  $access = false;
  if (isset($node->field_webform_results_access[0]['uid'])) { // Drupal 6
    // This webform has user(s) specified for submission results access control.
    if (user_access("administer nodes") || user_access("edit any webform content")) {
      $access = true;
    }
    else {
      // For each user specified, make sure the current user is one of them,
      // otherwise don't show the results.
      foreach ($node->field_webform_results_access as $i => $user_reference) {
        if ($user_reference['uid'] == $account->uid) {
          $access = true;
          break;
        }
      }
    }
  }
  return $access;
}

That's all folks, just flush all of your caches and you should be all set. Add some user's to the user reference field on your webform nodes and then they'll be able to access the submission results on that webform.

Drupal 7

There are only two lines of code that are different to make this work in Drupal 7, change this line:

if (isset($node->field_webform_results_access[0]['uid'])) {

To this:

if (isset($node->field_webform_results_access['und'][0]['uid'])) {

And this line:

foreach ($node->field_webform_results_access as $i => $user_reference) {

To this:

foreach ($node->field_webform_results_access['und'] as $i => $user_reference) {

Older Versions of Webform

Before Webform offered the hooks mentioned above (I'm not sure when the hooks were introduced, I didn't notice them until Webform 3.17), we had to take a slightly different approach to accomplish the same task. We needed to work with hook_menu_alter and our theme's template.php file, for example this is how I accomplished the same thing in Webform 3.2:

function my_module_menu_alter ($items) {

  // attach a custom access callback function to webform results and submissions
  if (module_exists("webform")) {

    // webform results menu paths
    $result_menu_paths = array(
      'node/%webform_menu/webform-results',
      'node/%webform_menu/webform-results/submissions',
      'node/%webform_menu/webform-results/analysis',
      'node/%webform_menu/webform-results/analysis/%webform_menu_component',
      'node/%webform_menu/webform-results/table',
      'node/%webform_menu/webform-results/download',
    );
    foreach ($result_menu_paths as $result_menu_path) {
      $items[$result_menu_path]['access callback'] = 'my_module_webform_results_access';  
    }

    // webform submission menu paths
    $submission_menu_paths = array(
      'node/%webform_menu/submissions',
      'node/%webform_menu/submission/%webform_menu_submission',
      'node/%webform_menu/submission/%webform_menu_submission/view',
    );
    foreach ($submission_menu_paths as $submission_menu_path) {
      $items[$submission_menu_path]['access callback'] = 'my_module_webform_submission_access';  
    }

  }

}

// override of webform_results_access
function my_module_webform_results_access ($node, $account = NULL) {
  global $user;
  $account = isset($account) ? $account : $user;

  // this is webform's default way of dealing with access, if this doesn't pass, fall back to our custom access check
  $webform_access = node_access('view', $node, $account) && (user_access('access all webform results', $account) || (user_access('access own webform results', $account) && $account->uid == $node->uid));

  if ($webform_access) {
    return true;
  }
  else {
    return my_module_webform_access($node);
  }
}

// override of webform_submission_access
function my_module_webform_submission_access($node, $submission, $op = 'view', $account = NULL) {
  global $user;
  $account = isset($account) ? $account : $user;

  $access_all = user_access('access all webform results', $account);
  $access_own_submission = isset($submission) && user_access('access own webform submissions', $account) && (($account->uid && $account->uid == $submission->uid) || isset($_SESSION['webform_submission'][$submission->sid]));
  $access_node_submissions = user_access('access own webform results', $account) && $account->uid == $node->uid;

  $general_access = $access_all || $access_own_submission || $access_node_submissions || my_module_webform_access($node);

  // Disable the page cache for anonymous users in this access callback,
  // otherwise the "Access denied" page gets cached.
  if (!$account->uid && user_access('access own webform submissions', $account)) {
    webform_disable_page_cache();
  }

  switch ($op) {
    case 'view':
      return $general_access;
    case 'edit':
      return $general_access && (user_access('edit all webform submissions', $account) || (user_access('edit own webform submissions', $account) && $account->uid == $submission->uid));
    case 'delete':
      return $general_access && (user_access('delete all webform submissions', $account) || (user_access('delete own webform submissions', $account) && $account->uid == $submission->uid));
    case 'list':
      return user_access('access all webform results', $account) || (user_access('access own webform submissions', $account) && ($account->uid || isset($_SESSION['webform_submission']))) || (user_access('access own webform results', $account) && $account->uid == $node->uid);
  }
}

// webform submission results user access cck field check
function my_module_webform_access ($node) {
  global $user;
  $account = isset($account) ? $account : $user;
  $access = false;
  if (isset($node->field_webform_results_access[0]['uid'])) { // this webform has uniqname(s) specified for access control
    if (user_access("administer nodes") || user_access("edit any webform content")) {
      $access = true;
    }
    else {
      // for each uniqname specified, make sure the current user is one of them, otherwise don't show the results
      foreach ($node->field_webform_results_access as $i => $user_reference) {
        if ($user_reference['uid'] == $account->uid) {
          $access = true;
          break;
        }
      }
    }
  }
  return $access;
}

And then a few simple template.php overriddes for themeing the webform properly [ the only difference is adding a call to my_theme_webform_access($node) ] :

/**
 * MY THEME OVERRIDE - Theme the header of the submissions table.
 *
 * This is done in it's own function so that webform can retrieve the header and
 * use it for sorting the results.
 */

function my_theme_webform_results_submissions_header($node) {
  global $user;

  $columns = array(
    array('data' => t('#'), 'field' => 'sid', 'sort' => 'asc'),
    array('data' => t('Submitted'), 'field' => 'submitted'),
  );
  if (
    user_access('access all webform results') ||
    (user_access('access own webform results') && $user->uid == $node->uid) ||
    my_theme_webform_access($node)
  ) {
    $columns[] = array('data' => t('User'), 'field' => 'name');
    $columns[] = array('data' => t('IP Address'), 'field' => 'remote_addr');
  }
  $columns[] = array('data' => t('Operations'), 'colspan' => module_exists('print') ? 5 : 3);

  return $columns;
}

/**
 * MY THEME OVERRIDE - Theme the submissions tab of the webform results page.
 *
 * @param $node
 *   The node whose results are being displayed.
 * @param $submissions
 *   An array of all submissions for this webform.
 * @param $total_count
 *   The total number of submissions to this webform.
 * @param $pager_count
 *   The number of results to be shown per page.
 */
function my_theme_webform_results_submissions($node, $submissions, $total_count = 0, $pager_count = 0) {
  global $user;

  drupal_add_css(drupal_get_path('module', 'webform') . '/css/webform-admin.css', 'theme', 'all', FALSE);

  // This header has to be generated separately so we can add the SQL necessary
  // to sort the results.
  $header = theme('webform_results_submissions_header', $node);
  $operation_column = end($header);
  $operation_total = $operation_column['colspan'];

  $rows = array();
  foreach ($submissions as $sid => $submission) {
    $row = array(
      $submission->is_draft ? t('@sid (draft)', array('@sid' => $sid)) : $sid,
      format_date($submission->submitted, 'small'),
    );
    if (
      user_access('access all webform results') ||
      (user_access('access own webform results') && $user->uid == $node->uid) ||
      my_theme_webform_access($node)
    ) {
      $row[] = theme('username', $submission);
      $row[] = $submission->remote_addr;
    }
    $row[] = l(t('View'), "node/$node->nid/submission/$sid");
    $operation_count = 1;
    if (module_exists('print_pdf') && user_access('access PDF version')) {
      $row[] = l(t('PDF'), "printpdf/$node->nid/submission/$sid", array('query' => drupal_get_destination()));
      $operation_count++;
    }
    if (module_exists('print') && user_access('access print')) {
      $row[] = l(t('Print'), "print/$node->nid/submission/$sid");
      $operation_count++;
    }
    if ((user_access('edit own webform submissions') && $user->uid == $submission->uid) || user_access('edit all webform submissions')) {
      $row[] = l(t('Edit'), "node/$node->nid/submission/$sid/edit", array('query' => drupal_get_destination()));
      $operation_count++;
    }
    if ((user_access('delete own webform submissions') && $user->uid == $submission->uid) || user_access('delete all webform submissions')) {
      $row[] = l(t('Delete'), "node/$node->nid/submission/$sid/delete", array('query' => drupal_get_destination()));
      $operation_count++;
    }
    if ($operation_count < $operation_total) {
      $row[count($row) - 1] = array('data' => $row[count($row) - 1], 'colspan' => $operation_total - $operation_count + 1);
    }
    $rows[] = $row;
  }

  if (count($rows) == 0) {
    $rows[] = array(array('data' => t('There are no submissions for this form. <a href="!url">View this form</a>.', array('!url' => url('node/' . $node->nid))), 'colspan' => 4 + $operation_total));
  }

  $output = '';
  $output .= theme('webform_results_per_page', $total_count, $pager_count);
  $output .= theme('table', $header, $rows);
  if (arg(2) == 'submissions') {
    $output .= theme('links', array('webform' => array('title' => t('Go back to the form'), 'href' => 'node/' . $node->nid)));
  }
  if ($pager_count) {
    $output .= theme('pager', NULL, $pager_count, 0);
  }
  return $output;
}

As you can see, there is a lot more code involved in doing this without the new hooks introduced by later version of Webform 3.x. So, you might as well just update your webform module to the latest version.

Comments

This is awesome! Thanks for taking the time to post this solution. Quick question, if you implement this code, the defined user would have access to the webform results reguardless of the user's permissions/role?

Thanks.

tyler's picture

Correct. The solution above will allow a user (added to the user reference field on the webform content type) to access the webform's results regardless of their permissions/roles, thanks!

Very nice! For Drupal 7 I had to change:

 

foreach($node->field_webform_results_access as $user_reference) {
    if ($user_reference['uid'] == $account->uid) {

to

 

$field = field_view_field('node', $node, 'field_webform_results_access');
foreach($field as $user_reference) {
    if ($user_reference['target_id'] == $account->uid) {

and then it worked just fine!

tyler's picture

Thank you for the note about Drupal 7. I will be trying this soon as I migrate a giant site from D6 to D7!

tyler's picture

Unfortunately this didn't work for me in Drupal 7. However, I just needed to make a slight adjustement to get it to work in D7. I've updated the post above with instructions on how to accomplish this in D7.

Hi Tyler,

Thanks for this post !  This seems to be what I'm looking for but forgive me for asking a dumb question... how this is implemented? I assume  one would create a module, which I did but where would the "user reference field" show up?

Sorry if I'm missing  something obvious.

I'm using Drupal 7 and the latest version of webform.

-Thanks. 

tyler's picture

Hi Bronwyn, you need to add a 'user reference' field to the 'Webform' content type. Then create a custom module to implement those hooks. I haven't yet tried this in Drupal 7, but I'm sure those hooks are still available.

Then when you add/edit webform nodes, you should see your new 'user reference' field on the form, there you can add/remove users from the field.

Ok, great. Got it.  Thanks so much!

tyler's picture

No problem. I just updated this post to show how it is done in Drupal 7.

I'm trying to use this code but not having to much success. I have not made a custom module before but I think I have it.  The module showed up and I enabled it.  Not sure it is doing anything.

I created the field to add the users.  but the field only shows up when I log in with the admin user.  Even if I add the users I want to have results access I don't see any change unless I log in with the main admin account.  Is there some permissions thing I'm missing for the module and/or field?

tyler's picture

Hi Ryan, I'd recommend flushing all of your caches. Also, make sure users are allowed to view the Webform node and that no other access restrictions are in place (from other contributed modules). Feel free to contact me directly and send along a demo account, and maybe I can take a look.

Hi! That was what I was looking for. Awesome. I created a repository on Github with the code (for Drupal 7.x):

https://github.com/plepe/drupal_webform_results_access

tyler's picture

Nicely done! Have you thought about creating a Sandbox repository on drupal.org?

Then you could eventually submit it as an official module.

I'd like to help you with this if you'd like. I have a client reqeusting this feature as a module as well. Let me know, thanks!

I just read your last message today. I created a sandbox project on Drupal: https://drupal.org/sandbox/plepe/2217803 . What's your username on drupal? I can add you as maintainer if you want.

I also added a change: The project is now compatible with the Entity Reference module.

tyler's picture

Sure, my user name is: tyler.frankenstein

I'm trying so hard, but it isn't working! I've tried "user reference" in the webform node, i've added some users to test, but nothing happens! What am I doing wrong? The code Above was added to webform.module, and i tried plepe module too. None of them worked for me.

tyler's picture

This code should (must) go into your custom module.

Make sure that your User Referience field machine name is used in the custom code.

Thanks a lot for your amazing work. I've used it today. It works like a charm. I've tweaked a bit to improve the performance by using &drupal_static(__FUNCTION__);

Very helpful page! It is useful to denote two things:

a) If one wants to restrict access to submission (e.g. allow only view and not edit or delete), then it is possible to use the $op argument as already shown in my_module_webform_submission_access() function in the example for older versions of the module.

b) In case there are several modules setting permissions on results access of a webform, then the webform module treats return values from all functions implementing hook_webform_results_access in such a way that the user has access to the webform results if at least one of them grants him/her with access by returning true (see https://www.drupal.org/node/2099859).