Pro Forms Direct Upload reports success, then the file is not saved

Submitted by: HHH Consulting Component: Pro Forms (nestable) — File field, Direct Upload to Server
Severity: Low — the form still saves. Leave Direct Upload off (the default) and the file is stored on the record. The loss is the progress state that Direct Upload would have shown.
Type: File handoff / field name


Environment

Bricksforge 4.0.0
Bricks 2.4
WordPress 7.1.1
PHP 8.2.33
ACF PRO 6.8.10
Browser Chromium
Theme Bricks Child Theme 1.1

Observed on a photo add form that is not a Repeater. The same image was submitted twice: once with Direct Upload on, once with it off.


Summary

With Direct Upload to Server on, FilePond says the file has uploaded. The form then saves the post, but the ACF image field (and the featured image, when that is mapped to the same file field) stays empty.

With Direct Upload off, the same image is stored.

The file input is named form-field-{id}[]. A normal submit lets PHP turn that into form-field-{id}, and the save matches {id}. Direct Upload keeps the brackets, so the save looks for {id}[] and skips the file.


Steps to reproduce

  1. Add a Pro Form with a File field. Custom ID e.g. photo-front. Turn Direct Upload to Server on. Do not put the field inside a Repeater.
  2. Map that field to an ACF image field on Create Post (for example uploaded_image = {{photo-front}}).
  3. On the front end, choose an image. Wait until FilePond reports the upload complete. Submit the form.
  4. Open the new post. The ACF image is empty.
  5. Turn Direct Upload off. Submit the same image again.

Expected: both submissions store the image on the ACF field.

Actual: only the submission with Direct Upload off stores the image. FilePond still reported success on the first attempt.


Evidence

Input name

bricksforge/includes/elements/pro-forms/elements/File.php (4.0.0):

$this->set_attribute('field', 'name', 'form-field-' . $id . '[]');

For a field whose id is photo-front, the name attribute is form-field-photo-front[].

What Direct Upload stores

FilePond’s server.process receives that name as fieldName and keeps it:

window.bricksforgeData.temporaryFileUploads.push({
    file: uploadedFile,
    field: fieldName,
});

handle_temporary_file_uploads() in includes/elements/pro-forms/actions/init.php uses $file['field'] as the array key. Nothing removes the trailing [].

process_file_group() then copies that key onto the file record:

'field' => $input_name,

So the saved record is keyed form-field-photo-front[].

What the save compares

includes/elements/pro-forms/actions/base.php, when resolving {{photo-front}}:

// $field is form-field-my_field. Strip the "form-field-" part concretely
$field = substr($file['field'], strpos($file['field'], 'form-field-') + strlen('form-field-'));

