<?php
// $Id: webform_protected_downloads.module,v 1.1.2.10 2010/11/19 15:50:52 berliner Exp $

/**
 * @file
 * This file contains hook declarations and functions for the Webform Protected
 * Downloads module.
 *
 * @see README.txt for more information
 */

/**
 * The session key.
 */
define('WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY', 'webform_protected_downloads_access');

/**
 * The access type "One-time access"
 */
define('WEBFORM_PROTECTED_DOWNLOADS_ACCESS_TYPE_SINGLE', 0);

/**
 * The access type "Expires"
 */
define('WEBFORM_PROTECTED_DOWNLOADS_ACCESS_TYPE_EXPIRES', 1);

/**
 * The default access type for new webforms
 */
define('WEBFORM_PROTECTED_DOWNLOADS_DEFAULT_ACCESS_TYPE', 0);

/**
 * The default expiration interval for the download itself in seconds.
 */
define('WEBFORM_PROTECTED_DOWNLOADS_DEFAULT_EXPIRATION_DOWNLOAD', 60 * 60 * 24 * 7);

/**
 * The default expiration interval for the file access that is managed by the
 * session information itself in seconds.
 */
define('WEBFORM_PROTECTED_DOWNLOADS_DEFAULT_EXPIRATION_SESSION', 60 * 60 * 24);

/**
 * Default setting for retroactive access, meaning whether or not a user can
 * access files that have been protected after the first form submission.
 */
define('WEBFORM_PROTECTED_DOWNLOADS_DEFAULT_RETROACTIVE', 1);

/**
 * Default setting for direct redirect to the downloads page after form
 * submission.
 */
define('WEBFORM_PROTECTED_DOWNLOADS_DEFAULT_REDIRECT', 0);

/**
 * Our identifier for watchdog messages
 */
define('WEBFORM_PROTECTED_DOWNLOADS_WATCHDOG_ID', 'Protected Downloads');

/**
 * Implementation of hook_menu().
 */
function webform_protected_downloads_menu() {
  $items = array();
  
  $items['node/%webform_menu/download'] = array(
    'title' => 'Download',
    'type' => MENU_CALLBACK,
    'page callback' => 'webform_protected_downloads_download_page',
    'page arguments' => array(1),
    'access callback' => 'webform_protected_downloads_access',
    'access arguments' => array('view', 1),
    'file' => 'webform_protected_downloads.page.inc',
  );
  
  $items['node/%webform_menu/download/%'] = array(
    'title' => 'Download',
    'type' => MENU_CALLBACK,
    'page callback' => 'webform_protected_downloads_download_page',
    'page arguments' => array(1, 3),
    'access callback' => 'webform_protected_downloads_access',
    'access arguments' => array('view', 1),
    'file' => 'webform_protected_downloads.page.inc',
  );
  
  $items['node/%webform_menu/protected-downloads'] = array(
    'title' => 'Protected Downloads',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array('webform_protected_downloads_configuration_form', 1),
    'access callback' => 'webform_protected_downloads_access',
    'access arguments' => array('update', 1),
    'weight' => 3,
    'file' => 'webform_protected_downloads.form.inc',
  );
  
  return $items;
}

/**
 * Implementation of hook_admin_paths().
 */
function webform_protected_downloads_admin_paths() {
  return array(
    'node/*/protected-downloads' => TRUE,
  );
}

/**
 * Implements hook_contextual_links_view_alter().
 */
function webform_protected_downloads_contextual_links_view_alter(&$element, &$items) {
  // Add a link to the protected downloads configuration page on
  // webform-enabled nodes.
  if (isset($element['#element']['#node']) && webform_protected_downloads_node_is_webform($element['#element']['#node'])) {
    $element['#links']['protected-downloads'] = array(
      'title' => t('Protected downloads'),
      'href' => 'node/' . $element['#element']['#node']->nid . '/protected-downloads',
    );
  }
}

/**
 * Custom access callback
 *
 * @param object $node 
 * @return boolean
 */
function webform_protected_downloads_access($op, $node) {
  
  switch ($op) {
    
    case 'view':
    
      // first check if there are any files attached to this node
      if (!isset($node->wpd['private_files']) || !count($node->wpd['private_files'])) {
        return FALSE;
      }
    
      return node_access($op, $node);
      break;
    
    case 'update':
      return node_access($op, $node) && user_access('administer webform protected downloads');
      break;
    
  }
  
  return FALSE;
  
}

/**
 * Implementation of hook_permission().
 */
function webform_protected_downloads_permission() {
  return array(
    'administer webform protected downloads' => array(
      'title' => t('Administer webform protected downloads'),
    )
  );
}

/**
 * Implementation of hook_theme().
 */
function webform_protected_downloads_theme() {
  return array(
    'webform_protected_downloads_configuration_form_file_list' => array(
      'render element' => 'element',
      'file' => 'webform_protected_downloads.form.inc',
    ),
    'webform_protected_downloads_mail_token_file_list'  => array(
      'variables' => array('files' => NULL, 'checksum' => FALSE),
      'file' => 'webform_protected_downloads.form.inc',
    )
  );
}

/**
 * Implementation of hook_file_download().
 *
 * @param string $filepath 
 */
