You are here

s3fs_cors.module in S3 File System CORS Upload 7

Same filename and directory in other branches
  1. 8 s3fs_cors.module

Allow uploading of files directly to AmazonS3 via the browser using CORS.

File

s3fs_cors.module
View source
<?php

/**
 * @file
 * Allow uploading of files directly to AmazonS3 via the browser using CORS.
 */

/**
 * Implements hook_menu().
 */
function s3fs_cors_menu() {
  $items = array();
  $items['ajax/s3fs_cors'] = array(
    'title' => 'S3 Request Signature Callback',
    'page callback' => 's3fs_cors_sign_request',
    // TODO: Add a permission for this? Or at least restrict to logged-in users.
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/media/s3fs/cors'] = array(
    'title' => 'CORS Upload',
    'description' => 'Configure S3 File System CORS Upload.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      's3fs_cors_admin_form',
    ),
    'access arguments' => array(
      'administer s3fs CORS',
    ),
    'file' => 's3fs_cors.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 20,
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function s3fs_cors_permission() {
  return array(
    'administer s3fs CORS' => array(
      'title' => t('Administer S3 File System CORS Upload'),
    ),
    'generate s3fs CORS upload parameters' => array(
      'title' => t('Generate CORS Upload Parameters'),
    ),
  );
}

/**
 * Implements hook_help().
 */
function s3fs_cors_help($path, $arg) {
  if ($path == 'admin/config/media/s3fs/cors') {
    $msg = t('Configure your S3 Bucket\'s CORS configuration from this page.
      Please be aware that submitting this form will <b>overwrite</b> your bucket\'s current CORS config.<br>
      So if you intend to configure your bucket\'s CORS policy manually, <b>never submit this form</b>.');
    return "<p>{$msg}</p>";
  }
}

/**
 * Implements hook_element_info().
 */
function s3fs_cors_element_info() {
  $config = _s3fs_get_config();
  $file_path = drupal_get_path('module', 's3fs_cors');
  $types['s3fs_cors_upload'] = array(
    '#input' => TRUE,
    '#process' => array(
      's3fs_cors_upload_process',
    ),
    '#value_callback' => 's3fs_cors_upload_value',
    '#element_validate' => array(
      's3fs_cors_upload_validate',
    ),
    '#pre_render' => array(
      'file_managed_file_pre_render',
    ),
    '#theme' => 's3fs_cors_upload',
    '#theme_wrappers' => array(
      'form_element',
    ),
    '#upload_validators' => array(),
    // The default directory for uploaded files. This will be overidden later,
    // but we must set it here to satisfy an internal Drupalism.
    '#upload_location' => 's3://',
    '#size' => 22,
    '#extended' => FALSE,
    '#attached' => array(
      'js' => array(
        $file_path . '/s3fs_cors.js',
        // The File module's JS does things like client side validation for us.
        drupal_get_path('module', 'file') . '/file.js',
      ),
      'library' => array(
        array(
          'system',
          'ui.progressbar',
        ),
      ),
    ),
  );
  return $types;
}

/**
 * Implements hook_field_widget_info().
 */
function s3fs_cors_field_widget_info() {
  return array(
    's3fs_cors' => array(
      'label' => t('S3 CORS File Upload'),
      'field types' => array(
        'file',
        'image',
      ),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
        'default value' => FIELD_BEHAVIOR_NONE,
      ),
      // Make sure our widget doesn't take over the File widget as the default.
      'weight' => 999,
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function s3fs_cors_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {

  // A lot of this is borrowed from file_field_widget_form().
  $defaults = array(
    'fid' => 0,
    'display' => !empty($field['settings']['display_default']),
    'description' => '',
  );

  // Load the items for form rebuilds from the field state as they might not be
  // in $form_state['values'] because of validation limitations. Also, they are
  // only passed in as $items when editing existing entities.
  $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state);
  if (isset($field_state['items'])) {
    $items = $field_state['items'];
  }

  // Since the upload isn't going through PHP, it is limited only by S3's max
  // filesize for single-part uploads, which is 5GB.
  $validators = file_field_widget_upload_validators($field, $instance);
  $max_filesize = parse_size('5G');

  // If the user is on IE8 or 9, they can't do CORS uploads, so PHP's upload
  // size limit matters.
  if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/MSIE [8-9]\\.0/', $_SERVER['HTTP_USER_AGENT'])) {
    $max_filesize = parse_size(file_upload_max_size());
  }

  // If the admin has specified a smaller max size, use that.
  if (!empty($instance['settings']['max_filesize']) && parse_size($instance['settings']['max_filesize']) < $max_filesize) {
    $max_filesize = parse_size($instance['settings']['max_filesize']);
  }
  $validators['file_validate_size'] = array(
    $max_filesize,
  );

  // We use the s3fs_cors_upload type, which is based of of the managed_file type,
  // extended with some enhancements for CORS.
  $element_info = element_info('s3fs_cors_upload');
  $element += array(
    '#type' => 's3fs_cors_upload',
    '#upload_location' => file_field_widget_uri($field, $instance),
    // TODO: See https://www.drupal.org/node/2185925 for ideas on how to
    // deal with file_field_widget_upload_validators() being too restrictive on file size.
    '#upload_validators' => $validators,
    '#value_callback' => 's3fs_cors_field_widget_value',
    '#process' => array_merge($element_info['#process'], array(
      'file_field_widget_process',
    )),
    // Allows this field to return an array instead of a single value.
    '#extended' => TRUE,
  );
  if ($field['cardinality'] == 1) {

    // Set the default value.
    $element['#default_value'] = !empty($items) ? $items[0] : $defaults;

    // If there's only one field, return it as delta 0.
    if (empty($element['#default_value']['fid'])) {
      $element['#description'] = theme('file_upload_help', array(
        'description' => $element['#description'],
        'upload_validators' => $element['#upload_validators'],
      ));
    }
    $elements = array(
      $element,
    );
  }
  else {

    // If there are multiple values, add an element for each existing one.
    foreach ($items as $item) {
      $elements[$delta] = $element;
      $elements[$delta]['#default_value'] = $item;
      $elements[$delta]['#weight'] = $delta;
      $delta++;
    }

    // And then add one more empty row for new uploads except when this is a
    // programmed form as it is not necessary.
    if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) {
      $elements[$delta] = $element;
      $elements[$delta]['#default_value'] = $defaults;
      $elements[$delta]['#weight'] = $delta;
      $elements[$delta]['#required'] = $element['#required'] && $delta == 0;
    }

    // The group of elements all-together need some extra functionality
    // after building up the full list (like draggable table rows).
    $elements['#file_upload_delta'] = $delta;
    $elements['#theme'] = 'file_widget_multiple';
    $elements['#theme_wrappers'] = array(
      'fieldset',
    );
    $elements['#process'] = array(
      'file_field_widget_process_multiple',
    );
    $elements['#title'] = $element['#title'];
    $elements['#description'] = $element['#description'];
    $elements['#field_name'] = $element['#field_name'];
    $elements['#language'] = $element['#language'];
    $elements['#display_field'] = !empty($field['settings']['display_field']) ? $field['settings']['display_field'] : 0;

    // Add some properties that will eventually be added to the file upload
    // field. These are added here so that they may be referenced easily through
    // a hook_form_alter().
    $elements['#file_upload_title'] = t('Add a new file');
    $elements['#file_upload_description'] = theme('file_upload_help', array(
      'description' => '',
      'upload_validators' => $elements[0]['#upload_validators'],
    ));
  }
  return $elements;
}

