0

I have a form with two '#type' => 'managed_file' fields.

  1. Anonymous user selects a file on field 1, clicks the Upload. Ajax upload occurs and the browse widget is replaced by the filename uploaded and the Remove button, as expected.

  2. user does same on field 2, which works same way. Form now displays with two uploaded files, as expected.

  3. user clicks main (non ajax) submit button. Form complains that file field 1 is missing

Other observations

  1. If I disable ajax, I notice that on clicking Upload on one managed file field, the other one loses its file!

  2. If I don't use the managed file upload button at all, and supply one file then press submit, the file shows as if uploaded, shows validation error about file 2 (as expected). However, if I immediately press submit again I would expect the same state to be preserved, but instead I see now that both file fields are again blank.

Is this bug/limitation in Drupal (7) or am I doing something stupid?

Here's my code:

function mymod_formtest($form, &$form_state) {                                                                                                                                                                   
  $form['f1'] = [                                                                                                                                                                                                  
    '#type' => 'managed_file',                                                                                                                                                                                     
    '#required' => TRUE,                                                                                                                                                                                           
    '#upload_validators' => [                                                                                                                                                                                      
      'file_validate_extensions' => ['doc docx odt pdf'],                                                                                                                                                          
    ],                                                                                                                                                                                                             
    '#upload_location' => 'private://test/',                                                                                                                                                                       
    '#title' => t('Upload 1'),                                                                                                                                                                                     
    '#default_value' => isset($form_state['values']['f1']) ? $form_state['values']['f1'] : '',                                                                                                                     
  ];                                                                                                                                                                                                               

  $form['f2'] = [                                                                                                                                                                                                  
    '#type' => 'managed_file',                                                                                                                                                                                     
    '#required' => TRUE,                                                                                                                                                                                           
    '#upload_validators' => [                                                                                                                                                                                      
      'file_validate_extensions' => ['doc docx odt pdf'],                                                                                                                                                          
    ],                                                                                                                                                                                                             
    '#upload_location' => 'private://test/',                                                                                                                                                                       
    '#default_value' => isset($form_state['values']['f2']) ? $form_state['values']['f2'] : '',                                                                                                                     
    '#title' => t('Upload 2'),                                                                                                                                                                                     
  ];                                                                                                                                                                                                               
  $form['submit'] = [                                                                                                                                                                                              
    '#type' => 'submit',                                                                                                                                                                                           
    '#value' => 'go',                                                                                                                                                                                              
    ];                                                                                                                                                                                                             
  return $form;                                                                                                                                                                                                    
}                                     
1

The #value_callback for a managed_file FAPI element is set to file_managed_file_value().

This function is responsible for setting up the element's value in one of two possible ways.

  1. If a file has been sent, then store this file in the file_managed table as a temporary file. Put this fid into the form element's value.

  2. If instead a fid value has been sent then determine whether that is valid, and if so put that in the form element's value.

    • The fid is invalid if it belongs to a temporary file.
    • The fid is invalid if it belongs to a private:// file that the current user does not have access to, according to hook_file_download

What's happening above in the OP's question is:

  1. 1sts File is sent. Gets stored. Form returns (because the 2nd file has not been provided) but for file 1 it shows the file uploaded and the file's new fid is added as the element's value.

  2. The user now submits the 2nd file. Processing starts with the fist file field. No file has been sent for this, but instead we have the fid value. This file object is loaded and inspected. But this file is temporary so it's assumed that the POST-ed fid is invalid. Hence it shows that there is no input for the first file.

Solution

So first of all you need the file to be made permanent. And then you need Drupal to realise that this particular anonymous user should be allowed to access this particular file (but not all files uploaded this way).

I did this by using an #element_validate property on the managed file fields which points to a function like this:

function anon_private_upload_remember($element, &$form_state, $form) {

  if (isset($element['#value']['fid'])) {
    $fid = (int) $element['#value']['fid'];
    if ($file = file_load($fid)) {
      if ($file->status != FILE_STATUS_PERMANENT) {
        // Looks like this file has just been uploaded.
        // Change status to permanent.
        $file->status = FILE_STATUS_PERMANENT;
        // Save.
        file_save($file);
        // Record that the module (in this example, user module) is using the file.
        file_usage_add($file, 'user', 'user', 0);

        // Now remember that this user is allowed this file.
        if (!isset($_SESSION['anon_user_owns_fids'])) {
          $_SESSION['anon_user_owns_fids'] = [];
        }
        $_SESSION['anon_user_owns_fids'][$file->uri] = $file->fid;
      }
      // Fine, the file is valid, so the field is valid.
      return;
    }
  }
}

WARNING: as well as the above #element_validation you also NEED to add file_managed_file_validate after the above one. So your form element's validation definition might look like:

$form['some_element'] =
  // ...    
  '#element_validate' => ['anon_private_upload_remember', 'file_managed_file_validate'],
  // ...

That function (which is normally added via the file module's element info function) is responsible for simplifying the element's value into a simple fid instead of a nested array.

This will make files uploaded permanent, and stores their uris in a $_SESSION variable. Then implement hook_file_download like:

/**
 * Implements hook_file_download.
 *
 * This is the hook used to control access to private:// files.
 * Annoyingly it's supposed to return headers.
 */
function myrmod_file_download($uri) {
  if (isset($_SESSION['anon_user_owns_fids'][$uri])) {
    // We are supposed to return headers, but for our purposes this is fine.
    // @todo - this won't work if you try to download the file, but
    // is fine for just checking access.
    return ['foo' => 'bar'];
  }
  // Don't know - pass on to other modules.
  return NULL;
}

For my purposes I then need to do some extra cleanup - e.g. set the file back to temporary and remove the usage record on final form submission, since I don't need the file after that.

Phew. That was difficult.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.