function webform_protected_downloads_file_download($uri) {
  global $conf;
  
  $admin_access = user_access('administer webform protected downloads');
  
  // Get the file record based on the URI. If not in the database just return.
  $files = file_load_multiple(array(), array('uri' => $uri));
  if (count($files)) {
    foreach ($files as $item) {
      // Since some database servers sometimes use a case-insensitive comparison
      // by default, double check that the filename is an exact match.
      if ($item->uri === $uri) {
        $file = $item;
        break;
      }
    }
  }
  
  // no file found, it is not up to us to handle this
  if (!isset($file)) {
    return NULL;
  }
  
  // check if the file is registered as protected
  $count = db_select('wpd_protected_files', 'p')->fields('p')->condition('fid', $file->fid)->countQuery()->execute()->fetchColumn();
  if (!$count) {
    return NULL;
  }
  
  // now we know, that this is a protected file
  // get all nodes that this file has been attached to
  $result = db_select('file_usage', 'f')->fields('f')->condition('fid', $file->fid)->condition('module', 'file')->condition('type', 'node')->execute();
  
  while ($record = $result->fetchObject()) {
    
    $nid = $record->id;
    
    // check if the file is protected for this node
    if (webform_protected_downloads_file_is_protected($nid, $file->fid)) {
      
      // don't cache access allowed or denied
      $conf['cache'] = 0;
      
      // check if the current user should be granted access to this file
      if (webform_protected_downloads_file_user_has_access($nid, $file->fid) || $admin_access) {
        // access granted
        return file_get_content_headers($file);
      }
      else {
        // access denied
        return -1;
      }
    }
    
  }
  
  // file is not protected so we have nothing to say
  return NULL;
}

/**
 * Implementation of hook_file_delete().
 */
function webform_protected_downloads_file_delete($file) {
  // delete all wpd information associated with the file
  db_delete('wpd_protected_files')->condition('fid', $file->fid)->execute();
}

/**
 * Implementation of hook_menu_alter().
 *
 * @param array $items 
 * @return void
 */
function webform_protected_downloads_menu_alter(&$items) {
  $item = &$items['node/%webform_menu/webform/components/%webform_menu_component/delete'];
  $item['access callback'] = 'webform_protected_downloads_component_delete_access';
  $item['access arguments'] = array('update', 1, 4);
}

/**
 * Custom access callback that checks wheather a webform component can be
 * deleted
 *
 * @param string $op 
 * @param object $node 
 * @param array $component 
 * @return boolean
 */
function webform_protected_downloads_component_delete_access($op, $node, $component) {
  
  // check whether the current component exists already, otherwhise the coming
  // checks would be unnecessary
  if (isset($component['cid'])) {
  
    $component_in_use = webform_protected_downloads_get_configuration($node->nid, 'mail_field_cid') == $component['cid'];
    $node_protected = webform_protected_downloads_node_has_protected_files($node->nid);
    if ($node_protected && $component_in_use) {
      if (arg(5) == 'delete') {
        // we are really on the delete confirmation page
        drupal_set_message(t('Access to this action has been disabled by the Webform Protected Downloads module. The component that you want to delete is in use. Please got to the <a href="@protected_downloads_page">Protected Downloads configuration</a> of this webform and change the Mail confirmation field or unprotect all files. <br /><a href="@last_page">Go back to the form</a>', array(
          '@protected_downloads_page' => url('node/' . $node->nid . '/protected-downloads'),
          '@last_page' => url($_GET['destination']),
        )));
      }
      else {
        // we are most probably on the components edit form
        drupal_set_message(t('Deletion of this component has been disabled by the Webform Protected Downloads module, because the component is currently in use. This can be changed on the <a href="@protected_downloads_page">Protected Downloads configuration</a> page.', array(
          '@protected_downloads_page' => url('node/' . $node->nid . '/protected-downloads'),
        )));
      }
      return FALSE;
    }
  }
  
  return node_access($op, $node);
}

/**
 * Implementation of hook_cron().
 */
function webform_protected_downloads_cron() {
  
  // delete expired hash codes, note the missing alias for the first table,
  // this is necessary to support sqlite
  db_query("DELETE    
            FROM      {wpd_access_hashes}
            WHERE     (expires < :expires AND expires != 0)
            OR        EXISTS (SELECT 1 FROM {webform_submissions} s
                                       LEFT JOIN {wpd_node_configuration} n USING (nid) 
                                       WHERE (s.sid = {wpd_access_hashes}.sid AND n.access_type = :access_type AND {wpd_access_hashes}.used != 0))", 
            array(
              ':expires' => time(),
              ':access_type' => WEBFORM_PROTECTED_DOWNLOADS_ACCESS_TYPE_SINGLE
            )
  );
  
}

/**
 * Implementation of hook_node_load().
 */