/**
 * Element process function for s3fs_cors_upload element.
 *
 * Expands the element to include Upload and Remove buttons, as well as support
 * for a default value.
 *
 * In order to take advantage of the work that file.module is already doing for
 * elements of type #managed_file, we stick to the same naming convention here.
 */
function s3fs_cors_upload_process($element, &$form_state, &$form) {
  $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0;
  $element['#file'] = $fid ? file_load($fid) : FALSE;
  $element['#tree'] = TRUE;
  $parents_id = implode('_', $element['#parents']);

  // AJAX settings used for upload and remove buttons.
  $ajax_settings = array(
    'callback' => 's3fs_cors_upload_js',
    'wrapper' => "{$element['#id']}-ajax-wrapper",
    'method' => 'replace',
    'effect' => 'fade',
  );

  // The "Upload" button.
  $element['upload_button'] = array(
    '#name' => "{$parents_id}_upload_button",
    '#type' => 'submit',
    '#value' => t('Upload'),
    '#validate' => array(),
    '#limit_validation_errors' => array(
      $element['#parents'],
    ),
    '#attributes' => array(
      'class' => array(
        'cors-form-submit',
      ),
    ),
    '#weight' => -5,
    '#submit' => array(
      's3fs_cors_upload_submit',
    ),
    '#ajax' => $ajax_settings,
  );

  // The "Remove" button.
  $element['remove_button'] = array(
    '#name' => "{$parents_id}_remove_button",
    '#type' => 'submit',
    '#value' => t('Remove'),
    '#validate' => array(),
    '#limit_validation_errors' => array(
      $element['#parents'],
    ),
    '#attributes' => array(
      'class' => array(
        'cors-form-remove',
      ),
    ),
    '#weight' => -5,
    '#submit' => array(
      's3fs_cors_remove_submit',
    ),
    '#ajax' => $ajax_settings,
  );

  // The file upload field itself.
  $element['upload'] = array(
    '#name' => "files[{$parents_id}]",
    '#type' => 'file',
    '#title' => t('Choose a file.'),
    '#title_display' => 'invisible',
    '#size' => $element['#size'],
    '#theme_wrappers' => array(),
    '#weight' => -10,
    '#attributes' => array(
      'class' => array(
        's3fs-cors-upload-file',
      ),
      'multiple' => 'multiple',
    ),
  );
  if ($fid && $element['#file']) {
    $element['filelink'] = array(
      '#type' => 'markup',
      '#markup' => theme('file_link', array(
        'file' => $element['#file'],
      )) . ' ',
      '#weight' => -10,
    );
  }

  // Add the extension list to the page as JavaScript settings.
  if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
    $extension_list = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0])));
    $element['upload']['#attached']['js'] = array(
      array(
        'type' => 'setting',
        'data' => array(
          'file' => array(
            'elements' => array(
              "#{$element['#id']}-upload" => $extension_list,
            ),
            'max_upload_size' => array(
              $element['upload']['#name'] => $element['#upload_validators']['file_validate_size'][0],
            ),
          ),
        ),
      ),
    );
  }

  // These hidden elements get populated by javascript after uploading the file
  // to S3. They are then used by the value callback to save the new file record
  // to the DB.
  $element['fid'] = array(
    '#type' => 'hidden',
    '#value' => $fid,
    '#attributes' => array(
      'class' => array(
        'fid',
      ),
    ),
  );
  $element['filename'] = array(
    '#type' => 'hidden',
    '#default_value' => isset($element['#file']->filename) ? $element['#file']->filename : '',
    '#attributes' => array(
      'class' => array(
        'filename',
      ),
    ),
    // This keeps theme_file_widget() happy.
    '#markup' => '',
  );
  $element['filemime'] = array(
    '#type' => 'hidden',
    '#attributes' => array(
      'class' => array(
        'filemime',
      ),
    ),
    '#default_value' => isset($element['#file']->filemime) ? $element['#file']->filemime : '',
  );
  $element['filesize'] = array(
    '#type' => 'hidden',
    '#attributes' => array(
      'class' => array(
        'filesize',
      ),
    ),
    '#default_value' => isset($element['#file']->filesize) ? $element['#file']->filesize : '',
  );

  // Add a class to the <form> element so we can find it with JS later.
  $form['#attributes']['class'][] = 's3fs-cors-upload-form';
  $element['#prefix'] = "<div id=\"{$element['#id']}-ajax-wrapper\">";
  $element['#suffix'] = '</div>';
  return $element;
}

