Years later… this is actually a bit of a tricky one. We can do something like this.
commit ea5927e3ff15616fc7df91ddcd361e6379a12582
Author: Sam Saffron <sam.saffron@gmail.com>
Date: Thu Oct 30 13:51:02 2025 +1100
FEATURE: filter uploads correctly in uppy uploader and image / avatar uploader
diff --git a/frontend/discourse/admin/components/images-uploader.gjs b/frontend/discourse/admin/components/images-uploader.gjs
index 18a857d581..978edaac30 100644
--- a/frontend/discourse/admin/components/images-uploader.gjs
+++ b/frontend/discourse/admin/components/images-uploader.gjs
@@ -2,13 +2,18 @@
import Component from "@ember/component";
import { getOwner } from "@ember/owner";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
+import { service } from "@ember/service";
import { tagName } from "@ember-decorators/component";
import icon from "discourse/helpers/d-icon";
+import { acceptedImageFormats } from "discourse/lib/uploads";
import UppyUpload from "discourse/lib/uppy/uppy-upload";
import { i18n } from "discourse-i18n";
@tagName("span")
export default class ImagesUploader extends Component {
+ @service currentUser;
+ @service siteSettings;
+
uppyUpload = new UppyUpload(getOwner(this), {
id: "images-uploader",
type: "avatar",
@@ -28,6 +33,10 @@ export default class ImagesUploader extends Component {
return this.uploadingOrProcessing ? i18n("uploading") : i18n("upload");
}
+ get acceptedFormats() {
+ return acceptedImageFormats(this.currentUser?.staff, this.siteSettings);
+ }
+
<template>
<label
class="btn"
@@ -40,7 +49,7 @@ export default class ImagesUploader extends Component {
class="hidden-upload-field"
disabled={{this.uppyUpload.uploading}}
type="file"
- accept="image/*"
+ accept={{this.acceptedFormats}}
multiple
/>
</label>
diff --git a/frontend/discourse/app/components/avatar-uploader.gjs b/frontend/discourse/app/components/avatar-uploader.gjs
index d3bf8fc341..0f77c9e06a 100644
--- a/frontend/discourse/app/components/avatar-uploader.gjs
+++ b/frontend/discourse/app/components/avatar-uploader.gjs
@@ -3,15 +3,20 @@ import Component from "@ember/component";
import { action } from "@ember/object";
import { getOwner } from "@ember/owner";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
+import { service } from "@ember/service";
import { isBlank } from "@ember/utils";
import { tagName } from "@ember-decorators/component";
import DButton from "discourse/components/d-button";
import discourseComputed from "discourse/lib/decorators";
+import { acceptedImageFormats } from "discourse/lib/uploads";
import UppyUpload from "discourse/lib/uppy/uppy-upload";
import { i18n } from "discourse-i18n";
@tagName("span")
export default class AvatarUploader extends Component {
+ @service currentUser;
+ @service siteSettings;
+
uppyUpload = new UppyUpload(getOwner(this), {
id: "avatar-uploader",
type: "avatar",
@@ -34,6 +39,11 @@ export default class AvatarUploader extends Component {
imageIsNotASquare = false;
+ @discourseComputed()
+ acceptedFormats() {
+ return acceptedImageFormats(this.currentUser?.staff, this.siteSettings);
+ }
+
@discourseComputed("uppyUpload.uploading", "uploadedAvatarId")
customAvatarUploaded() {
return !this.uppyUpload.uploading && !isBlank(this.uploadedAvatarId);
@@ -58,7 +68,7 @@ export default class AvatarUploader extends Component {
class="hidden-upload-field"
disabled={{this.uploading}}
type="file"
- accept="image/*"
+ accept={{this.acceptedFormats}}
aria-hidden="true"
/>
<DButton
diff --git a/frontend/discourse/app/components/uppy-image-uploader.gjs b/frontend/discourse/app/components/uppy-image-uploader.gjs
index 6a94ae6cb8..28bf264e8a 100644
--- a/frontend/discourse/app/components/uppy-image-uploader.gjs
+++ b/frontend/discourse/app/components/uppy-image-uploader.gjs
@@ -15,7 +15,12 @@ import concatClass from "discourse/helpers/concat-class";
import icon from "discourse/helpers/d-icon";
import { getURLWithCDN } from "discourse/lib/get-url";
import lightbox from "discourse/lib/lightbox";
-import { authorizesOneOrMoreExtensions, isVideo } from "discourse/lib/uploads";
+import {
+ acceptedImageFormats,
+ acceptedVideoFormats,
+ authorizesOneOrMoreExtensions,
+ isVideo,
+} from "discourse/lib/uploads";
import UppyUpload from "discourse/lib/uppy/uppy-upload";
import { i18n } from "discourse-i18n";
@@ -138,7 +143,20 @@ export default class UppyImageUploader extends Component {
}
get acceptedFormats() {
- return this.args.allowVideo ? "image/*,video/*" : "image/*";
+ const imageFormats = acceptedImageFormats(
+ this.currentUser?.staff,
+ this.siteSettings
+ );
+
+ if (this.args.allowVideo) {
+ const videoFormats = acceptedVideoFormats(
+ this.currentUser?.staff,
+ this.siteSettings
+ );
+ return videoFormats ? `${imageFormats},${videoFormats}` : imageFormats;
+ }
+
+ return imageFormats;
}
get isVideoFile() {
diff --git a/frontend/discourse/app/lib/uploads.js b/frontend/discourse/app/lib/uploads.js
index a49005f6a8..da38f2abf4 100644
--- a/frontend/discourse/app/lib/uploads.js
+++ b/frontend/discourse/app/lib/uploads.js
@@ -178,6 +178,17 @@ function imagesExtensions(staff, siteSettings) {
return exts;
}
+function videosExtensions(staff, siteSettings) {
+ let exts = extensions(siteSettings).filter((ext) => isVideo(`.${ext}`));
+ if (staff) {
+ const staffExts = staffExtensions(siteSettings).filter((ext) =>
+ isVideo(`.${ext}`)
+ );
+ exts = exts.concat(staffExts);
+ }
+ return exts;
+}
+
function isAuthorizedFile(fileName, staff, siteSettings) {
if (
staff &&
@@ -215,6 +226,16 @@ function authorizedImagesExtensions(staff, siteSettings) {
: imagesExtensions(staff, siteSettings).join(", ");
}
+export function acceptedImageFormats(staff, siteSettings) {
+ const exts = imagesExtensions(staff, siteSettings);
+ return exts.map((ext) => `.${ext}`).join(",");
+}
+
+export function acceptedVideoFormats(staff, siteSettings) {
+ const exts = videosExtensions(staff, siteSettings);
+ return exts.map((ext) => `.${ext}`).join(",");
+}
+
export function authorizesAllExtensions(staff, siteSettings) {
return (
siteSettings.authorized_extensions.includes("*") ||
One slight downside is that our client side image compression can also sometimes handle format conversion. So potentially this would block some image types we don’t want to block.
I am mixed on if we want to fix this or not, but do see this as having high impact if you configured your site (for example) only to allow png.
cc @martin