function webform_protected_downloads_node_load($nodes, $types) {
  
  foreach ($nodes as $nid => $node) {
    
    if (webform_protected_downloads_node_is_webform($node) && !isset($node->wpd['valid'])) {
      
      // check if the node has protected files
      if (webform_protected_downloads_node_has_protected_files($node->nid)) {
        // check if the registered component still exists, needed, because we
        // can't prevent webform from deleting a component that we might use
        // as a mail field for the protected downloads        
        $result = db_query("SELECT COUNT(*) FROM {wpd_node_configuration} n LEFT JOIN {webform_component} c ON c.cid = n.mail_field_cid AND c.nid = n.nid WHERE n.nid = :nid AND c.cid IS NOT NULL", array(':nid' => $node->nid))->fetchAssoc();
        $nodes[$nid]->wpd['valid'] = $result > 0;
      }
      else {
        $nodes[$nid]->wpd['valid'] = TRUE;
      }
      
      // attach any private files to this node
      $node->wpd['private_files'] = webform_protected_downloads_node_get_private_files($node);
      
      // attach any protected files to this node
      $node->wpd['protected_files'] = array();
      foreach ($node->wpd['private_files'] as $file) {
        if ($file->protected) {
          $node->wpd['protected_files'][$file->fid] = $file;
        }
      }
      
      // attach wpd configuration for this node
      $node->wpd['config'] = webform_protected_downloads_get_configuration($node->nid);
      $node->wpd['nid'] = $node->nid;
      
    }
    
  }
  
}

/**
 * Implementation of hook_insert().
 */
function webform_protected_downloads_node_insert($node) {
  
  // check for webform enabled node types and programatically created nodes
  // with a wpd configuration
  if (webform_protected_downloads_node_is_webform($node) && isset($node->wpd)
  && $node->nid != $node->wpd['nid']) {
    // a new node has probably been created programatically or by a helper
    // module like node_clone, here we must therefore save all the necessary
    // data
    webform_protected_downloads_set_configuration($node->nid, (array) $node->wpd['config']);
    foreach ($node->wpd['protected_files'] as $file) {
      webform_protected_downloads_file_set_protected($node->nid, $file->fid, TRUE);
    }
  }
  
}

/**
 * Implementation of hook_node_view().
 */
function webform_protected_downloads_node_view($node, $view_mode, $langcode) {
  
  if (!webform_protected_downloads_node_is_webform($node)) {
    return;
  }
  
  // check if the node has protected files
  if (webform_protected_downloads_node_has_protected_files($node->nid)) {
    
    // make sure that protected files are not displayed
    $disabled = array();
    foreach ($node->wpd['protected_files'] as $file) {
      
      if (!isset($node->content[$file->field])) {
        // the attribute is only there for files that have been set to display
        // on the node edit form and that have been protected afterwards via
        // this module, once the node form is hit and saved the property is no
        // more there, that's why we need to check that here
        continue;
      }
      
      foreach ($node->content[$file->field]['#items'] as $key => $item) {
        if ($item['fid'] == $file->fid) {
          // disable access
          $node->content[$file->field][$key]['#access'] = FALSE;
          // count the number of protected files for this field
          $disabled[$file->field] = isset($disabled[$file->field]) ? $disabled[$file->field] + 1 : 1;
        }
      }
      
    }
    
    // disable the whole fieldset if all files for a field are protected
    foreach ($disabled as $field => $count) {
      if (!isset($node->content[$field]['#items']) || count($node->content[$field]['#items']) <= $count) {
        $node->content[$field]['#access'] = FALSE;
      }
    }
  }
  
}

/**
 * Implementation of hook_node_delete().
 *
 * @param object $node 
 * @return void
 */
function webform_protected_downloads_node_delete($node) {
  // delete our own references and configuration for this node
  db_delete("wpd_node_configuration")->condition('nid', $node->nid)->execute();
  db_delete("wpd_protected_files")->condition('nid', $node->nid)->execute();
  
  // delete all entries in table wpd_access_hashes that are no longer used,
  // note the missing alias for the first table, this is necessary to support
  // sqlite
  $sql = "DELETE    
          FROM      {wpd_access_hashes}
          WHERE     NOT EXISTS (SELECT 1 FROM {webform_submissions} s WHERE s.sid = {wpd_access_hashes}.sid)";
  db_query($sql);
}

/**
 * Check whether the given node is used as a webform
 *
 * @param object $node 
 * @return boolean
 */
function webform_protected_downloads_node_is_webform($node) {
  // API change introduced by https://drupal.org/node/2062235
  if (function_exists('webform_node_types')) {
    $webform_types = webform_node_types();
  }
  else {
    $webform_types = webform_variable_get('webform_node_types');
  }
  return in_array($node->type, $webform_types);
}

/**
 * Retrieve all private file fields.
 *
 * This actually retrieves fields of types 'file' and 'image'.
 *
 * @param string $node 
 * @return array
 */
function webform_protected_downloads_node_get_private_file_fields($node) {
  $private_file_fields = &drupal_static(__FUNCTION__);
  if (!isset($private_file_fields[$node->nid])) {
    $private_file_fields[$node->nid] = array();
    $field_instances = field_info_instances('node', $node->type);
    foreach ($field_instances as $field_name => $field) {
      $field_info = field_info_field($field_name);
      if (in_array($field_info['type'], array('file', 'image')) && $field_info['settings']['uri_scheme'] == 'private') {
        $private_file_fields[$node->nid][] = $field_name;
      }
    }
  }
  return $private_file_fields[$node->nid];
}

/**
 * Retrieve private files for the given node
 *
 * @param object $node 
 * @return array
 */