/**
 * Value callback for s3fs_cors_upload element type.
 */
function s3fs_cors_upload_value(&$element, $input = FALSE, $form_state = NULL) {
  global $user;
  $fid = 0;
  $return = array();
  $parents = $element['#parents'];
  $parents_id = implode('_', $parents);
  if (!empty($input['fid'])) {

    // The input will have a non-zero fid only when saving the full node form.
    // We don't want to do anything when that happens, because everything has
    // already been done in the AJAX workflow.
    return $input;
  }
  $remove_button_clicked = isset($form_state['input']['_triggering_element_name']) && $form_state['input']['_triggering_element_name'] == "{$parents_id}_remove_button";

  // TODO: I'm relatively sure this is useless, because of how we deal with the files.
  // But just in case, I'm going to leave it around for now.
  // Find the current value of this field from the form state, if it's there.
  $form_state_fid = $form_state['values'];
  foreach ($parents as $parent) {
    $form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0;
  }
  if ($element['#extended'] && isset($form_state_fid['fid'])) {
    $fid = $form_state_fid['fid'];
  }
  elseif (is_numeric($form_state_fid)) {
    $fid = $form_state_fid;
  }

  // If there's valid input, save the new upload.
  if ($input !== FALSE && $fid == 0 && !empty($input['filename']) && !$remove_button_clicked) {
    $return = $input;
    $base_dir = 's3://';
    if (!empty($element['#upload_location'])) {
      $base_dir = $element['#upload_location'];
      if (!preg_match('/\\/$/', $base_dir)) {
        $base_dir .= '/';
      }
    }
    if (module_exists('transliteration') && variable_get('transliteration_file_uploads', 1)) {
      $input['filename'] = transliteration_clean_filename($input['filename']);
    }

    // Construct a Drupal file object.
    $file = new stdClass();
    $file->uid = $user->uid;
    $file->filename = $input['filename'];
    $file->filesize = $input['filesize'];
    $file->filemime = $input['filemime'];
    $file->uri = file_destination("{$base_dir}{$input['filename']}", FILE_EXISTS_RENAME);
    $file->status = 0;
    $file->timestamp = REQUEST_TIME;

    // Save the uploaded file to the file_managed table.
    $file = _s3fs_cors_file_save($file);
    $return['fid'] = $file->fid;

    // Store the file's metadata into s3fs's metadata cache.
    $wrapper = new S3fsStreamWrapper();
    $wrapper
      ->writeUriToCache($file->uri);
  }
  if ($input === FALSE || $remove_button_clicked) {

    // If there is no input, or the remove button was just clicked, set the
    // default value.
    if ($element['#extended']) {
      $default_fid = isset($element['#default_value']['fid']) ? $element['#default_value']['fid'] : 0;
      $return = isset($element['#default_value']) ? $element['#default_value'] : array(
        'fid' => 0,
      );
    }
    else {
      $default_fid = isset($element['#default_value']) ? $element['#default_value'] : 0;
      $return = array(
        'fid' => 0,
      );
    }

    // Confirm that the file exists when used as a default value.
    if ($default_fid && ($file = file_load($default_fid))) {
      $return['fid'] = $file->fid;
    }
    else {
      $return['fid'] = $fid;
    }
  }
  return $return;
}

