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
@@ -431,6 +431,14 @@
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--line-brass);
|
||||
}
|
||||
.photo-preview {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--line-brass);
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
.dev-name {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-weight: 400;
|
||||
@@ -1814,8 +1822,9 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="apImage">Photo <span class="hint">(optional)</span></label>
|
||||
<input id="apImage" name="Image" type="file" accept="image/*" />
|
||||
<span class="hint">Leave it out if you prefer. If you add one it goes on your card, and it is checked before it is published.</span>
|
||||
<input id="apImage" name="Image" type="file" accept="image/jpeg,image/png,image/webp" />
|
||||
<span class="hint">Leave it out if you prefer. If you add one it goes on your card, and it is checked before it is published. JPEG, PNG or WebP, up to 8 MB — we resize it to a square of at most 384 px first.</span>
|
||||
<span class="hint" id="apImageResult" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
<label class="form-check">
|
||||
<input type="checkbox" name="is_individual" value="1" required />
|
||||
@@ -1950,6 +1959,7 @@
|
||||
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script src="/static/focus-rule.js"></script>
|
||||
<script src="/static/photo-upload.js"></script>
|
||||
<script>
|
||||
/* ============================================================================
|
||||
Odoo freelancer directory — page script.
|
||||
@@ -2146,15 +2156,6 @@
|
||||
refresh();
|
||||
}
|
||||
|
||||
function readImageAsDataUrl(file) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () { resolve(reader.result); };
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function setStatus(el, message, state) {
|
||||
if (!el) return;
|
||||
el.textContent = message;
|
||||
@@ -2921,6 +2922,23 @@
|
||||
var form = document.getElementById("applyForm");
|
||||
if (!form) return;
|
||||
var status = document.getElementById("applyStatus");
|
||||
// The photo is validated and downscaled the moment it is chosen (static/photo-upload.js);
|
||||
// the submit only uses the already-resized data URL, so nothing is read at submit time.
|
||||
// The preview element is built here (not in the markup) so no empty-src <img> ships.
|
||||
var applyImage = null;
|
||||
var resultEl = document.getElementById("apImageResult");
|
||||
var previewEl = document.createElement("img");
|
||||
previewEl.id = "apImagePreview";
|
||||
previewEl.className = "photo-preview";
|
||||
previewEl.alt = "";
|
||||
previewEl.hidden = true;
|
||||
if (resultEl && resultEl.parentNode) resultEl.parentNode.insertBefore(previewEl, resultEl);
|
||||
O4PPhoto.wire(
|
||||
form.querySelector('[name="Image"]'),
|
||||
previewEl,
|
||||
resultEl,
|
||||
function (dataUrl) { applyImage = dataUrl; }
|
||||
);
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
if (!form.reportValidity()) return;
|
||||
@@ -2940,14 +2958,9 @@
|
||||
setStatus(status, "Sending…", null);
|
||||
|
||||
var payload = buildPayload(form, false); // no id → create
|
||||
var fileEl = form.querySelector('[name="Image"]');
|
||||
var file = fileEl && fileEl.files ? fileEl.files[0] : null;
|
||||
var withImage = file
|
||||
? readImageAsDataUrl(file).then(function (dataUrl) { payload.Image = dataUrl; })
|
||||
: Promise.resolve();
|
||||
if (applyImage) payload.Image = applyImage;
|
||||
|
||||
withImage
|
||||
.then(function () { return postTo(ODOO_EXPERTS_WEBHOOK, payload); })
|
||||
postTo(ODOO_EXPERTS_WEBHOOK, payload)
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
setStatus(
|
||||
@@ -2956,6 +2969,11 @@
|
||||
"ok"
|
||||
);
|
||||
form.reset();
|
||||
applyImage = null;
|
||||
var pv = document.getElementById("apImagePreview");
|
||||
if (pv) pv.hidden = true;
|
||||
var pr = document.getElementById("apImageResult");
|
||||
if (pr) pr.textContent = "";
|
||||
initFocusUI(form, "apFocusWarn", "apFocusTotal");
|
||||
initFocusCounter(form, "apFocus", "apFocusCount");
|
||||
initFocusCounter(form, "apDescription", "apDescriptionCount", DESC_MAX_CHARS);
|
||||
|
||||
Reference in new issue
Block a user