function webform_protected_downloads_node_get_private_files($node) {
  
  $private_file_fields = webform_protected_downloads_node_get_private_file_fields($node);
  if (!count($private_file_fields)) {
    return array();
  }
  
  /*
  When trying to retrieve attached private fields, we need to pay attention
  to language handling, possibilities:
   1. Node has no language (locale disabled) and file has no language either
   2. Node has a language but file has no language
   3. Node has a language and file has a language
  For now all attached files are treated the same.
  See http://drupal.org/node/1239916 for examples of arising problems.
  */
  $private_files = array();
  foreach ($private_file_fields as $field) {

    // check if the node has acceptable file fields
    if (!is_array($node->$field) || !count($node->$field)) {
      continue;
    }
    
    // iterate over possible fields, in the node object they are organised by a
    // language code, so in the first iteration we go into the langcode
    foreach ($node->$field as $langcode => $files) {
      
      // now iterate over the specific files
      foreach ($files as $key => $file) {
        $file = (object) $file;
        $file->field = $field;
        $file->nid = $node->nid;
        $file->weight = $key;
        $file->protected = webform_protected_downloads_file_is_protected($node->nid, $file->fid);
        $private_files[$file->fid] = $file;
      }
    
    }
    
  }
  
  return $private_files;
}

/**
 * Implementation of hook_help().
 */
function webform_protected_downloads_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/help#webform_protected_downloads':
      $output = t("<h3>Purpose of this module</h3>
      This module is based on the webform module and lets you configure protected downloads, meaning files of any type that can be accessed by a user only after the user has submitted a webform with a valid e-mail address.

      <h3>How it works</h3>
      This module operates on nodes that are used as webforms. A webform node that has been configured for protected downloads acts just like a normal webform survey, expect that the user must give a valid e-mail address. After the user submits the webform an e-mail will be sent to the given e-mail address. This e-mail contains a link to a download page where all protected files are listed and can be downloaded from.
      In order to use a webform node for protected downloads this node must meet the following criteria:
      <ul>
        <li>must have at least one file field that is configured as a private file field</li>
        <li>at least one private file must have been attached to the node</li>
        <li>the webform must have at least one mandatory e-mail component</li>
      </ul>

      <h3>Step-by-step guide to use this module</h3>
      <ol>
        <li><a href='@webform_help_url'>Create a webform</a></li>
        <li>Add a a mandatory mail component to the webform</li>
        <li>Add a file field for the created webform node and configure it as a private file field (see <em>Upload destination</em> under <em>File Field settings</em>)</li>
        <li>Upload at least one file to this field using the node edit form</li>
        <li>Go to the <em>Protected downloads</em> tab of the node and configure details like
        <ul>
          <li>how long should the download stay available for a given user</li>
          <li>the mail text of the outgoing mail, including a placeholder for the download link</li>
          <li>custom text that appears on the downloads page above the file listing</li>
          <li>custom text for an access denied page</li>
        </ul>
        </li>
      </ol>

      <h3>How to send HTML formatted mails</h3>
      The e-mails containing the access information for the protected files are send using default drupal mail functionality. That's why you should be able to use any mailengine that offers html support in order to send formatted mails. The only mailengine that has actively been tested in conjunction with this module is the <a href='@mimemail_project_url'>mimemail module</a>.
      
      <h3>Having problems with the configuration? Here's the checklist:</h3>
      <ul>
        <li>Content type in question is webform enabled? Check your <a href='@webform_settings_url'>Webform settings</a>.</li>
        <li>Content type has a file field that is configured as private?</li>
        <li>On /node/NID/edit a file has been added to the aforementioned file field?</li>
        <li>On node/NID/webform you have at least one component of type \"mail\" that is set as required?</li>
        <li>The user you are logged in as, has the permission \"administer webform protected downloads\"?</li>
      </ul>
      
      <h3>Useful references</h3>
      <ul>
        <li><a href='@reference_files_url'>Working with files in Drupal 7</li>
      </ul>", array(
        '@webform_settings_url' => url('admin/config/content/webform'),
        '@webform_help_url' => url('admin/help/webform'),
        '@mimemail_project_url' => url('http://drupal.org/project/mimemail'),
        '@reference_files_url' => url('http://drupal.org/documentation/modules/file'),
      ));
      break;
    case 'node/%/protected-downloads':
      $output .= '<p>'. t("This page displays files that are currently attached to this webform. You can select one or more of these files to be protected downloads. This means, that they won't be listed on the normal webform view page. Instead when the user submits the form, he receives an email to a given mail (you can choose any webform component of the type mail that you have already added to this webform) containing a link to download the protected file.") .'</p>';
      break;
  }

  return $output;
}

/**
 * Set the protected status for the given node / file combination
 *
 * @param int $nid 
 * @param int $fid 
 * @param boolean $protected 
 * @return void
 */
function webform_protected_downloads_file_set_protected($nid, $fid, $protected) {
  if (webform_protected_downloads_file_is_protected($nid, $fid) && !$protected) {
    db_delete('wpd_protected_files')->condition('nid', $nid)->condition('fid', $fid)->execute();
  }
  elseif (!webform_protected_downloads_file_is_protected($nid, $fid) && $protected) {
    $record = array('nid' => $nid, 'fid' => $fid, 'created' => time());
    drupal_write_record('wpd_protected_files', $record);
  }
}

/**
 * Checks wheather the given file is protected for the given node
 *
 * @param int $nid 
 * @param int $fid 
 * @return boolean
 */