/**
 * The #value_callback for the s3fs_cors field element.
 *
 * This is pretty much a copy of file_field_widget_value(), but modified to use
 * the s3fs_cors_upload_value() function instead of the file.module one.
 */
function s3fs_cors_field_widget_value($element, $input = FALSE, $form_state) {
  if ($input) {

    // Checkboxes lose their value when empty.
    // If the display field is present make sure its unchecked value is saved.
    $field = field_widget_field($element, $form_state);
    if (empty($input['display'])) {
      $input['display'] = $field['settings']['display_field'] ? 0 : 1;
    }
  }

  // Handle uploads and the like.
  $return = s3fs_cors_upload_value($element, $input, $form_state);

  // Ensure that all the required properties are returned, even if empty.
  $return += array(
    'fid' => 0,
    'display' => 1,
    'description' => '',
  );
  return $return;
}

/**
 * Validation callback for s3fs_cors element type.
 */
function s3fs_cors_upload_validate(&$element, &$form_state) {
  file_managed_file_validate($element, $form_state);

  // Consolidate the array value of this field to a single FID.
  if (!$element['#extended']) {
    form_set_value($element, $element['fid']['#value'], $form_state);
  }
}
function s3fs_cors_upload_js($form, &$form_state) {

  // Find the element that triggered the AJAX callback and return it so that it
  // can be replaced.
  $parents = $form_state['triggering_element']['#array_parents'];
  $button_key = array_pop($parents);
  $element = drupal_array_get_nested_value($form, $parents);
  return $element;
}

