fotos: si no se pueden limpiar, no se suben

Si quitar los metadatos fallaba, blobHandler subia la foto original, con el
GPS. Ahora se para con un 400 y un aviso (imageMetadataUnsafe, en los 11
idiomas): HEIC/HEIF/AVIF y TIFF, que pueden llevar EXIF y aqui no se saben
limpiar, y JPEG/PNG/WebP que no se dejan recorrer. Un JPEG cortado ya no
sube como una imagen vacia. Formatos sin EXIF (BMP, SVG...) pasan igual.

Es el cambio del 10 de octubre, llevado a la copia de la limpieza que vive
ahora en blobHandler.js.
This commit is contained in:
oasis-bot 2026-10-19 02:02:16 +02:00
parent ed77a13c85
commit b64dcf9b7a
2 changed files with 59 additions and 8 deletions

View file

@ -85,11 +85,12 @@ const stripJpeg = (buf) => {
const out = [buf.subarray(0, 2)]; const out = [buf.subarray(0, 2)];
let orientation = 0; let orientation = 0;
let i = 2; let i = 2;
let scan = false;
while (i + 4 <= buf.length) { while (i + 4 <= buf.length) {
if (buf[i] !== 0xFF) return null; // no es un JPEG sano: no tocarlo if (buf[i] !== 0xFF) return null; // no es un JPEG sano: no tocarlo
const marker = buf[i + 1]; const marker = buf[i + 1];
if (marker === 0xD8 || (marker >= 0xD0 && marker <= 0xD9)) { i += 2; continue; } if (marker === 0xD8 || (marker >= 0xD0 && marker <= 0xD9)) { i += 2; continue; }
if (marker === 0xDA) { out.push(buf.subarray(i)); break; } // datos comprimidos: hasta el final if (marker === 0xDA) { out.push(buf.subarray(i)); scan = true; break; } // datos comprimidos: hasta el final
const len = buf.readUInt16BE(i + 2); const len = buf.readUInt16BE(i + 2);
if (len < 2 || i + 2 + len > buf.length) return null; if (len < 2 || i + 2 + len > buf.length) return null;
const seg = buf.subarray(i + 4, i + 2 + len); const seg = buf.subarray(i + 4, i + 2 + len);
@ -97,6 +98,8 @@ const stripJpeg = (buf) => {
if (!JPEG_DROP.has(marker)) out.push(buf.subarray(i, i + 2 + len)); if (!JPEG_DROP.has(marker)) out.push(buf.subarray(i, i + 2 + len));
i += 2 + len; i += 2 + len;
} }
// sin datos de imagen (SOS) es un JPEG cortado: devolver la cabecera sola subiria una foto vacia
if (!scan) return null;
if (orientation > 1) { if (orientation > 1) {
const exif = exifOnlyOrientation(orientation); const exif = exifOnlyOrientation(orientation);
const head = Buffer.alloc(4); const head = Buffer.alloc(4);
@ -150,14 +153,40 @@ const stripWebp = (buf) => {
return Buffer.concat([head, body]); return Buffer.concat([head, body]);
}; };
/** Limpia segun el formato; si algo no cuadra, devuelve la imagen tal cual. */ // Formatos que pueden llevar EXIF (GPS incluido) y que aqui no se saben limpiar:
const stripMetadataPure = (buffer) => { // HEIC/HEIF/AVIF (caja ftyp) y TIFF. Antes subian tal cual, con la ubicacion.
try { const isUncleanable = (buf) => {
const out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer); if (buf.length >= 12 && buf.toString("latin1", 4, 8) === "ftyp") {
return out && out.length ? out : buffer; return /^(heic|heix|hevc|hevx|heim|heis|mif1|msf1|avif|avis)$/.test(buf.toString("latin1", 8, 12));
} catch {
return buffer;
} }
return buf.length >= 4 && (buf.toString("latin1", 0, 4) === "II*\0" || buf.toString("latin1", 0, 4) === "MM\0*");
};
const looksCleanable = (buf) =>
(buf.length >= 2 && buf[0] === 0xFF && buf[1] === 0xD8) ||
(buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))) ||
(buf.length >= 12 && buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP");
const unsafe = () => {
let msg = "This image could not be cleaned of its metadata, so it has not been uploaded.";
try { msg = require("../views/main_views").i18n.imageMetadataUnsafe || msg; } catch (_) {}
const err = new Error(msg);
err.status = 400;
return err;
};
/**
* Limpia segun el formato. Si es JPEG/PNG/WebP y no se puede limpiar, o es un
* formato con EXIF que no sabemos limpiar, LANZA: es mejor no subir la foto que
* subirla con la ubicacion. Formatos sin metadatos de ese tipo (BMP, SVG, ICO...)
* pasan tal cual.
*/
const stripMetadataPure = (buffer) => {
if (isUncleanable(buffer)) throw unsafe();
if (!looksCleanable(buffer)) return buffer;
let out = null;
try { out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer); } catch (_) { out = null; }
if (!out || !out.length) throw unsafe();
return out;
}; };
const stripImageMetadata = async (buffer) => { const stripImageMetadata = async (buffer) => {

View file

@ -17,6 +17,8 @@
module.exports = { module.exports = {
en: { en: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "This image could not be cleaned of its metadata (location, camera…), so it has not been uploaded. Save it as JPEG or PNG and try again.",
// Karvan // Karvan
karvanActive: "Active rooms", karvanActive: "Active rooms",
karvanBlurb: "Rooms that live only in memory and destroy themselves.", karvanBlurb: "Rooms that live only in memory and destroy themselves.",
@ -80,6 +82,8 @@ module.exports = {
spreadMore: "more" spreadMore: "more"
}, },
es: { es: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "No se han podido quitar los metadatos de esta imagen (ubicación, cámara…), así que no se ha subido. Guárdala como JPEG o PNG y vuelve a intentarlo.",
// Karvan // Karvan
karvanActive: "Salas activas", karvanActive: "Salas activas",
karvanBlurb: "Salas que viven solo en memoria y se destruyen solas.", karvanBlurb: "Salas que viven solo en memoria y se destruyen solas.",
@ -149,6 +153,8 @@ module.exports = {
totalspreads: "Réplicas totales" totalspreads: "Réplicas totales"
}, },
fr: { fr: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Impossible de retirer les métadonnées de cette image (position, appareil…), elle n'a donc pas été envoyée. Enregistrez-la en JPEG ou PNG et réessayez.",
// Karvan // Karvan
karvanActive: "Salons actifs", karvanActive: "Salons actifs",
karvanBlurb: "Des salons qui ne vivent qu'en mémoire et se détruisent d'eux-mêmes.", karvanBlurb: "Des salons qui ne vivent qu'en mémoire et se détruisent d'eux-mêmes.",
@ -212,6 +218,8 @@ module.exports = {
spreadMore: "plus" spreadMore: "plus"
}, },
eu: { eu: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Ezin izan dira irudi honen metadatuak kendu (kokapena, kamera…), beraz ez da igo. Gorde JPEG edo PNG gisa eta saiatu berriro.",
// Karvan // Karvan
karvanActive: "Gela aktiboak", karvanActive: "Gela aktiboak",
karvanBlurb: "Memorian bakarrik bizi diren eta beren kabuz suntsitzen diren gelak.", karvanBlurb: "Memorian bakarrik bizi diren eta beren kabuz suntsitzen diren gelak.",
@ -275,6 +283,8 @@ module.exports = {
spreadMore: "gehiago" spreadMore: "gehiago"
}, },
de: { de: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Die Metadaten dieses Bildes (Standort, Kamera…) konnten nicht entfernt werden, daher wurde es nicht hochgeladen. Speichere es als JPEG oder PNG und versuche es erneut.",
// Karvan // Karvan
karvanActive: "Aktive Räume", karvanActive: "Aktive Räume",
karvanBlurb: "Räume, die nur im Speicher leben und sich von selbst zerstören.", karvanBlurb: "Räume, die nur im Speicher leben und sich von selbst zerstören.",
@ -338,6 +348,8 @@ module.exports = {
spreadMore: "mehr" spreadMore: "mehr"
}, },
it: { it: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Non è stato possibile rimuovere i metadati di questa immagine (posizione, fotocamera…), quindi non è stata caricata. Salvala come JPEG o PNG e riprova.",
// Karvan // Karvan
karvanActive: "Stanze attive", karvanActive: "Stanze attive",
karvanBlurb: "Stanze che vivono solo in memoria e si distruggono da sole.", karvanBlurb: "Stanze che vivono solo in memoria e si distruggono da sole.",
@ -401,6 +413,8 @@ module.exports = {
spreadMore: "altro" spreadMore: "altro"
}, },
pt: { pt: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Não foi possível remover os metadados desta imagem (localização, câmara…), por isso não foi enviada. Guarda-a como JPEG ou PNG e tenta de novo.",
// Karvan // Karvan
karvanActive: "Salas ativas", karvanActive: "Salas ativas",
karvanBlurb: "Salas que vivem apenas em memória e destroem-se sozinhas.", karvanBlurb: "Salas que vivem apenas em memória e destroem-se sozinhas.",
@ -464,6 +478,8 @@ module.exports = {
spreadMore: "mais" spreadMore: "mais"
}, },
zh: { zh: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "无法清除这张图片的元数据(位置、相机等),因此没有上传。请另存为 JPEG 或 PNG 后重试。",
// Karvan // Karvan
karvanActive: "活跃房间", karvanActive: "活跃房间",
karvanBlurb: "只存在于内存中、会自行销毁的房间。", karvanBlurb: "只存在于内存中、会自行销毁的房间。",
@ -527,6 +543,8 @@ module.exports = {
spreadMore: "更多" spreadMore: "更多"
}, },
ar: { ar: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "تعذّر إزالة البيانات الوصفية من هذه الصورة (الموقع، الكاميرا…)، لذلك لم تُرفع. احفظها بصيغة JPEG أو PNG وحاول مجددًا.",
// Karvan // Karvan
karvanActive: "الغرف النشطة", karvanActive: "الغرف النشطة",
karvanBlurb: "غرف تعيش في الذاكرة فقط وتدمّر نفسها بنفسها.", karvanBlurb: "غرف تعيش في الذاكرة فقط وتدمّر نفسها بنفسها.",
@ -590,6 +608,8 @@ module.exports = {
spreadMore: "المزيد" spreadMore: "المزيد"
}, },
hi: { hi: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "इस छवि का मेटाडेटा (स्थान, कैमरा…) हटाया नहीं जा सका, इसलिए इसे अपलोड नहीं किया गया। इसे JPEG या PNG के रूप में सहेजें और फिर कोशिश करें।",
// Karvan // Karvan
karvanActive: "सक्रिय कक्ष", karvanActive: "सक्रिय कक्ष",
karvanBlurb: "ऐसे कक्ष जो केवल मेमोरी में रहते हैं और स्वयं नष्ट हो जाते हैं।", karvanBlurb: "ऐसे कक्ष जो केवल मेमोरी में रहते हैं और स्वयं नष्ट हो जाते हैं।",
@ -653,6 +673,8 @@ module.exports = {
spreadMore: "और" spreadMore: "और"
}, },
ru: { ru: {
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Не удалось удалить метаданные этого изображения (местоположение, камера…), поэтому оно не загружено. Сохраните его как JPEG или PNG и попробуйте снова.",
// Karvan // Karvan
karvanActive: "Активные комнаты", karvanActive: "Активные комнаты",
karvanBlurb: "Комнаты, которые живут только в памяти и уничтожаются сами.", karvanBlurb: "Комнаты, которые живут только в памяти и уничтожаются сами.",