function webform_protected_downloads_file_is_protected($nid, $fid) {
  $wpd_protected = &drupal_static(__FUNCTION__);
  if (!isset($wpd_protected[$nid])) {
    $wpd_protected[$nid] = array();
    $result = db_query("SELECT fid FROM {wpd_protected_files} WHERE nid = :nid", array(':nid' => $nid));
    while ($row = $result->fetchObject()) {
      if (!in_array($row->fid, $wpd_protected[$nid])) {
        $wpd_protected[$nid][] = $row->fid;
      }
    }
  }
  return isset($wpd_protected[$nid]) ? in_array($fid, $wpd_protected[$nid]) : FALSE;
}

/**
 * Checks wheather the current user has access to the given file for the given
 * node
 *
 * @param int $nid 
 * @param int $fid 
 * @return boolean
 */
function webform_protected_downloads_file_user_has_access($nid, $fid) {
  
  // if it is protected, allow access only if the hash has been added to the
  // session
  if (!isset($_SESSION[WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY])) {
    return FALSE;
  }
  
  $sql = "SELECT    expires, hash
          FROM      {wpd_protected_files} p
          LEFT JOIN {webform_submissions} s ON (s.nid = p.nid)
          LEFT JOIN {wpd_node_configuration} n ON (p.nid = n.nid)
          LEFT JOIN {wpd_access_hashes} h USING(sid)
          WHERE     p.nid = :nid
          AND       p.fid = :fid
          AND       expires IS NOT NULL
          AND       hash IS NOT NULL
          AND       (n.retroactive = 1 OR (n.retroactive = 0 AND s.submitted > p.created))";
  $args = array(':nid' => $nid, ':fid' => $fid);
  $result = db_query($sql, $args);
  
  while ($row = $result->fetchObject()) {
    
    $ok = FALSE;
    // necessary condition
    if (isset($_SESSION[WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY][$row->hash])) {
      $session_expires = $_SESSION[WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY][$row->hash]['expires'];
      if ($session_expires == 0 || $session_expires > time()) {
        $ok = TRUE;
      }
      else {
        unset($_SESSION[WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY][$row->hash]);
        $ok = FALSE;
      }
    } else {
      $ok = FALSE;
    }
    $ok = $ok && ($row->expires == 0 || $row->expires > time());
    
    if ($ok) {
      return TRUE;
    }
  }
  return FALSE;
  
}

/**
 * Checks if the given node has protected files
 *
 * @param int $nid 
 * @return void
 */
function webform_protected_downloads_node_has_protected_files($nid) {
  $wpd_nodes = &drupal_static(__FUNCTION__);
  if (!isset($wpd_nodes[$nid])) {
    $count = db_query("SELECT COUNT(*) FROM {wpd_protected_files} WHERE nid = :nid", array(':nid' => $nid))->fetchColumn();
    $wpd_nodes[$nid] = $count > 0;
  }
  return $wpd_nodes[$nid];
}

/**
 * Set the configuration for the given node
 *
 * @param int $nid 
 * @param int $cid 
 * @param string $subject 
 * @param string $body 
 * @param string $text_access 
 * @param string $text_noaccess 
 * @return void
 */
function webform_protected_downloads_set_configuration($nid, $args) {
  $configuration = array(
    'nid'                           => $nid,
    'mail_field_cid'                => $args['mail_field_cid'],
    'mail_from'                     => $args['mail_from'],
    'mail_subject'                  => $args['mail_subject'],
    'mail_body'                     => $args['mail_body'],
    'access_type'                   => $args['access_type'],
    'expiration_download'           => $args['expiration_download'],
    'expiration_session'            => $args['expiration_session'],
    'retroactive'                   => $args['retroactive'],
    'redirect'                      => $args['redirect'],
    'text_download_access'          => $args['text_download_access'],
    'text_download_access_format'   => $args['text_download_access_format'],
    'text_download_noaccess'        => $args['text_download_noaccess'],
    'text_download_noaccess_format' => $args['text_download_noaccess_format'],
  );
  drupal_write_record('wpd_node_configuration', $configuration, webform_protected_downloads_get_configuration($nid) ? array('nid') : array());
}

/**
 * Get the configuration for the given node
 *
 * @param int $nid 
 * @param string $field optional
 * @return void
 */
function webform_protected_downloads_get_configuration($nid, $field = NULL, $default = NULL) {
  $wpd_node_conf = &drupal_static(__FUNCTION__);
  
  if (!isset($wpd_node_conf[$nid])) {
    $wpd_node_conf[$nid] = db_query("SELECT * FROM {wpd_node_configuration} WHERE nid = :nid", array(':nid' => $nid))->fetchObject();
  }
  
  if ($wpd_node_conf[$nid]) {
    return $field !== NULL ? (isset($wpd_node_conf[$nid]->$field) && !empty($wpd_node_conf[$nid]->$field) ? $wpd_node_conf[$nid]->$field : $default) : $wpd_node_conf[$nid];
  }
  else {
    return FALSE;
  }
}

/**
 * Process unprocessed webform submissions
 *
 * @return void
 */