/**
 * Submit callback for the remove button on s3fs_cors elements.
 */
function s3fs_cors_remove_submit($form, &$form_state) {
  $parents = $form_state['triggering_element']['#array_parents'];

  // Drop the button_key value off the end of the parents array, since we don't need it.
  array_pop($parents);
  $element = drupal_array_get_nested_value($form, $parents);

  // If it's a temporary file we can safely remove it immediately, otherwise
  // it's up to the implementing module to clean up files that are in use.
  if ($element['#file'] && $element['#file']->status == 0) {
    file_delete($element['#file']);
  }

  // Update both $form_state['values'] and $form_state['input'] to reflect
  // that the file has been removed, so that the form is rebuilt correctly.
  // $form_state['values'] must be updated in case additional submit handlers
  // run, and for form building functions that run during the rebuild, such as
  // when the s3fs_cors_upload element is part of a field widget.
  // $form_state['input'] must be updated so that s3fs_cors_upload_value()
  // has correct information during the rebuild.
  $values_element = $element['#extended'] ? $element['fid'] : $element;
  form_set_value($values_element, NULL, $form_state);
  drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], NULL);

  // Set the form to rebuild so that $form is correctly updated in response to
  // processing the file removal.
  $form_state['rebuild'] = TRUE;
}

/**
 * Submit callback for the upload button on s3fs_cors elements.
 */
function s3fs_cors_upload_submit($form, &$form_state) {

  // No action is needed here because all file uploads on the form are
  // processed by s3fs_cors_upload_value().
  // Since this function did not change $form_state, a rebuild isn't
  // necessary; setting $form_state['redirect'] to FALSE would suffice.
  // However, we choose to always rebuild, to keep the form processing
  // workflow consistent between the two buttons.
  $form_state['rebuild'] = TRUE;
}

/**
 * AJAX callback to create paramaters necessary for submitting a CORS request.
 *
 * Use the filename, filesize, and filemime properies in $_POST in conjunction
 * with the AWS key/secret in order to create the required parameters for
 * sending a file to S3 via a CORS request.
 *
 * The heavy lifting here is handled by the AWSSDK.
 *
 * @see s3fs_cors.js
 */
