Fix photo upload on mobile devices

- Add HEIC/HEIF support for iPhone camera photos
- Make frontend file type validation more permissive for mobile browsers
- Add 'capture' attribute to prefer rear camera on mobile
- Include actual file extension in error messages for debugging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-02-08 12:56:50 -08:00
parent 12a8c054f3
commit 9fc984bea3
3 changed files with 34 additions and 20 deletions

View file

@ -64,14 +64,19 @@ if (bizId LTE 0) {
</cfif> </cfif>
</cfif> </cfif>
<!--- Validate file type ---> <!--- Validate file type (include HEIC for iPhone) --->
<cfset allowedExtensions = "jpg,jpeg,gif,png,webp"> <cfset allowedExtensions = "jpg,jpeg,gif,png,webp,heic,heif">
<cfif NOT listFindNoCase(allowedExtensions, actualExt)> <cfif NOT listFindNoCase(allowedExtensions, actualExt)>
<cffile action="DELETE" file="#headersDir#/#uploadResult.ServerFile#"> <cffile action="DELETE" file="#headersDir#/#uploadResult.ServerFile#">
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp)" })#</cfoutput> <cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp, heic)" })#</cfoutput>
<cfabort> <cfabort>
</cfif> </cfif>
<!--- Convert HEIC/HEIF extension to jpg for consistency --->
<cfif actualExt EQ "heic" OR actualExt EQ "heif">
<cfset actualExt = "jpg">
</cfif>
<!--- No resize - accept image as-is ---> <!--- No resize - accept image as-is --->
<!--- Delete old header if exists ---> <!--- Delete old header if exists --->

View file

@ -26,17 +26,23 @@ if (itemId LTE 0) {
<!--- Upload the file to temp location first ---> <!--- Upload the file to temp location first --->
<cffile action="UPLOAD" filefield="photo" destination="#itemsDir#/" nameconflict="MAKEUNIQUE" mode="755" result="uploadResult"> <cffile action="UPLOAD" filefield="photo" destination="#itemsDir#/" nameconflict="MAKEUNIQUE" mode="755" result="uploadResult">
<!--- Validate file type ---> <!--- Validate file type (include HEIC/HEIF for iPhone) --->
<cfset allowedExtensions = "jpg,jpeg,gif,png,webp"> <cfset allowedExtensions = "jpg,jpeg,gif,png,webp,heic,heif">
<cfif NOT listFindNoCase(allowedExtensions, uploadResult.ClientFileExt)> <cfset actualExt = lCase(uploadResult.ClientFileExt)>
<cfif NOT listFindNoCase(allowedExtensions, actualExt)>
<cffile action="DELETE" file="#itemsDir#/#uploadResult.ServerFile#"> <cffile action="DELETE" file="#itemsDir#/#uploadResult.ServerFile#">
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp)" })#</cfoutput> <cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp, heic). Got: #actualExt#" })#</cfoutput>
<cfabort> <cfabort>
</cfif> </cfif>
<!--- Convert HEIC/HEIF to jpg for broader compatibility --->
<cfif actualExt EQ "heic" OR actualExt EQ "heif">
<cfset actualExt = "jpg">
</cfif>
<!--- Delete old photo if exists (any extension) ---> <!--- Delete old photo if exists (any extension) --->
<cfscript> <cfscript>
for (ext in listToArray(allowedExtensions)) { for (ext in listToArray("jpg,jpeg,gif,png,webp,heic,heif")) {
oldFile = "#itemsDir#/#itemId#.#ext#"; oldFile = "#itemsDir#/#itemId#.#ext#";
if (fileExists(oldFile)) { if (fileExists(oldFile)) {
try { fileDelete(oldFile); } catch (any e) {} try { fileDelete(oldFile); } catch (any e) {}
@ -44,15 +50,15 @@ for (ext in listToArray(allowedExtensions)) {
} }
</cfscript> </cfscript>
<!--- Rename to ItemID.ext ---> <!--- Rename to ItemID.ext (use actualExt which may be converted from heic) --->
<cffile action="RENAME" source="#itemsDir#/#uploadResult.ServerFile#" destination="#itemsDir#/#itemId#.#uploadResult.ClientFileExt#" mode="755"> <cffile action="RENAME" source="#itemsDir#/#uploadResult.ServerFile#" destination="#itemsDir#/#itemId#.#actualExt#" mode="755">
<!--- Return success with image URL ---> <!--- Return success with image URL --->
<cfoutput>#serializeJSON({ <cfoutput>#serializeJSON({
"OK": true, "OK": true,
"ERROR": "", "ERROR": "",
"MESSAGE": "Photo uploaded successfully", "MESSAGE": "Photo uploaded successfully",
"IMAGEURL": "/uploads/items/#itemId#.#uploadResult.ClientFileExt#" "IMAGEURL": "/uploads/items/#itemId#.#actualExt#"
})#</cfoutput> })#</cfoutput>
<cfcatch type="any"> <cfcatch type="any">

View file

@ -2819,14 +2819,17 @@
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.accept = 'image/jpeg,image/png,image/gif,image/webp'; input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif,.heic,.heif';
input.capture = 'environment'; // Prefer rear camera on mobile
input.onchange = async (e) => { input.onchange = async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; // Be permissive with file types - server will validate
if (!validTypes.includes(file.type)) { // Mobile cameras may report inconsistent MIME types
this.toast('Please select a valid image (JPG, PNG, GIF, or WebP)', 'error'); const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif', ''];
if (file.type && !validTypes.includes(file.type) && !file.type.startsWith('image/')) {
this.toast('Please select a valid image file', 'error');
return; return;
} }
if (file.size > 5 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) {
@ -3115,15 +3118,15 @@
uploadHeader() { uploadHeader() {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.accept = 'image/jpeg,image/png,image/gif,image/webp'; input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif,.heic,.heif';
input.onchange = async (e) => { input.onchange = async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
// Validate file type // Validate file type (be permissive for mobile cameras)
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif', ''];
if (!validTypes.includes(file.type)) { if (file.type && !validTypes.includes(file.type) && !file.type.startsWith('image/')) {
this.toast('Please select a valid image (JPG, PNG, GIF, or WebP)', 'error'); this.toast('Please select a valid image file', 'error');
return; return;
} }