function webform_protected_downloads_process_submissions($form, &$form_state) {
  
  $sid = $form_state['values']['details']['sid'];
  $nid = $form_state['values']['details']['nid'];
  
  // check whether the node has protected files, otherwise we can skip the
  // following steps
  if (!webform_protected_downloads_node_has_protected_files($nid)) {
    return;
  }
  
  // load the configuration for this node
  $conf = webform_protected_downloads_get_configuration($nid);
  
  // this should not happen, if it does something is seriously not working and
  // we can't figurue out where to send the mail, so skip it
  if (!isset($form_state['values']['submitted'][$conf->mail_field_cid])) {
    _webform_protected_downloads_log('Problem with node !nid: The mail field could not be found in the form submission. No e-mail has been send.', array('!nid' => $nid), WATCHDOG_ERROR);
    return;
  }
  
  // now get the mail adress that should be used
  $mail = $form_state['values']['submitted'][$conf->mail_field_cid];
  
  // create hash, calculate expiration timestamp
  $hash = webform_protected_downloads_create_hash();
  $processed = time();
  $expires = $conf->expiration_download == 0 ? 0 : $processed + $conf->expiration_download;
  
  // we need to save before sending the mail
  $record = array(
    'sid' => $sid,
    'hash' => $hash,
    'processed' => $processed,
    'expires' => $expires,
    'used' => 0,
  );
  drupal_write_record('wpd_access_hashes', $record);
  
  // now send the mail
  webform_protected_downloads_send_mail($sid, $nid, $mail, $hash);
  
  if ($conf->redirect) {
    $form_state['redirect'] = 'node/' . $nid . '/download/' . $hash;
  }

}

/**
 * Create a hash that users can use to access the download page
 *
 * @param string $row 
 * @return void
 */
function webform_protected_downloads_create_hash() {
  $seed = 'JvKnrQWPsThuJteNQAuH';
  $hash = sha1(uniqid($seed . mt_rand(), true));
  $hash = substr($hash, 0, 32);
  return $hash;
}

/**
 * Retrieve details for this hash
 *
 * @param string $hash 
 * @return void
 */
function webform_protected_downloads_get_hash_details($hash) {
  $wpd_hash = &drupal_static(__FUNCTION__);
  if (!isset($wpd_hash[$hash])) {
    $result = db_query("SELECT * FROM {wpd_access_hashes} WHERE hash = :hash", array(':hash' => $hash));
    $wpd_hash[$hash] = $result->fetchObject();
  }
  return $wpd_hash[$hash];
}

/**
 * Retrieve the webform node based on the given hash
 *
 * @param string $hash 
 * @return void
 */
function webform_protected_downloads_get_node_from_hash($hash) {
  $result = db_query("SELECT nid FROM {wpd_access_hashes} LEFT JOIN {webform_submissions} USING(sid) WHERE hash = :hash", array(':hash' => $hash));
  $row = $result->fetchObject();
  return isset($row->nid) ? $row->nid : FALSE;
}

/**
 * Send mail to the user with a valid hash so that he can access the download page
 *
 * @param string $mail 
 * @param string $hash 
 * @return void
 */
function webform_protected_downloads_send_mail($sid, $nid, $mail, $hash) {
  
  global $user;
  
  // choose the language
  $language = $user->uid ? user_preferred_language($user) : language_default();
  
  // load the webform node, needed for token replacement
  $node = node_load($nid);
  
  // get the submission, including all it's data
  $submission = webform_get_submission($nid, $sid);
  
  // the sender address
  $from = webform_protected_downloads_get_configuration($nid, 'mail_from', variable_get('site_mail', NULL));
  
  // build the subject
  $subject = webform_protected_downloads_get_configuration($nid, 'mail_subject');
  $subject = _webform_protected_downloads_token_replace($subject, $node, $hash);
  $subject = _webform_filter_values($subject, $node, $submission, $email = NULL, $strict = FALSE, $allow_anonymous = TRUE);
  
  // build the body
  $body = webform_protected_downloads_get_configuration($nid, 'mail_body');
  $body = _webform_protected_downloads_token_replace($body, $node, $hash);
  $body = _webform_filter_values($body, $node, $submission, $email = NULL, $strict = FALSE, $allow_anonymous = TRUE);
  
  $params = array(
    'subject' => $subject,
    'body' => $body,
    'From' => $from,
  );
  
  // send the mail
  drupal_mail('webform_protected_downloads', 'confirmation', $mail, $language, $params, $from);
}

/**
 * Implementation of hook_mail().
 */
function webform_protected_downloads_mail($key, &$message, $params) {
  switch($key) {
    case 'confirmation':
      $message['subject'] = $params['subject'];
      $message['body'][] = $params['body'];
      break;
  }
}

/**
 * Helper function for token replacement
 *
 * @param string $string 
 * @param object $node 
 * @param string $hash 
 * @return string
 */
function _webform_protected_downloads_token_replace($string, $node, $hash) {
  return token_replace($string, array('node' => $node, 'hash' => $hash));
}

/**
 * Implementation of hook_form_alter().
 *
 * @param array $form 
 * @param array $form_state 
 * @param string $form_id 
 * @return void
 */
