You are here

public static function S3fsCorsFile::processManagedFile in S3 File System CORS Upload 8

Handle a managed file from a form upload field.

Overrides ManagedFile::processManagedFile

File

src/Element/S3fsCorsFile.php, line 150

Class

S3fsCorsFile
Provides an S3fs Cors File Element.

Namespace

Drupal\s3fs_cors\Element

Code

public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) {

  // Get ManagedFile Process Form Element and Alter it.
  $element = parent::processManagedFile($element, $form_state, $complete_form);

  // Alter Upload - Input File element.
  $element['upload']['#attributes'] = [
    'class' => [
      's3fs-cors-upload',
    ],
  ];
  $config = \Drupal::config('s3fs.settings');
  $cors_config = \Drupal::config('s3fs_cors.settings');

  // Create Configurations needed for AWS S3 CORS Upload.
  $acl = $cors_config
    ->get('s3fs_access_type');
  $bucket = $config
    ->get('bucket');
  $upload_parts = explode('://', $element['#upload_location']);
  $s3_key = $upload_parts[1];

  // If a base folder for public or private uri schemes has been defined,
  // prepend it to the $s3 key, else use the same defaults as the s3fs module.
  if (method_exists('\\Drupal\\Core\\StreamWrapper\\StreamWrapperManager', 'getScheme')) {

    // Drupal 8.8+, inc. Drupal 9.
    $uri_scheme = StreamWrapperManager::getScheme($element['#upload_location']);
  }
  else {

    // Drupal < 8.8.
    $uri_scheme = \Drupal::service('file_system')
      ->uriScheme($element['#upload_location']);
  }
  if ($uri_scheme == 'public' || $uri_scheme == 'private') {
    $config_key = $uri_scheme . '_folder';
    $folder_key = empty($config
      ->get($config_key)) ? 's3fs-' . $uri_scheme : $config
      ->get($config_key);
    $s3_key = $folder_key . '/' . $s3_key;
  }

  // If a root folder has been set, prepend it to the $s3_key at this time.
  if (!empty($config
    ->get('root_folder'))) {
    $s3_key = $config
      ->get('root_folder') . '/' . $s3_key;
  }

  // Drop the "s3://" stream prefix as it is misleading.
  $element['#upload_location'] = $bucket . '::' . $s3_key;
  $datenow = new DrupalDateTime('now');
  $datenow
    ->setTimezone(new \DateTimeZone('UTC'));
  $expiration = clone $datenow;
  $expiration
    ->add(new \DateInterval('PT6H'));
  $region = $config
    ->get('region') ?: Settings::get('s3fs.region', '');

  // Use the memoized default credential provider.
  $provider = CredentialProvider::defaultProvider();

  // Chain a provider with the local values if they exist.
  $credentials = NULL;
  $access_key = $config
    ->get('access_key') ?: Settings::get('s3fs.access_key', '');
  $secret_key = $config
    ->get('secret_key') ?: Settings::get('s3fs.secret_key', '');
  if ($access_key && $secret_key) {
    $credentials = new Credentials($access_key, $secret_key);
    $provider = CredentialProvider::chain(CredentialProvider::fromCredentials($credentials), $provider);
  }

  // Create an S3 client using the provider. This should use the Instance
  // Profile provider if this code is running in an AWS instance.

  /** @var \Drupal\s3fs\S3fsServiceInterface $s3fs */
  $s3fs = \Drupal::service('s3fs');
  $client = $s3fs
    ->getAmazonS3Client($config
    ->get());
  $creds = $client
    ->getCredentials()
    ->wait();
  $access_key = $creds
    ->getAccessKeyId();
  $secret_key = $creds
    ->getSecretKey();
  $session_token = $creds
    ->getSecurityToken();

  // If not running on an AWS instance the S3 Client doesn't have session
  // token, so create an STS client and use the session token from that.
  if (empty($credentials) && empty($session_token)) {
    $sts_policy_resource = $cors_config
      ->get('s3fs_sts_policy_resource') ?: '';
    $sts = new StsClient([
      'region' => $region,
      'version' => 'latest',
    ]);
    $sessionToken = $sts
      ->getFederationToken([
      'Name' => 'User1',
      'DurationSeconds' => '3600',
      'Policy' => json_encode([
        'Statement' => [
          'Sid' => 'drupals3fscorsid' . time(),
          'Action' => [
            "s3:PutObject",
            "s3:GetObjectAcl",
            "s3:GetObject",
            "s3:DeleteObjectVersion",
            "s3:PutObjectVersionAcl",
            "s3:GetObjectVersionAcl",
            "s3:DeleteObject",
            "s3:PutObjectAcl",
            "s3:GetObjectVersion",
          ],
          'Effect' => 'Allow',
          'Resource' => $sts_policy_resource,
        ],
      ]),
    ]);
    $access_key = $sessionToken['Credentials']['AccessKeyId'];
    $secret_key = $sessionToken['Credentials']['SecretAccessKey'];
    $session_token = $sessionToken['Credentials']['SessionToken'];
  }

  // Specify the S3 upload policy.
  $policy = [
    'expiration' => $expiration
      ->format('Y-m-d\\TH:i:s\\Z'),
    'conditions' => [
      [
        'bucket' => $bucket,
      ],
      [
        'acl' => $acl,
      ],
      [
        'starts-with',
        '$key',
        $s3_key,
      ],
      [
        'starts-with',
        '$Content-Type',
        '',
      ],
      [
        'success_action_status' => '201',
      ],
      [
        'x-amz-algorithm' => 'AWS4-HMAC-SHA256',
      ],
      [
        'x-amz-credential' => $access_key . '/' . $datenow
          ->format('Ymd') . '/' . $region . '/s3/aws4_request',
      ],
      [
        'x-amz-date' => $datenow
          ->format('Ymd\\THis\\Z'),
      ],
      [
        'x-amz-expires' => '21600',
      ],
    ],
  ];

  // Include the session token if it exists.
  if ($session_token) {
    $policy['conditions'][] = [
      'x-amz-security-token' => $session_token,
    ];
  }

  // Generate a string to sign from the policy.
  $base64Policy = base64_encode(json_encode($policy));

  // Generate the v4 signing key.
  $date_key = hash_hmac('sha256', $datenow
    ->format('Ymd'), 'AWS4' . $secret_key, TRUE);
  $region_key = hash_hmac('sha256', $region, $date_key, TRUE);
  $service_key = hash_hmac('sha256', 's3', $region_key, TRUE);
  $signing_key = hash_hmac('sha256', 'aws4_request', $service_key, TRUE);
  $signature = hash_hmac('sha256', $base64Policy, $signing_key);
  $js_settings = [];

  // Add the extension list to the page as JavaScript settings.
  if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
    $js_settings['extension_list'] = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0])));
  }
  if (isset($element['#max_filesize'])) {
    $max_filesize = Bytes::toInt($element['#max_filesize']);
  }
  elseif (isset($element['#upload_validators']['file_validate_size'])) {
    $max_filesize = $element['#upload_validators']['file_validate_size'][0];
  }
  else {
    $max_filesize = Environment::getUploadMaxSize();
  }
  $js_settings['max_size'] = $max_filesize;
  $js_settings['upload_location'] = $element['#upload_location'];
  $js_settings['cors_form_data'] = [
    'acl' => $acl,
    'success_action_status' => 201,
    'x-amz-algorithm' => 'AWS4-HMAC-SHA256',
    'x-amz-credential' => $access_key . '/' . $datenow
      ->format('Ymd') . '/' . $region . '/s3/aws4_request',
    'x-amz-date' => $datenow
      ->format('Ymd\\THis\\Z'),
    'policy' => $base64Policy,
    'x-amz-signature' => $signature,
    'x-amz-expires' => '21600',
  ];

  // Include the session token if it exists.
  if ($session_token) {
    $js_settings['cors_form_data']['x-amz-security-token'] = $session_token;
  }
  $element_parents = $element['#array_parents'];

  // Remove the delta value from element parents if multiple files allowed.
  if ($element['#multiple']) {
    array_pop($element_parents);
  }

  // Pass the element parents through to the javascript function.
  $js_settings['element_parents'] = implode('/', $element_parents);

  // Use s3fs settings for constructing the form action.
  $hostname = $config
    ->get('use_customhost') ? $config
    ->get('hostname') : 's3.' . $region . '.amazonaws.com';
  $endpoint = $config
    ->get('use_path_style_endpoint') ? $hostname . '/' . $bucket : $bucket . '.' . $hostname;
  $js_settings['cors_form_action'] = $cors_config
    ->get('s3fs_https') . '://' . $endpoint . '/';
  $field_name = $element['#field_name'];
  if (!empty($element['#field_parents'])) {
    $field_name = sprintf('%s_%s', implode('_', $element['#field_parents']), $field_name);
  }
  $element['upload']['#attached']['drupalSettings']['s3fs_cors'][$field_name] = $js_settings;
  $item = $element['#value'];
  $item['fids'] = $element['fids']['#value'];

  // Add the description field if enabled.
  if ($element['#description_field'] && $item['fids']) {
    $config = \Drupal::config('file.settings');
    $element['description'] = [
      '#type' => $config
        ->get('description.type'),
      '#title' => t('Description'),
      '#value' => isset($item['description']) ? $item['description'] : '',
      '#maxlength' => $config
        ->get('description.length'),
      '#description' => t('The description may be used as the label of the link to the file.'),
    ];
  }
  return $element;
}