diff --git a/components/file.inc b/components/file.inc index c28e2a1..607ab83 100644 --- a/components/file.inc +++ b/components/file.inc @@ -19,10 +19,10 @@ function _webform_defaults_file() { 'filtering' => array( 'types' => array('gif', 'jpg', 'png'), 'addextensions' => '', - 'size' => 800, + 'size' => '2 MB', ), - 'savelocation' => '', - 'width' => '', + 'scheme' => 'public', + 'directory' => '', 'title_display' => 0, 'description' => '', 'attributes' => array(), @@ -40,10 +40,6 @@ function _webform_theme_file() { 'render element' => 'form', 'file' => 'components/file.inc', ), - 'webform_render_file' => array( - 'render element' => 'element', - 'file' => 'components/file.inc', - ), 'webform_display_file' => array( 'render element' => 'element', 'file' => 'components/file.inc', @@ -57,6 +53,8 @@ function _webform_theme_file() { function _webform_edit_file($component) { $form = array(); $form['#theme'] = 'webform_edit_file'; + $form['#element_validate'] = array('_webform_edit_file_check_directory'); + $form['#after_build'] = array('_webform_edit_file_check_directory'); $form['validation']['filtering'] = array( '#element_validate' => array('_webform_edit_file_filtering_validate'), @@ -120,24 +118,38 @@ function _webform_edit_file($component) { '#type' => 'textfield', '#title' => t('Max Upload Size'), '#default_value' => $component['extra']['filtering']['size'], - '#description' => t('Enter the max file size a user may upload (in KB).'), + '#description' => t('Enter the max file size a user may upload such as 2 MB or 800 KB. Your server has a max upload size of @size.', array('@size' => format_size(file_upload_max_size()))), '#size' => 10, '#weight' => 3, - '#field_suffix' => t('KB'), '#default_value' => $component['extra']['filtering']['size'], '#parents' => array('extra', 'filtering', 'size'), '#element_validate' => array('_webform_edit_file_size_validate'), ); - $form['extra']['savelocation'] = array( + + $scheme_options = array(); + foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $stream_wrapper) { + $scheme_options[$scheme] = $stream_wrapper['name']; + } + $form['extra']['scheme'] = array( + '#type' => 'radios', + '#title' => t('Upload destination'), + '#options' => $scheme_options, + '#default_value' => $component['extra']['scheme'], + '#description' => t('Private file storage has significantly more overhead than public files, but restricts file access to users who can view submissions.'), + '#weight' => 4, + '#access' => count($scheme_options) > 1, + ); + $form['extra']['directory'] = array( '#type' => 'textfield', - '#title' => t('Upload Directory'), - '#default_value' => $component['extra']['savelocation'], - '#description' => t('Webform uploads are always saved in the site files directory. You may optionally specify a subfolder to store your files.'), - '#weight' => 3, - '#field_prefix' => file_stream_wrapper_get_instance_by_scheme(file_default_scheme())->getDirectoryPath() . '/webform/', - '#element_validate' => array('_webform_edit_file_check_directory'), - '#after_build' => array('_webform_edit_file_check_directory'), + '#title' => t('Upload directory'), + '#default_value' => $component['extra']['directory'], + '#description' => t('You may optionally specify a sub-directory to store your files.'), + '#weight' => 5, + '#field_prefix' => 'webform/', ); + + // TODO: Make managed_file respect the "size" parameter. + /* $form['display']['width'] = array( '#type' => 'textfield', '#title' => t('Width'), @@ -148,16 +160,25 @@ function _webform_edit_file($component) { '#weight' => 4, '#parents' => array('extra', 'width') ); + */ + return $form; } /** - * A Form API element validate function to check filesize is numeric. + * A Form API element validate function to check filesize is valid. */ function _webform_edit_file_size_validate($element) { if (!empty($element['#value'])) { - if (!is_numeric($element['#value']) || intval($element['#value']) != $element['#value']) { - form_error($element, t('Max upload size must be a number in KB.')); + $set_filesize = parse_size($element['#value']); + if ($set_filesize == FALSE) { + form_error($element, t('File size @value is not a valid filesize. Use a value such as 2 MB or 800 KB.', array('@value' => $element['#value']))); + } + else { + $max_filesize = parse_size(file_upload_max_size()); + if ($max_filesize < $set_filesize) { + form_error($element, t('An upload size of @value is too large, you are allow to upload files @max or less.', array('@value' => $element['#value'], '@max' => format_size($max_filesize)))); + } } } } @@ -168,17 +189,19 @@ function _webform_edit_file_size_validate($element) { * Ensure that the destination directory exists and is writable. */ function _webform_edit_file_check_directory($element) { - $base_dir = file_build_uri('webform'); - $destination_dir = file_build_uri('webform/' . $element['#value']); + $scheme = $element['extra']['scheme']['#value']; + $directory = $element['extra']['directory']['#value']; + + $destination_dir = file_stream_wrapper_uri_normalize($scheme . '://' . $directory . '/webform'); // Sanity check input to prevent use parent (../) directories. if (preg_match('/\.\.[\/\\\]/', $destination_dir . '/')) { - form_error($element, t('The save directory %directory is not valid.', array('%directory' => $destination_dir))); + form_error($element['extra']['directory'], t('The save directory %directory is not valid.', array('%directory' => $directory))); } else { $destination_success = file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY); if (!$destination_success) { - form_error($element, t('The save directory %directory could not be created. Check that the webform files directory is writtable.', array('%directory' => $destination_dir))); + form_error($element['extra']['directory'], t('The save directory %directory could not be created. Check that the webform files directory is writable.', array('%directory' => $directory))); } } @@ -304,214 +327,52 @@ function theme_webform_edit_file($variables) { * Implements _webform_render_component(). */ function _webform_render_file($component, $value = NULL, $filter = TRUE) { - // Normally every file component is given a unique ID based on its key. - if (isset($component['nid'])) { - $node = node_load($component['nid']); - $form_key = implode('_', webform_component_parent_keys($node, $component)); - } - // If being used as a default though, we don't yet have a form key. - else { - $form_key = 'default'; + // Cap the upload size according to the PHP limit. + $max_filesize = parse_size(file_upload_max_size()); + $set_filesize = $component['extra']['filtering']['size']; + if (!empty($set_filesize) && parse_size($set_filesize) < $max_filesize) { + $max_filesize = parse_size($set_filesize); } - $element[$form_key] = array( - '#type' => 'file', - //'#required' => $component['mandatory'], // Drupal core bug with required file uploads. + $element = array( + '#type' => 'managed_file', + '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', + '#required' => $component['mandatory'], + '#default_value' => isset($value[0]) ? $value[0] : NULL, '#attributes' => $component['extra']['attributes'], - '#tree' => FALSE, // file_check_upload assumes a flat $_FILES structure. - '#element_validate' => array( - '_webform_validate_file', - '_webform_required_file', // Custom required routine. + '#upload_validators' => array( + 'file_validate_size' => array($max_filesize), + 'file_validate_extensions' => array(implode(' ', $component['extra']['filtering']['types'])), ), + '#pre_render' => array_merge(element_info_property('managed_file', '#pre_render'), array('webform_file_allow_access')), + '#upload_location' => $component['extra']['scheme'] . '://webform/' . $component['extra']['directory'], + '#description' => $filter ? _webform_filter_descriptions($component['extra']['description']) : $component['extra']['description'], + '#weight' => $component['weight'], + '#theme_wrappers' => array('webform_element'), '#webform_component' => $component, - '#theme_wrappers' => array(), - ); - $element['#webform_component'] = $component; - $element['#weight'] = $component['weight']; - $element['#title'] = $filter ? _webform_filter_xss($component['name']) : $component['name']; - $element['#title_display'] = $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before'; - $element['#description'] = $filter ? _webform_filter_descriptions($component['extra']['description']) : $component['extra']['description']; - - $element['#theme'] = 'webform_render_file'; - $element['#theme_wrappers'] = array('webform_element'); - - // Change the 'width' option to the correct 'size' option. - if ($component['extra']['width'] > 0) { - $element[$form_key]['#size'] = $component['extra']['width']; - } - - // Handles the asterisk for mandatory fields. Note that this is only on the - // wrapper, not on the file field itself. We handle validation separately. - if ($component['mandatory']) { - $element['#required'] = TRUE; - } - - // Add a hidden element to store the FID for new files. - $element['_fid'] = array( - '#type' => 'hidden', - '#default_value' => '', - ); - - // Add a hidden element to store the FID for existing files. - $element['_old'] = array( - '#type' => 'hidden', - '#value' => isset($value[0]) ? $value[0] : NULL, ); return $element; } /** - * Render a File component. - */ -function theme_webform_render_file($variables) { - $element = $variables['element']; - - // Add information about the existing file, if any. - if (isset($element['#default_value'])) { - $element['_fid']['#value'] = $element['#default_value']; - } - $value = $element['_fid']['#value'] ? $element['_fid']['#value'] : $element['_old']['#value']; - - if ($value && ($file = webform_get_file($value))) { - $firstchild = array_shift(array_keys($element)); - $element[$firstchild]['#suffix'] = ' ' . l(t('Download @filename', array('@filename' => webform_file_name($file->uri))), webform_file_url($file->uri)) . (isset($element['#suffix']) ? $element['#suffix'] : ''); - $element[$firstchild]['#description'] = '
' . t('Uploading a new file will replace the current file.') . '
' . (isset($element[$firstchild]['#description']) ? $element[$firstchild]['#description'] : ''); - } - - return drupal_render_children($element); -} - -/** - * A Form API element validate function. + * Pre-render callback to allow access to uploaded files. * - * Fix Drupal core's handling of required file fields. - */ -function _webform_required_file($element, $form_state) { - $component = $element['#webform_component']; - $parents = $element['#array_parents']; - array_pop($parents); - $form_key = implode('_', $parents); - - // Do not validate requiredness on back or draft button. - if (isset($form_state['clicked_button']['#validate']) && empty($form_state['clicked_button']['#validate'])) { - return; - } - - // Check if a value is already set in the hidden field. - $values = $form_state['values']; - $key = array_shift($parents); - $found = FALSE; - while (isset($values[$key])) { - if (isset($values[$key])) { - $values = $values[$key]; - $found = TRUE; - } - else { - $found = FALSE; - } - $key = array_shift($parents); - } - - if (!$found || (empty($values['_fid']) && empty($values['_old']))) { - if (empty($_FILES['files']['name'][$form_key]) && $component['mandatory']) { - form_error($element, t('%field field is required.', array('%field' => $component['name']))); - } - } -} - -/** - * A Form API element validate function. + * Files that have not yet been saved into a submission must be accessible to + * the user who uploaded it, but no one else. After the submission is saved, + * access is granted through the file_usage table. Before then, we use a + * $_SESSION value to record a user's upload. * - * Ensure that the uploaded file matches the specified file types. + * @see webform_file_download() */ -function _webform_validate_file($element, &$form_state) { - $component = $element['#webform_component']; - $form_key = implode('_', $element['#parents']); - - if (empty($_FILES['files']['name'][$form_key]) || form_get_error($element)) { - return; - } - - // Build a human readable list of extensions: - $extensions = $component['extra']['filtering']['types']; - $extension_list = ''; - if (count($extensions) > 1) { - for ($n = 0; $n < count($extensions) - 1; $n++) { - $extension_list .= $extensions[$n] . ', '; - } - $extension_list .= 'or ' . $extensions[count($extensions) - 1]; - } - elseif (!empty($extensions)) { - $extension_list = $extensions[0]; - } - - if (in_array('jpg', $extensions)) { - $extensions[] = 'jpeg'; +function webform_file_allow_access($element) { + if (!empty($element['#value']['fid'])) { + $fid = $element['#value']['fid']; + $_SESSION['webform_files'][$fid] = $fid; } - $strrpos = function_exists('mb_strrpos') ? 'mb_strrpos' : 'strrpos'; - $dot = $strrpos($_FILES['files']['name'][$form_key], '.'); - $extension = drupal_strtolower(drupal_substr($_FILES['files']['name'][$form_key], $dot+1)); - $file_error = FALSE; - if (!empty($extensions) && !in_array($extension, $extensions)) { - form_error($element, t("Files with the '%ext' extension are not allowed, please upload a file with a %exts extension.", array('%ext' => $extension, '%exts' => $extension_list))); - $file_error = TRUE; - } - - // Now let's check the file size (limit is set in KB). - if ($_FILES['files']['size'][$form_key] > $component['extra']['filtering']['size'] * 1024) { - form_error($element, t("The file '%filename' is too large (%filesize KB). Please upload a file %maxsize KB or smaller.", array('%filename' => $_FILES['files']['name'][$form_key], '%filesize' => (int) ($_FILES['files']['size'][$form_key]/1024), '%maxsize' => $component['extra']['filtering']['size']))); - $file_error = TRUE; - } - - // Save the file to a temporary location. - if (!$file_error) { - $upload_dir = file_build_uri('webform/' . $component['extra']['savelocation']); - if (file_prepare_directory($upload_dir, FILE_CREATE_DIRECTORY)) { - // Note that the $extensions list was already validated above but needs to - // be passed into file_save_upload() for it to pass internal validation - // and not use the default extension checklist. - $file = file_save_upload($form_key, array('file_validate_extensions' => array(implode(' ', $extensions))), $upload_dir); - if ($file) { - // Set the hidden field value. - $parents = $element['#array_parents']; - array_pop($parents); - $parents[] = '_fid'; - form_set_value(array('#parents' => $parents), $file->fid, $form_state); - } - else { - drupal_set_message(t('The uploaded file %filename was unable to be saved. The destination directory may not be writable.', array('%filename' => $file->filename)), 'error'); - } - } - else { - drupal_set_message(t('The uploaded file was unable to be saved. The destination directory does not exist.'), 'error'); - } - } -} - -/** - * Implements _webform_submit_component(). - */ -function _webform_submit_file($component, $value) { - - if ($value['_fid'] && ($file = webform_get_file($value['_fid']))) { - // Save any new files permanently. - $file->status = FILE_STATUS_PERMANENT; - file_save($file); - - // Delete any previous files. - if ($value['_old'] && $value['_old'] != $value['_fid'] && ($existing = webform_get_file($value['_old']))) { - file_delete($existing); - } - - $value = $file->fid; - } - else { - $value = $value['_old'] ? $value['_old'] : NULL; - } - - return $value; + return $element; } /** @@ -646,3 +507,22 @@ function webform_get_file($fid) { // system error. return $fid ? file_load($fid) : FALSE; } + +/** + * Given a submission with file_usage set, add or remove file usage entries. + */ +function webform_file_usage_adjust($submission) { + if (isset($submission->file_usage)) { + $files = file_load_multiple($submission->file_usage['added_fids']); + foreach ($files as $file) { + file_usage_add($file, 'webform', 'submission', $submission->sid); + } + + $files = file_load_multiple($submission->file_usage['deleted_fids']); + foreach ($files as $file) { + file_usage_delete($file, 'webform', 'submission', $submission->sid); + file_delete($file); + } + } +} + diff --git a/webform.install b/webform.install index a80b24e..2c13656 100644 --- a/webform.install +++ b/webform.install @@ -717,3 +717,81 @@ function webform_update_7316() { db_add_field('webform', 'total_submit_interval', array('type' => 'int', 'not null' => TRUE, 'default' => -1)); } } + +/** + * Upgrade file components to support the new AJAX-upload element. + */ +function webform_update_7317() { + $result = db_select('webform_component', 'wc', array('fetch' => PDO::FETCH_ASSOC)) + ->fields('wc') + ->condition('type', 'date') + ->execute(); + foreach ($result as $component) { + $component['extra'] = unserialize($component->extra); + if (!isset($component['extra']['directory'])) { + $component['extra']['directory'] = $component['extra']['savelocation']; + $component['extra']['scheme'] = file_default_scheme(); + $component['extra']['size'] = $component['extra']['size'] . ' KB'; + unset($component['extra']['filtering']['addextensions']); + unset($component['extra']['savelocation']); + $component['extra'] = serialize($component['extra']); + drupal_write_record('webform_component', $component, array('nid', 'cid')); + } + } +} + +/** + * Add file usage entries for all files uploaded through Webform. + */ +function webform_update_7318(&$sandbox) { + if (!isset($sandbox['progress'])) { + // Initialize batch update information. + $sandbox['progress'] = 0; + $sandbox['last_fid_processed'] = -1; + $sandbox['max'] = db_select('file_managed') + ->condition('uri', '%' . db_like('://webform/') . '%', 'LIKE') + ->countQuery() + ->execute() + ->fetchField(); + } + + // Process all files attached to a given revision during the same batch. + $limit = variable_get('webform_update_batch_size', 100); + $files = db_select('file_managed', 'f') + ->fields('f') + ->condition('uri', '%' . db_like('://webform/') . '%', 'LIKE') + ->condition('fid', $sandbox['last_fid_processed'], '>') + ->orderBy('fid', 'ASC') + ->range(0, $limit) + ->execute() + ->fetchAllAssoc('fid', PDO::FETCH_ASSOC); + + // Determine each submission with which a file is associated. + if (!empty($files)) { + foreach ($files as $fid => $file) { + $file = (object) $file; + $sids = db_query('SELECT wsd.sid FROM {webform_component} wc INNER JOIN {webform_submitted_data} wsd ON wc.nid = wsd.nid AND wc.type = :file WHERE data = :fid', array(':file' => 'file', ':fid' => $file->fid))->fetchAllAssoc('sid', PDO::FETCH_ASSOC); + foreach ($sids as $sid => $row) { + file_usage_add($file, 'webform', 'submission', $sid); + } + + // Update our progress information for the batch update. + $sandbox['progress']++; + $sandbox['last_fid_processed'] = $file->fid; + } + } + + // If less than limit was processed, the update process is finished. + if (count($files) < $limit || $sandbox['progress'] == $sandbox['max']) { + $finished = TRUE; + } + + // If there's no max value then there's nothing to update and we're finished. + if (empty($sandbox['max']) || isset($finished)) { + return t('Webform file entries created in the file_usage table.'); + } + else { + // Indicate our current progress to the batch update system. + $sandbox['#finished'] = $sandbox['progress'] / $sandbox['max']; + } +} diff --git a/webform.module b/webform.module index 15f704b..c8e86d9 100644 --- a/webform.module +++ b/webform.module @@ -888,46 +888,102 @@ function webform_webform_submission_actions($node, $submission) { } /** + * Implements hook_webform_submission_update(). + * + * We implement our own hook here to facilitate the File component, which needs + * to clean up manage file usage records and delete files from submissions that + * have been edited if necessary. + */ +function webform_webform_submission_presave($node, &$submission) { + // Check if there are any file components in this submission and if any of + // them currently contain files. + $has_file_components = FALSE; + $new_fids = array(); + $old_fids = array(); + + foreach ($node->webform['components'] as $cid => $component) { + if ($component['type'] == 'file') { + $has_file_components = TRUE; + if (!empty($submission->data[$cid]['value'])) { + $new_fids = array_merge($new_fids, $submission->data[$cid]['value']); + } + } + } + + if ($has_file_components) { + // If we're updating a submission, build a list of previous files. + if (isset($submission->sid)) { + $old_submission = webform_get_submission($node->nid, $submission->sid, TRUE); + + foreach ($node->webform['components'] as $cid => $component) { + if ($component['type'] == 'file') { + if (!empty($old_submission->data[$cid]['value'])) { + $old_fids = array_merge($old_fids, $old_submission->data[$cid]['value']); + } + } + } + } + + // Save the list of added or removed files so we can add usage in + // hook_webform_submission_insert() or _update(). + $submission->file_usage = array( + // Diff the old against new to determine what files were deleted. + 'deleted_fids' => array_diff($old_fids, $new_fids), + // Diff the new files against old to determine new uploads. + 'added_fids' => array_diff($new_fids, $old_fids) + ); + } +} + +/** + * Implements hook_webform_submission_insert(). + */ +function webform_webform_submission_insert($node, $submission) { + if (isset($submission->file_usage)) { + webform_component_include('file'); + webform_file_usage_adjust($submission); + } +} + +/** + * Implements hook_webform_submission_update(). + */ +function webform_webform_submission_update($node, $submission) { + if (isset($submission->file_usage)) { + webform_component_include('file'); + webform_file_usage_adjust($submission); + } +} + +/** * Implements hook_file_download(). * * Only allow users with view webform submissions to download files. */ function webform_file_download($uri) { - global $user; + module_load_include('inc', 'webform', 'includes/webform.submissions'); - // Determine whether this file was a webform upload. If it was, retrieve file - // information, plus the user id of the uploader. - $file = db_query("SELECT ws.uid, f.* FROM {file_managed} f INNER JOIN {webform_submitted_data} wsd ON f.fid = wsd.data INNER JOIN {webform_submissions} ws ON ws.sid = wsd.sid INNER JOIN {webform_component} wc ON wc.cid = wsd.cid WHERE f.uri = :uri AND wc.type = :file", array('uri' => $uri, ':file' => 'file'))->fetchObject(); - if ($file) { - // Allow file access for admins, or for users who are viewing their own - // submissions. - if (user_access('access all webform results') || ($user->uid == $file->uid && user_access('access own webform results'))) { - // TODO: This is a copy/paste from file_file_download. Switch to using - // file_get_content_headers() instead if http://drupal.org/node/943112 - // gets committed. - $name = mime_header_encode($file->filename); - $type = mime_header_encode($file->filemime); - // Serve images, text, and flash content for display rather than download. - $inline_types = variable_get('file_inline_types', array('^text/', '^image/', 'flash$')); - $disposition = 'attachment'; - foreach ($inline_types as $inline_type) { - // Exclamation marks are used as delimiters to avoid escaping slashes. - if (preg_match('!' . $inline_type . '!', $file->filemime)) { - $disposition = 'inline'; - } - } - return array( - 'Content-Type' => $type . '; name="' . $name . '"', - 'Content-Length' => $file->filesize, - 'Content-Disposition' => $disposition . '; filename="' . $name . '"', - 'Cache-Control' => 'private', - ); + // Determine whether this file was a webform upload. + $row = db_query("SELECT fu.id as sid, f.fid FROM {file_managed} f LEFT JOIN {file_usage} fu ON f.fid = fu.fid AND fu.module = :webform AND fu.type = :submission WHERE f.uri = :uri", array('uri' => $uri, ':webform' => 'webform', ':submission' => 'submission'))->fetchObject(); + if ($row) { + $file = file_load($row->fid); + } + if (!empty($file->sid)) { + $submissions = webform_get_submissions(array('sid' => $row->sid)); + $submission = reset($submissions); + } + + // Grant access based on access to the submission. + if (!empty($submission)) { + $node = node_load($submission->nid); + if (webform_submission_access($node, $submission)) { + return file_get_content_headers($file); } - // This is a webform-controlled file, but the user doesn't have access. - return -1; } - // This is not a webform-controlled file. - return NULL; + // Grant access to files uploaded by a user before the submission is saved. + elseif (!empty($file) && !empty($_SESSION['webform_files'][$file->fid])) { + return file_get_content_headers($file); + } } /** @@ -1742,6 +1798,10 @@ function webform_client_form($form, $form_state, $node, $submission, $is_draft = module_load_include('inc', 'webform', 'includes/webform.components'); module_load_include('inc', 'webform', 'includes/webform.submissions'); + $form['#process'] = array( + 'webform_client_form_includes', + ); + // If in a multi-step form, a submission ID may be specified in form state. // Load this submission. This allows anonymous users to use auto-save. if (empty($submission) && !empty($form_state['values']['details']['sid'])) { @@ -1903,6 +1963,19 @@ function webform_client_form($form, $form_state, $node, $submission, $is_draft = } /** + * Process function for webform_client_form(). + * + * Include all the enabled components for this form to ensure availability. + */ +function webform_client_form_includes($form, $form_state) { + $components = webform_components(); + foreach ($components as $component_type => $component) { + webform_component_include($component_type); + } + return $form; +} + +/** * Check if a component should be displayed on the current page. */ function _webform_client_form_rule_check($node, $component, $page_num, $form_state = NULL, $submission = NULL) {