function webform_protected_downloads_form_alter(&$form, &$form_state, $form_id) {
  
  if (strpos($form_id, 'webform_client_form_') !== FALSE) {
    // trigger processing after the form has been submitted and handled by webform
    $form['#submit'][] = 'webform_protected_downloads_process_submissions';
  }
  
  // update the file upload form, so that protected files can't be deleted
  // and listed
  if (strpos($form_id, '_node_form') !== FALSE && isset($form['#node'])
  && webform_protected_downloads_node_is_webform($form['#node'])) {
    if (isset($form['#node']->nid) && webform_protected_downloads_node_has_protected_files($form['#node']->nid)) {
      foreach ($form['#node']->wpd['protected_files'] as $file) {
        $file_item = &$form[$file->field][$form[$file->field]['#language']];
        $file_item[$file->weight]['#default_value']['display'] = FALSE;
        if (!in_array('webform_protected_downloads_file_widget_after_build', $file_item['#after_build'])) {
          $file_item['#after_build'][] = 'webform_protected_downloads_file_widget_after_build';
        }
      }
    }
  }

  switch ($form_id) {
    
    // disable redirect options on the form configuration form if the redirect
    // to the downloads page has been activated for protected downloads
    case 'webform_configure_form':
      if (webform_protected_downloads_get_configuration($form['nid']['#value'], 'redirect')) {
        $form['submission']['redirection']['redirect']['#attributes']['disabled'] = 'disabled';
        $form['submission']['redirection']['redirect_url']['#attributes']['disabled'] = 'disabled';
        $form['submission']['redirection']['#description'] .= '<br />' . t('This setting has been deactivated, because this webform is configured to redirect to the downloads page for the protected files. You can change this on the <a href="@protected_downloads_page">Protected Downloads page</a>.', array('@protected_downloads_page' => url('node/' . $form['nid']['#value'] . '/protected-downloads')));
      }
      break;
    
    // disable the mandatory checkbox on the webform component overview, for
    // the component that is in use as mail for protected downloads
    case 'webform_components_form':
      if (webform_protected_downloads_node_has_protected_files($form['#node']->nid)) {
        $mandatory_field_name = webform_protected_downloads_get_mandatory_field_name();
        foreach ($form['components'] as $cid => &$element) {
          if (webform_protected_downloads_get_configuration($form['#node']->nid, 'mail_field_cid', NULL) == $cid) {
            $element[$mandatory_field_name]['#disabled'] = TRUE;
          }
        }
      }
      break;
    
    // disable the mandatory checkbox on the webform component edit form, if
    // the current component is the one used as mail for protected downloads
    case 'webform_component_edit_form':
      if (webform_protected_downloads_node_has_protected_files($form['nid']['#value'])) {
        if (webform_protected_downloads_get_configuration($form['nid']['#value'], 'mail_field_cid', NULL) == $form['cid']['#value']) {
          $mandatory_field_name = webform_protected_downloads_get_mandatory_field_name();
          $form['validation'][$mandatory_field_name]['#disabled'] = TRUE;
          $form['validation'][$mandatory_field_name]['#default_value'] = 1;
          $form['validation'][$mandatory_field_name]['#description'] .= '<br />' . t('This field has been disabled because one or more files attached to this webform have been protected. This component is used as the mail address for the confirmation mail that users need in order to access the protected files.');
          $form['display']['disabled']['#default_value'] = 0;
          $form['display']['disabled']['#disabled'] = TRUE;
          $form['display']['disabled']['#description'] .= '<br />' . t('This field has been disabled because one or more files attached to this node have been protected. This component is used as the mail address for the confirmation mail that users need in order to access the protected files so it must be possible to edit it.');
        }
      }
      break;
    
  }
  
  // check if this is a webform form and set a message if there is a problem
  // with this webform
  if (strpos($form_id, 'webform_') !== FALSE) {
    if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) != 'webform-results') {
      $node = node_load(arg(1));
      if (webform_protected_downloads_node_is_webform($node) && $node->wpd['valid'] === FALSE) {
        _webform_protected_downloads_set_warning();
      }
    }
  }
  
}

/**
 * Implementation of hook_after_build().
 */
function webform_protected_downloads_file_widget_after_build($form_element, &$form_state) {
  
  // get the protected files of the current node
  $files = $form_state['complete form']['#node']->wpd['protected_files'];
  
  // flag to see if the current field has protected files
  $field_has_protected_files = FALSE;
  
  // get the fields properties
  $field = field_widget_field($form_element, $form_state);
  
  $single_field_adjustments = array(t('The <em>removal</em> of this file has been disabled.'));
  if (!empty($field['settings']['display_field'])) {
    $single_field_adjustments[] = t('The <em>display checkbox</em> for this file has been disabled.');
  }
  
  foreach ($files as $file) {
    
    if ($form_element['#field_name'] == $file->field && isset($form_element[$file->weight])) {
      
      // remove the remove button
      $form_element[$file->weight]['remove_button']['#access'] = FALSE;
      
      // disable the display checkbox, if available (only for file fields with
      // "Enable display field"-option)
      if (!empty($field['settings']['display_field'])) {
        $form_element[$file->weight]['display']['#attributes']['disabled'] = 'disabled';
      }
      
      if ($field['cardinality'] == 1) {
        $form_element[$file->weight]['#description'] .= ' ' . t('This file has been protected by Webform Protected Downloads. !adjustments If you want to remove this file unprotect it first on the <a href="@protected_downloads_page">Protected Downloads page</a>.', array(
          '@protected_downloads_page' => url('node/' . $form_state['complete form']['#node']->nid . '/protected-downloads'),
          '!adjustments' => implode(' ', $single_field_adjustments),
        ));
      }
      
      $field_has_protected_files = TRUE;
    }
  }
  
  // adjust the description for fields that allow multiple files
  if ($field_has_protected_files && $field['cardinality'] != 1) {
    $adjustments = array(t('The <em>removal</em> of those files has been disabled.'));
    if (!empty($field['settings']['display_field'])) {
      $adjustments[] = t('The <em>display checkbox</em> for those files has been disabled.');
    }
    $form_element['#description'] .= ' ' . t('One or more of the following files have been protected using the Webform Protected Downloads module. !adjustments If you want to remove one of those files, unprotect it first on the <a href="@protected_downloads_page">Protected Downloads page</a>.', array(
      '@protected_downloads_page' => url('node/' . $form_state['complete form']['#node']->nid . '/protected-downloads'),
      '!adjustments' => implode(' ', $adjustments),
    ));
  }
  
  return $form_element;
}

