Public Access
feat(freelancer): validate and downscale photo uploads on both forms
Both the application form (/odoo-community-developers/) and the self-service edit form (/freelancer-edit/) now use one shared module, static/photo-upload.js (window.O4PPhoto), so the limits live in one place: - validate() decides on the File object BEFORE any read: type allowlist (image/jpeg, image/png, image/webp) and an 8 MiB cap. A refused file is read zero bytes and is never submitted. - downscale() reads, centre-crops square, caps the longest side at 384 px, encodes JPEG at 0.82 and steps the quality down until the result is <= 80 KiB. Re-encoding also strips EXIF (a phone photo's GPS does not reach the page) and refuses a spoofed type that fails to decode. - The edit form's photo field becomes a real labelled input that keeps the current photo when untouched; the whole-record save carries it back byte-identical. Existing stored photos are untouched, so the directory page is not lighter until profiles re-upload. accept= is a hint, not validation.
This commit is contained in:
1 parent
5cf58ffa8a
commit
8319a74f99
3 files changed
+223
-27
No files matched your search
@@ -148,6 +148,7 @@
|
||||
|
||||
<!-- The 15-point rule, in ONE copy for both forms (this one and the application form). -->
|
||||
<script src="/static/focus-rule.js"></script>
|
||||
<script src="/static/photo-upload.js"></script>
|
||||
|
||||
<!-- 1) Read the credential, then take it OUT of the address bar.
|
||||
The hash arrives as ?has=... or ?hash=... (both spellings accepted, 'has' first — the
|
||||
@@ -570,22 +571,53 @@
|
||||
form.appendChild(linkField("edFiverr", "fiverr", "Fiverr URL",
|
||||
readField(record, "fiverr"), "https://www.fiverr.com/…"));
|
||||
|
||||
// The image is READ-ONLY on purpose: replacing a photo is a different problem from
|
||||
// editing text (an upload path, a base64 blob, a size limit). Shown, not editable.
|
||||
// The photo is editable here with the SAME rules as the application form
|
||||
// (static/photo-upload.js). A picked file is validated and downscaled on the spot;
|
||||
// leaving it untouched keeps the record's own Image, which rides back byte-identical
|
||||
// because the save sends the whole record (see the submit handler below).
|
||||
var newImage = null;
|
||||
var photoWrap = el("div", "field");
|
||||
photoWrap.appendChild(el("p", "", "Photo (not editable here)"));
|
||||
var photoLabel = el("label", "", "Photo");
|
||||
photoLabel.setAttribute("for", "edImage");
|
||||
photoWrap.appendChild(photoLabel);
|
||||
var photoRow = el("div", "photo-readonly");
|
||||
var img = readField(record, "Image");
|
||||
if (typeof img === "string" && (img.indexOf("https://") === 0 || img.indexOf("data:image/") === 0)) {
|
||||
var im = el("img");
|
||||
im.src = img;
|
||||
im.alt = "";
|
||||
photoRow.appendChild(im);
|
||||
var currentImage = (typeof img === "string" && (img.indexOf("https://") === 0 || img.indexOf("data:image/") === 0)) ? img : null;
|
||||
var previewImg = el("img");
|
||||
previewImg.id = "edImagePreview";
|
||||
previewImg.alt = "";
|
||||
if (currentImage) {
|
||||
previewImg.src = currentImage;
|
||||
photoRow.appendChild(previewImg);
|
||||
} else {
|
||||
previewImg.hidden = true;
|
||||
photoRow.appendChild(previewImg);
|
||||
photoRow.appendChild(el("span", "mono", initials(who)));
|
||||
}
|
||||
photoRow.appendChild(el("p", "hint", "To change your photo, reply to the email that brought you here."));
|
||||
photoWrap.appendChild(photoRow);
|
||||
var photoInput = el("input");
|
||||
photoInput.type = "file";
|
||||
photoInput.id = "edImage";
|
||||
photoInput.name = "Image";
|
||||
photoInput.accept = "image/jpeg,image/png,image/webp";
|
||||
photoWrap.appendChild(photoInput);
|
||||
var photoResult = el("p", "hint", "");
|
||||
photoResult.id = "edImageResult";
|
||||
photoResult.setAttribute("role", "status");
|
||||
photoResult.setAttribute("aria-live", "polite");
|
||||
photoWrap.appendChild(photoResult);
|
||||
photoWrap.appendChild(el("p", "hint",
|
||||
"JPEG, PNG or WebP, up to 8 MB — we resize it to a square of at most 384 px first. " +
|
||||
"Leave it to keep your current photo."));
|
||||
O4PPhoto.wire(photoInput, previewImg, photoResult, function (dataUrl) {
|
||||
newImage = dataUrl;
|
||||
var fb = photoRow.querySelector(".mono");
|
||||
if (fb) fb.hidden = !!dataUrl;
|
||||
if (dataUrl) return; // wire already shows the new image
|
||||
// reverted to "no new photo": show the record's own photo again
|
||||
if (currentImage) { previewImg.src = currentImage; previewImg.hidden = false; }
|
||||
else previewImg.hidden = true;
|
||||
});
|
||||
form.appendChild(photoWrap);
|
||||
|
||||
var save = el("button", "btn", "Save my changes");
|
||||
@@ -651,6 +683,11 @@
|
||||
else if (had) payload[pair[1]] = "";
|
||||
});
|
||||
|
||||
// An untouched photo rides back exactly as loaded — it came in with the record
|
||||
// above and was never touched, so it stays byte-identical. Only a NEWLY picked
|
||||
// photo (already validated and downscaled) replaces it.
|
||||
if (newImage !== null) payload.Image = newImage;
|
||||
|
||||
fetch(C.webhook, {
|
||||
method: C.saveMethod,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
Reference in new issue
Block a user