function s3fs_cors_sign_request() {

  // Be careful with these, as they are user input.
  $filename = $_POST['filename'];
  if (module_exists('transliteration') && variable_get('transliteration_file_uploads', 1)) {
    $filename = transliteration_clean_filename($filename);
  }
  $filemime = $_POST['filemime'];
  $form_build_id = $_POST['form_build_id'];
  $field_name = $_POST['field_name'];
  $library = _s3fs_load_awssdk2_library();
  if (!$library['loaded']) {
    $error = t('Unable to load the AWS SDK for PHP. Please check you have the library installed correctly and have your S3 credentials configured.');
    header('HTTP/1.1 500 Internal Server Error');
    drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
    print json_encode(array(
      'error' => $error,
    ));
    drupal_exit();
  }

  // Retrieve the form from which this request is being made, so we can get some much-needed context.
  $form_state = form_state_defaults();
  $form = form_get_cache($form_build_id, $form_state);
  if (!$form) {

    // If $form cannot be loaded from the cache, the form_build_id in $_POST
    // must be invalid, which means that someone performed a POST request onto
    // system/ajax without actually viewing the concerned form in the browser.
    // This is likely a hacking attempt as it never happens under normal
    // circumstances, so we just do nothing.
    watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING);
    drupal_exit();
  }

  // Get the "File directory" setting for this field, as a URI.
  $instance_info = field_info_instance($form['#entity_type'], $field_name, $form['#bundle']);
  $field_info = field_info_field($field_name);

  // "File directory" supports tokens, so we have to do the replacement ourselves.
  $file_directory = token_replace($instance_info['settings']['file_directory']);
  $file_scheme = $field_info['settings']['uri_scheme'];

  // S3 config and client initialization.
  $config = _s3fs_get_config();
  $key = $file_scheme . '_folder';
  $config['root_folder'] = isset($config[$key]) ? $config[$key] : '';
  $client = _s3fs_get_amazons3_client($config);

  // Use file_create_filename() to avoid overwriting an existing file.
  $file_directory_uri = "{$file_scheme}://{$file_directory}";
  $uri = file_create_filename($filename, $file_directory_uri);
  $acl = strpos($uri, 'private://') === FALSE ? 'public-read' : 'private';
  $s3_scheme_prefix = !empty($config['root_folder']) ? $config['root_folder'] . '/' : '';
  $s3_key = $s3_scheme_prefix . file_uri_target($uri);
  $formInputs = array(
    'acl' => $acl,
    'Content-Type' => $filemime,
    // The root folder is not part of a file's URI, so we don't add it until
    // we're setting up the final s3 parameters.
    'key' => $s3_key,
    'X-Amz-Expires' => '21600',
    'success_action_status' => '201',
  );
  $options = [
    [
      'bucket' => $config['bucket'],
    ],
    [
      'acl' => $acl,
    ],
    [
      'starts-with',
      '$key',
      $s3_key,
    ],
    [
      'starts-with',
      '$Content-Type',
      '',
    ],
    [
      'success_action_status' => '201',
    ],
    [
      'X-Amz-Expires' => '21600',
    ],
  ];

  // Allow other modules to change the options.
  drupal_alter('s3fs_cors_sign_request_options', $options);
  if (!empty($config['cache_control_header'])) {
    $options['Cache-Control'] = $config['cache_control_header'];
  }
  module_load_include('inc', 's3fs_cors', 's3fs_cors.post_object_v4.class');
  $post_object = new S3fsCorsPostObjectV4($client, $config['bucket'], $formInputs, $options);

  // Use next code with AWS SDK v3.
  // $post_object = new Aws\S3\PostObjectV4($client, $config['bucket'], $formInputs, $options);
  $data = array(
    'inputs' => $post_object
      ->getFormInputs(),
    'form' => $post_object
      ->getFormAttributes(),
    // Tell our javascript the filename that Drupal ended up giving us.
    'file_real' => drupal_basename($s3_key),
  );

  // Prepare to send JSON text to the browser.
  if (ob_get_level()) {
    ob_end_clean();
  }
  drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
  print json_encode($data);
  drupal_exit();
}

/**
 * Custom version of drupal's file_save() function.
 *
 * This function exists because we need to call file_save() before saving the
 * file's s3 metadata to the cache. Which means that the filesize() function
 * will fail. So we skip it in this version, since we already know the size.
 *
 * @param $file
 *   A file object returned by file_load().
 *
 * @return
 *   The updated file object.
 *
 * @see hook_file_insert()
 * @see hook_file_update()
 */
function _s3fs_cors_file_save(stdClass $file) {
  module_invoke_all('file_presave', $file);
  module_invoke_all('entity_presave', $file, 'file');
  drupal_write_record('file_managed', $file);

  // Inform modules about the newly added file.
  module_invoke_all('file_insert', $file);
  module_invoke_all('entity_insert', $file, 'file');

  // Clear the static loading cache.
  entity_get_controller('file')
    ->resetCache(array(
    $file->fid,
  ));
  return $file;
}

Functions

Namesort descending Description
s3fs_cors_element_info Implements hook_element_info().
s3fs_cors_field_widget_form Implements hook_field_widget_form().
s3fs_cors_field_widget_info Implements hook_field_widget_info().
s3fs_cors_field_widget_value The #value_callback for the s3fs_cors field element.
s3fs_cors_help Implements hook_help().
s3fs_cors_menu Implements hook_menu().
s3fs_cors_permission Implements hook_permission().
s3fs_cors_remove_submit Submit callback for the remove button on s3fs_cors elements.
s3fs_cors_sign_request AJAX callback to create paramaters necessary for submitting a CORS request.
s3fs_cors_upload_js
s3fs_cors_upload_process Element process function for s3fs_cors_upload element.
s3fs_cors_upload_submit Submit callback for the upload button on s3fs_cors elements.
s3fs_cors_upload_validate Validation callback for s3fs_cors element type.
s3fs_cors_upload_value Value callback for s3fs_cors_upload element type.
_s3fs_cors_file_save Custom version of drupal's file_save() function.