/**
 * Set a warning message about problems with this webform configuration
 *
 * @return void
 */
function _webform_protected_downloads_set_warning() {
  $wpd_message = &drupal_static(__FUNCTION__);
  if (!isset($wpd_message)) {
    drupal_set_message(t('The mandatory email field has been removed from this webform. Protected downloads have been disabled. Please fix this problem.'));
    $wpd_message = TRUE;
  }
}

/**
 * Wrapper around watchdog
 *
 * @param string $msg 
 * @param array $variables 
 * @param string $severity 
 * @param string $link 
 * @return void
 */
function _webform_protected_downloads_log($msg, $variables, $severity = WATCHDOG_NOTICE, $link = NULL) {
  watchdog(WEBFORM_PROTECTED_DOWNLOADS_WATCHDOG_ID, $msg, $variables, $severity, $link);
}

/**
 * Implements hook_token_info() on behalf of text.module.
 */
function webform_protected_downloads_token_info() {
  // Text module provides three different text field types.
  $info['types']['protected-downloads'] = array(
    'name' => t('Protected Downloads'),
    'description' => t('Tokens related to Protected Downloads.'),
  );
  $info['tokens']['protected-downloads']['download-url'] = array(
    'name' => t('Download Url'),
    'description' => t("A url to access the protected downloads."),
  );
  $info['tokens']['protected-downloads']['download-expires'] = array(
    'name' => t('Download expiration date'),
    'description' => t("The date when the download url expires or 'never'."),
  );
  $info['tokens']['protected-downloads']['file-list'] = array(
    'name' => t('File list'),
    'description' => t("A list of the files attached to this node (not linked)."),
  );
  $info['tokens']['protected-downloads']['file-list-checksum'] = array(
    'name' => t('File list with ckecksum'),
    'description' => t("A list of the files attached to this node including checksum (not linked)."),
  );
  return $info;
}

/**
 * Implements of hook_tokens().
 */
function webform_protected_downloads_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  
  if ($type == 'protected-downloads') {
    $node = $data['node'];
    
    // fill basic tokens
    $hash = webform_protected_downloads_get_hash_details($data['hash']);
    $expires = $hash->expires > 0 ? format_date($hash->expires) : t('never');
    
    if (isset($tokens['download-url'])) {
      $replacements[$tokens['download-url']] = url('node/' . $node->nid . '/download/' . $hash->hash, array('absolute' => TRUE));
    }
    
    if (isset($tokens['download-expires'])) {
      $replacements[$tokens['download-expires']] = $expires;
    }
    
    // fill file list tokens
    $theme_function = 'webform_protected_downloads_mail_token_file_list';
    
    if (isset($tokens['file-list'])) {
      $replacements[$tokens['file-list']] = theme($theme_function, array('files' => $node->wpd['private_files']));
    }
    
    if (isset($tokens['file-list-checksum'])) {
      $replacements[$tokens['file-list-checksum']] = theme($theme_function, array('files' => $node->wpd['private_files'], 'checksum' => TRUE));
    }
  }
  return $replacements;
}

/**
 * Theme function for the file list that may be included in the confirmation
 * mail which is send to the user.
 *
 * @param array $files 
 * @param boolean $checksum 
 * @return string
 */
function theme_webform_protected_downloads_mail_token_file_list($variables) {
  $files = $variables['files'];
  $checksum = $variables['checksum'];
  
  $rendered_files = array();
  foreach ($files as $file) {
    
    if (!webform_protected_downloads_file_is_protected($file->nid, $file->fid)) {
      continue;
    }
    
    $args = array(
      '!filename' => $file->filename,
      '!size' => format_size($file->filesize),
      '!type' => $file->filemime
    );
    if ($checksum === FALSE) {
      $rendered_files[] = t('!filename (!size, !type)', $args);
    }
    else {
      $args['!checksum'] = md5_file(drupal_realpath($file->uri));
      $rendered_files[] = t('!filename (!size, !type, checksum: !checksum)', $args);
    }
  }
  return count($rendered_files) ? implode("\n", $rendered_files) : '';
}

/**
 * Find out what version of webform is currently installed.
 *
 * @return string
 */
function webform_protected_downloads_get_webform_version() {
  $module_info = system_get_info('module', 'webform');
  if (strpos($module_info['version'], '7.x-4') !== FALSE) {
    $version = '7.4';
  }
  else {
    $version = '7.3';
  }
  return $version;
}

/**
 * Retrieve the name used to access the "Required" form element for webform
 * components. This has been named "mandatory" in all webform versions prior to
 * 7.x-4.0-alpha7.
 *
 * @return string
 * @see https://drupal.org/node/1609324#webform-required
 */
function webform_protected_downloads_get_mandatory_field_name() {
  include_once DRUPAL_ROOT . '/includes/install.inc';
  if (drupal_get_installed_schema_version('webform') >= 7408) {
    return 'required';
  }
  else {
    return 'mandatory';
  }
}