if ($field === $id) {

$id is photo-front. After the strip, $field is photo-front[]. The strict compare fails, so handle_file() is never called for that field. The same strip is used in handle_file_field_ids().

get_allowed_file_types_from_settings() also strips only form-field-, so it looks up photo-front[], misses the field, and falls back to the default MIME list. The file is not rejected. It is accepted, then not attached.

Why Direct Upload off works

With the checkbox off, the browser posts the file in the form. PHP’s $_FILES key for an input named form-field-photo-front[] is form-field-photo-front (the brackets are not part of the key). The same compare then yields photo-front, which matches the field id, and the image is written.

Why FilePond still says it uploaded

form_file_upload() writes the file into the Bricksforge temp directory and returns that path. FilePond treats that response as success. No media attachment is created at that step. The attachment is supposed to be created on submit, from the temp file, and that step is the one that skips the field.


Cause

Direct Upload and a normal submit do not use the same field name.

  • Normal submit: form-field-photo-front
  • Direct Upload: form-field-photo-front[]

The save only removes the form-field- prefix. It never removes [], so the temp file is not applied to the field.


Suggested fix

Strip a trailing [] when the temp upload is recorded, and again when the field id is read back, so both paths produce photo-front:

$file_field = preg_replace('/\[\]$/', '', $file['field']);
$field = substr($file['field'], strpos($file['field'], 'form-field-') + strlen('form-field-'));
$field = preg_replace('/\[\]$/', '', $field);

Apply the second strip everywhere the prefix is removed (get_form_field_by_id, handle_file_field_ids, and the other file loop in base.php), and in get_allowed_file_types_from_settings().


What this is not

  • Not a bad image. The same file is stored when Direct Upload is off.
  • Not the known Repeater limit. Direct Upload is already unsupported inside a Repeater. This form is not a Repeater.
  • Not a permissions or ACF return-format problem. The post is created. Only the file mapped from that field is missing.

Happy to test a patch on a staging install.

Hi Pete,

thanks for the detailed write-up. I checked the code paths you listed, and the suggested cause does not hold up on the shipped code, so I want to be upfront about that before we dig further.

You are right that the input is named form-field-{id} and that FilePond hands that name to the temporary upload. But the brackets never reach the server on submit: the form’s submit handler walks the temporary uploads and strips a trailing from the field name before it appends temporaryFileUploads to the request (that has been in bricksforge_elements.js since late 2024 and is unchanged in 4.0.0). So on the server the temp file is keyed form-field-photo-front, exactly like a normal $_FILES upload, and the compare against the field ID in base.php matches. On top of that, the field lookup in get_form_field_by_id already accepts both id and id. So the bracket theory cannot be what loses the file.

So there is something else going on in your setup, and I would like to find it. Could you check two things:

  1. In the browser Network tab, open the form_submit request and look at the temporaryFileUploads value. It should contain “field”:“form-field-photo-front” (no brackets) and a “file” path that points into wp-content/uploads/bricksforge/tmp/. Please paste that JSON (you can mask the domain).

  2. 4.0.0 is stricter about which temporary files it accepts: the path in that JSON has to resolve inside uploads/bricksforge/tmp and the extension has to be one WordPress allows, otherwise the file is silently skipped. So please tell me: is this a multisite, is the uploads folder symlinked or moved by another plugin (custom upload path, offload plugins), and what is the exact file type of the image (jpg, png, heic, …)?

Also useful: does the same field work with Direct Upload on when you map it to something other than the ACF image, for example a plain Update Post Meta or an email attachment? That tells us whether the file is lost before the actions run or only in the ACF handoff.

With that info I can pin it down. Happy to send you a patch to test on staging once we know where it breaks.

Best,
Daniele

Thanks for the correction on the brackets. We checked a live form_submit from the browser. The field name is form-field-photo-front, with no brackets. The file is lost for a different reason, and we have submitted a bug report to the author of the plugin that moves it.

temporaryFileUploads

This is the value from the form_submit request. The domain is masked.

[
  {
    "file": {
      "file": "/home/customer/www/[site]/public_html/wp-content/uploads/2014-06-07-16h20m20.webp",
      "url": "https://[site]/wp-content/uploads/2014-06-07-16h20m20.webp",
      "type": "image/webp",
      "originalFilename": "2014-06-07-16h20m20.JPG",
      "originalFilesize": 2020440
    },
    "field": "form-field-photo-front"
  }
]

"field" is form-field-photo-front. The path does not point into wp-content/uploads/bricksforge/tmp/. It points at a WebP in the uploads root. The file we chose was a JPG (2014-06-07-16h20m20.JPG, 2,020,440 bytes).

Server and file type

  • Not a multisite.
  • wp-content/uploads is not a symlink. realpath matches basedir.
  • upload_path and upload_url_path are empty. No offload plugin is moving the uploads folder.
  • The original file was a JPG. The value that arrived on submit was image/webp. WordPress on this site allows webp, jpg, jpeg, png, gif, heic, and heif, so the extension check is not what skipped it.

Where the path is changed

Direct Upload places the JPG in uploads/bricksforge/tmp/ by filtering upload_dir for the wp_handle_upload call. Auto WebP Converter & Logger 2.0 (author BabaPinnak) is hooked to wp_handle_upload. It converts the JPG to WebP and replaces file, url, and type.

It does not use the directory of the source file. At plugin setup it stores wp_upload_dir() once:

$this->upload_dir = wp_upload_dir();

The handler writes the WebP with that stored path and returns the new location. That is the path in temporaryFileUploads above. WebP Express is also active; it converts in place and does not change the directory.

We have submitted a bug report to that plugin’s author asking them to write the WebP into the same directory as the source file, so a caller that pointed upload_dir at uploads/bricksforge/tmp gets the WebP back inside that folder.

Plain post meta, on a path that stays in tmp

We also ran form_submit with Direct Upload’s payload shape while the file was already inside uploads/bricksforge/tmp/ and the converter did not rewrite the path. The same attachment ID was stored in:

  • the ACF image field (uploaded_image = {{photo-front}})
  • the featured image ({{photo-front}})
  • a plain Update Post Meta field (_bf_direct_upload_probe = {{photo-front}})

So when the path stays inside bricksforge/tmp, the file is not lost before the actions, and it is not lost in the ACF handoff. We did not add an email-attachment action. The plain meta field was the same check.

On the live request above, 4.0.0 skips the file in handle_temporary_file_uploads because realpath is outside BRICKSFORGE_TEMP_DIR. The skip is a continue, with no error. The post still saves, and the image fields stay empty.

What we still see on the Bricksforge side

The temp-directory check on submit is doing what 4.0.0 describes. The gap is earlier. form_file_upload returns success with whatever wp_handle_upload returned, including a path that is already outside uploads/bricksforge/tmp/. FilePond reports the upload complete. On submit the file is then skipped silently.

After wp_handle_upload, if the returned path is outside the temp directory, the direct upload should fail. FilePond would show an error instead of success, and the form would not save a post with an empty image.

Hi Pete,

thank you for going all the way down to the actual cause - that is a very useful trace, and you are right about the part that is still on us.

I checked it and can confirm your conclusion. In form_file_upload the file is moved with wp_handle_upload while our upload_dir filter points at uploads/bricksforge/tmp, but whatever wp_handle_upload returns is passed straight back to FilePond without checking that the path is still inside that folder. Any plugin on wp_handle_upload can relocate the file, FilePond gets a success response, and on submit handle_temporary_file_uploads then drops the entry silently because realpath is outside the temp directory. The post still saves, the field stays empty, and nothing is logged anywhere.

So the gap is exactly where you put it: the direct upload should fail at upload time instead of looking successful and losing the file at submit time. I will add that check, so the upload returns an error, FilePond shows it, and the visitor does not end up with a saved record and a missing file. The silent skip on submit stays as the security backstop, it just should not be the first place where the problem becomes visible.

Thanks also for reporting it to the converter plugin’s author - with the path check in place you would at least have seen the failure immediately.

Best,
Daniele