How to programmatically submit a form that has a file-field using drupal_form_submit()? I'm working to allow the submission of an existing form via REST using the Services module and drupal_form_submit(). Though this is reasonably routine, the form in question takes a file-upload which is a required field.

To avoid embedding extra logic in the form itself to handle files differently under programmatic submission than under manual submission, is there a trick for getting Drupal to accept a file submitted "headlessly" via REST?

Most of this is not relevant to the answer, but for completeness, here's how I'm set up:

function mymodule_my_resource_create($data) {
    $form_state = array();
    $form_state['values'] = $data + form_state_defaults();
    drupal_form_submit('mymodule_form',$form_state);

    $errors = form_get_errors();
    $status = '200 OK';
    if (!empty($errors)) {
        $status = '400 Bad Request - submission caused errors';
    }
    drupal_add_http_header('status',$status);

    return (object)array(
        'message' =&gt; $status,
        'errors' =&gt; $errors,
    );
}

function mymodule_form($form,&amp;$form_state) {
    return array(
        'spreadsheet' =&gt; array(
            '#type' =&gt; 'file',
            '#title' =&gt; 'Spreadsheet file',
            // '#required' =&gt; 1,
            // ^ there have been issues with this since 4.x...
            // https://www.drupal.org/node/59750
            // so this is done manually in the '#validate' function
        ),
        'reason' =&gt; array(
            '#type' =&gt; 'textfield',
            '#title' =&gt; 'Why file is being submitted',
            '#required' =&gt; 1,
        ),
        'submit' =&gt; array(
            '#value' =&gt; 'Submit',
            '#type' =&gt; 'submit',
            '#validate' =&gt; array('mymodule_form_validate_upload'),
        ),
    );
}

function mymodule_form_validate_upload($form,&amp;$form_state) {
    if (!isset($_FILES['files']['name'] || !isset($_FILES['files']['name']['spreadsheet'])) {
        form_set_error('spreadsheet','File must be provided');
    }
}

function mymodule_services_resources() {
    return array(
        'my_resource' =&gt; array(
            'create' =&gt; array(
                'callback' =&gt; 'mymodule_my_resource_create'
                'args' =&gt; array(
                    'name' =&gt; 'post_data',
                    'type' =&gt; 'struct',
                    'description' =&gt; "Should match contents of $form_state['values'] when submitting through the UI.",
                    'source' =&gt; 'data',
                    'optional' =&gt; FALSE,
                ),
            ),
        ),
    );
}

function mymodule_actools_plugin_api($owner,$api) {
    if ($owner=='services' &amp;&amp; $api=='services') {
        return array(
            'version' =&gt; 3,
            'file' =&gt; 'mymodule_services.inc',
            'path' =&gt; drupal_get_path('module','wos2'),
        );
    }
}

function mymodule_default_services_endpoint() {
    $endpoint = new stdClass();
    $endpoint-&gt;disabled = FALSE;
    $endpoint-&gt;api_version = 3;
    $endpoint-&gt;name = 'my_rest';
    $endpoint-&gt;server = 'rest_server';
    $endpoint-&gt;path = 'my_rest';
    $endpoint-&gt;authentication = array();
    $endpoint-&gt;server_settings = array();
    $endpoint-&gt;resources = array(
        'my_resource' =&gt; array(
            'operations' =&gt; array(
                'create' =&gt; array(
                    'enabled' =&gt; '1',
                ),
            ),
        ),
    );
    $endpoint-&gt;debug = 0;
    return array($endpoint);
}
This wasn't as hard as I expected; Drupal has different handling for fields with '#type' =&gt; 'file' as opposed to other types. There are two ways of solving the problem. One is simply adjusting the request-payload, expressed here as a curl command:



curl -i \ 
    'https://example.com/drupal/my_rest/my_resource' \
    -H 'Accept: application/json' \
    -H 'Content-Type: multipart/form-data' \
    -F "files[spreadsheet]=@my_spreadsheet.xlsx" # note the weird field-name
# This automatically uses the POST method, so Drupal reads it as a 'create'


Drupal munges file-field-names by default apparently apparently to match the file.inc code. This is stated expressly in comments:

if (!isset($element['#name'])) {
    $name = array_shift($element['#parents']);
    $element['#name'] = $name;
    if ($element['#type'] == 'file') {
        // To make it easier to handle $_FILES in file.inc, we place all
        // file fields in the 'files' array. Also, we do not support
        // nested file names.
        $element['#name'] = 'files[' . $element['#name'] . ']';
    }


If you want to API-endpoint users from the implementation shown above, the other solution is to explicitly provide a '#name' value for the file-field in question, as should be obvious:

return array(
        'spreadsheet' =&gt; array(
            '#type' =&gt; 'file',
            '#name' =&gt; 'spreadsheet_file', // &lt;-- adding this prevents name-munging
            '#title' =&gt; 'Spreadsheet file',
            // ...


Note of course that the above might have consequences for other sections of code if the file used to be expected under the old name.