Skip to content

fix: validate file type before previewing avatar upload#370

Open
thisismyurl wants to merge 4 commits into
10up:developfrom
thisismyurl:fix/avatar-upload-file-type-validation
Open

fix: validate file type before previewing avatar upload#370
thisismyurl wants to merge 4 commits into
10up:developfrom
thisismyurl:fix/avatar-upload-file-type-validation

Conversation

@thisismyurl

@thisismyurl thisismyurl commented Jun 9, 2026

Copy link
Copy Markdown

Summary

The local-file avatar input accepted any file type — a user could select a PDF, video, or binary file and URL.createObjectURL() would attempt to render it as a preview image. Two changes close this gap:

  • PHP (class-simple-local-avatars.php): Add accept="image/*" to the <input type="file">. This tells the OS file picker to default to image files and filters non-image files in supporting browsers.
  • JS (assets/js/simple-local-avatars.js): Add a MIME type guard in the change handler. If the selected file's reported type does not start with image/, the input is cleared and the existing avatar preview is restored. The guard is skipped for browsers that don't populate file.type (older IE) so behaviour is unchanged in those environments.

Changes

includes/class-simple-local-avatars.php

// Before
<input type="file" name="simple-local-avatar" id="simple-local-avatar" class="standard-text" />

// After
<input type="file" name="simple-local-avatar" id="simple-local-avatar" class="standard-text" accept="image/*" />

assets/js/simple-local-avatars.js

// Added inside the change handler, before createObjectURL:
const file = event.target.files[0];
if (file.type && 0 !== file.type.indexOf('image/')) {
    avatar_input.val('');
    avatar_preview.attr('src', current_avatar);
    return;
}

Testing

  1. Go to a user profile page with the Simple Local Avatars field visible.
  2. Attempt to upload a non-image file (e.g. a .pdf or .txt).
  3. Confirm the file picker defaults to images and, if a non-image is selected, the avatar preview does not change.
  4. Confirm uploading a valid image still works correctly.

Changelog

  • Add accept="image/*" attribute to avatar file input for better OS-level file filtering
  • Add client-side MIME type validation to reject non-image files before preview attempt

Credits

@thisismyurl

Fixes #233

Add accept="image/*" to the file input so the OS file picker filters
to image files by default. Add a client-side MIME type check in the
change handler so non-image files selected via drag-and-drop or by
clearing the type filter are silently rejected and the existing avatar
preview is preserved.

Fixes 10up#233
Copilot AI review requested due to automatic review settings June 9, 2026 13:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds client-side guardrails to reduce non-image avatar uploads and improve the file chooser UX.

Changes:

  • Restricts the profile avatar file input to images via accept="image/*".
  • Adds a JS MIME-type check to reset the preview if the selected file is not an image.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
includes/class-simple-local-avatars.php Limits the avatar <input type="file"> chooser to image types.
assets/js/simple-local-avatars.js Prevents non-image selections from updating the avatar preview.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread assets/js/simple-local-avatars.js Outdated
Comment thread assets/js/simple-local-avatars.js Outdated
@thrijith
thrijith self-requested a review June 26, 2026 08:47
Comment thread assets/js/simple-local-avatars.js Outdated
Comment thread assets/js/simple-local-avatars.js Outdated
Comment thread includes/class-simple-local-avatars.php Outdated
Addresses @thrijith's review on 10up#370:

- Replace the `file.type &&` short-circuit with an allowlist check, so an
  empty or unknown `file.type` (e.g. a renamed binary) is rejected instead
  of passed to URL.createObjectURL().
- Mirror the server's accepted set (jpeg/gif/png, from the `mimes` array on
  media_handle_upload()) in both the JS guard and the input `accept`
  attribute, so the client never previews a type the server rejects on
  submit (svg/webp previously slipped through `image/*`).
- Surface a visible inline rejection notice and announce it to assistive
  technology via wp.a11y.speak() (add `wp-a11y` as a script dependency).
- Add Cypress coverage for the non-image and empty-type rejection paths
  plus the valid-image happy path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thisismyurl

Copy link
Copy Markdown
Author

Thanks @thrijith — all three addressed in the latest push.

1. Empty / unknown MIME no longer slips through. The old if (file.type && 0 !== file.type.indexOf('image/')) short-circuited when file.type was '' (the renamed-binary case you flagged), so createObjectURL ran on an unknown file. The guard is now an allowlist check:

const avatar_allowed_types = ['image/jpeg', 'image/gif', 'image/png'];
// indexOf('') === -1, so empty/unknown types are rejected, not previewed.
if (-1 === avatar_allowed_types.indexOf(file.type)) { ... }

I went with an explicit allowlist rather than your suggested !file.type || !file.type.startsWith('image/') for one reason: startsWith('image/') would still admit image/svg+xml and image/webp, which is exactly the preview-then-fail gap in point 3. The allowlist closes that in the same check.

2. Client now mirrors the server's accepted set. The server only accepts jpeg/gif/png (the mimes array on media_handle_upload() in edit_user_profile_update()), so the JS uses the same three, and I tightened the input to accept="image/jpeg,image/png,image/gif" so the native picker filters too. svg/webp no longer preview only to fail on submit.

3. Rejections are surfaced, not silent. On reject the input now shows a visible inline notice (#simple-local-avatar-error) and announces via wp.a11y.speak( message, 'assertive' ). I added wp-a11y as a script dependency and feature-guard the call (window.wp && wp.a11y && typeof wp.a11y.speak === 'function') so it degrades quietly if the dependency is ever absent. The assertive politeness level is intentional — a rejected upload is a now-relevant correction, not a passive status.

Added tests/cypress/integration/avatar-file-type.test.js covering the text-file reject, the empty-MIME reject, and the valid-image happy path.

One note on verification: I ran php -l and a JS syntax check locally, but I wasn't able to spin up wp-env/Cypress or npm run lint:js in my environment, so the new e2e specs and lint will get their first real run in CI on this push — I'll keep an eye on the checks and follow up here if anything needs a touch-up.

(full disclosure: AI helped me identify the issue and verify my work)

@thrijith

Copy link
Copy Markdown
Member

Hi @thisismyurl

Thanks a lot for the updates, code looks good to me.

Added tests/cypress/integration/avatar-file-type.test.js covering the text-file reject, the empty-MIME reject, and the valid-image happy path.

The tests seem to be failing because it is logging in as admin who won't see the file input control, can you set up a user with subscriber role in tests see if that helps?

@jeffpaul jeffpaul added this to the 2.9.0 milestone Jul 6, 2026
@thisismyurl

Copy link
Copy Markdown
Author

Done, thanks for the review

thisismyurl and others added 2 commits July 13, 2026 10:27
…t renders

Admins get the media-library uploader; #simple-local-avatar only renders
for users without the upload_files capability. Creates the subscriber in
the wp-env initialize step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deprecated cypress-file-upload attachFile does not reliably trigger
the input change handler on CI (Electron 118), so the MIME guard never
runs and the reject cases leave the input value populated. Switch to the
native cy.selectFile, which dispatches real input/change events. The
valid-image case uses an inline PNG buffer to avoid project-root path
resolution ambiguity with --config-file.
@thrijith

Copy link
Copy Markdown
Member

Passing to @peterwilsoncc for final review the last failing test is unrelated to the changes done here, please let me know if that should handled in this PR, thanks!

@thrijith
thrijith requested a review from peterwilsoncc July 16, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing Check for File Type Before Uploading Media for Avatar

4 participants