openvibers/openvibe.tours Commits
Place photos show: accept Media's /o/<id> URLs; one upload pace per process (#110)
Changes
@@@ -3,10 +3,12 @@// A checked Commons image is copied once into Media as a public object. The file's Commons page,// author and licence stay on the place row; Media serves only the smaller image bytes.// Media takes at most 30 uploads a minute from one caller (media.object.upload); one every UPLOAD_GAP_MS stays under it.--const UPLOAD_GAP_MS = 2_100;++const UPLOAD_GAP_MS = 2_500;++// One pace for the whole process: the places job and on-demand discovery each make a cache, and together they must++// stay under Media's limit too.++let nextUploadAt = 0;function createPhotoCache({ config, s, fetchImpl = globalThis.fetch, log = console, objectsClient = null, uploadGapMs = UPLOAD_GAP_MS }) {let objects = objectsClient;-- let nextUploadAt = 0;async function uploadSlot() {const now = Date.now();const at = Math.max(now, nextUploadAt);@@@ -34,7 +34,7 @@ const safeImage = (v) => {try {const { protocol, hostname, pathname } = new URL(u);if (protocol !== 'https:' || !IMAGE_HOSTS.test(hostname)) return null;-- return /wikimedia\.org$/i.test(hostname) || (hostname === 'openvibe.media' && /^\/api\/v2\/tours\/objects\//.test(pathname))++ return /wikimedia\.org$/i.test(hostname) || (hostname === 'openvibe.media' && (/^\/api\/v2\/tours\/objects\//.test(pathname) || /^\/o\/[A-Za-z0-9_-]{6,64}$/.test(pathname)))|| /\.(jpe?g|png|webp|gif|avif|svg)$/i.test(pathname) ? u : null;} catch { return null; }};@@@ -31,6 +31,10 @@ assert.ok(safeImage('https://upload.wikimedia.org/wikipedia/commons/a/ab/Hachikoassert.ok(safeImage('https://commons.wikimedia.org/wiki/Special:FilePath/Hachiko.jpg?width=640'), 'Commons file paths');assert.ok(safeImage('https://openvibe.media/files/tower.webp'), 'an OpenVibe image');assert.strictEqual(safeImage('https://openvibe.media/gallery'), null, 'a page is not a picture');++// Media answers a public object at /o/<id> (no extension): the place photos Tours copies there (11-08: all were refused).++assert.ok(safeImage('https://openvibe.media/o/med_01JCX9S3K2M4N5P6Q7R8S9T0VW'), 'a Media public object');++assert.strictEqual(safeImage('https://openvibe.media/o/../admin'), null, 'only an object id');++assert.strictEqual(safeImage('https://openvibe.media/o/'), null, 'an id is required');// Only hosts the page's img-src allows: anything else would be refused and draw a broken image (2026-11-02).assert.strictEqual(safeImage('https://images.mapillary.com/6Vri6u81GdjqVKBQTbQYBg/thumb-2048.jpg'), null, 'Mapillary is not allowed');assert.strictEqual(safeImage('https://example.com/photos/tower.webp'), null, 'an image on another host');
@@ -3,10 +3,12 @@
@@ -3,10 +3,12 @@
// A checked Commons image is copied once into Media as a public object. The file's Commons page,
// A checked Commons image is copied once into Media as a public object. The file's Commons page,
// author and licence stay on the place row; Media serves only the smaller image bytes.
// author and licence stay on the place row; Media serves only the smaller image bytes.
// Media takes at most 30 uploads a minute from one caller (media.object.upload); one every UPLOAD_GAP_MS stays under it.
// Media takes at most 30 uploads a minute from one caller (media.object.upload); one every UPLOAD_GAP_MS stays under it.
-const UPLOAD_GAP_MS = 2_100;
+const UPLOAD_GAP_MS = 2_500;
+// One pace for the whole process: the places job and on-demand discovery each make a cache, and together they must
+// stay under Media's limit too.
+let nextUploadAt = 0;
function createPhotoCache({ config, s, fetchImpl = globalThis.fetch, log = console, objectsClient = null, uploadGapMs = UPLOAD_GAP_MS }) {
function createPhotoCache({ config, s, fetchImpl = globalThis.fetch, log = console, objectsClient = null, uploadGapMs = UPLOAD_GAP_MS }) {
let objects = objectsClient;
let objects = objectsClient;
- let nextUploadAt = 0;
async function uploadSlot() {
async function uploadSlot() {
const now = Date.now();
const now = Date.now();
const at = Math.max(now, nextUploadAt);
const at = Math.max(now, nextUploadAt);
@@ -34,7 +34,7 @@ const safeImage = (v) => {
@@ -34,7 +34,7 @@ const safeImage = (v) => {
try {
try {
const { protocol, hostname, pathname } = new URL(u);
const { protocol, hostname, pathname } = new URL(u);
if (protocol !== 'https:' || !IMAGE_HOSTS.test(hostname)) return null;
if (protocol !== 'https:' || !IMAGE_HOSTS.test(hostname)) return null;
- return /wikimedia\.org$/i.test(hostname) || (hostname === 'openvibe.media' && /^\/api\/v2\/tours\/objects\//.test(pathname))
+ return /wikimedia\.org$/i.test(hostname) || (hostname === 'openvibe.media' && (/^\/api\/v2\/tours\/objects\//.test(pathname) || /^\/o\/[A-Za-z0-9_-]{6,64}$/.test(pathname)))
|| /\.(jpe?g|png|webp|gif|avif|svg)$/i.test(pathname) ? u : null;
|| /\.(jpe?g|png|webp|gif|avif|svg)$/i.test(pathname) ? u : null;
} catch { return null; }
} catch { return null; }
};
};
@@ -31,6 +31,10 @@ assert.ok(safeImage('https://upload.wikimedia.org/wikipedia/commons/a/ab/Hachiko
@@ -31,6 +31,10 @@ assert.ok(safeImage('https://upload.wikimedia.org/wikipedia/commons/a/ab/Hachiko
assert.ok(safeImage('https://commons.wikimedia.org/wiki/Special:FilePath/Hachiko.jpg?width=640'), 'Commons file paths');
assert.ok(safeImage('https://commons.wikimedia.org/wiki/Special:FilePath/Hachiko.jpg?width=640'), 'Commons file paths');
assert.ok(safeImage('https://openvibe.media/files/tower.webp'), 'an OpenVibe image');
assert.ok(safeImage('https://openvibe.media/files/tower.webp'), 'an OpenVibe image');
assert.strictEqual(safeImage('https://openvibe.media/gallery'), null, 'a page is not a picture');
assert.strictEqual(safeImage('https://openvibe.media/gallery'), null, 'a page is not a picture');
+// Media answers a public object at /o/<id> (no extension): the place photos Tours copies there (11-08: all were refused).
+assert.ok(safeImage('https://openvibe.media/o/med_01JCX9S3K2M4N5P6Q7R8S9T0VW'), 'a Media public object');
+assert.strictEqual(safeImage('https://openvibe.media/o/../admin'), null, 'only an object id');
+assert.strictEqual(safeImage('https://openvibe.media/o/'), null, 'an id is required');
// Only hosts the page's img-src allows: anything else would be refused and draw a broken image (2026-11-02).
// Only hosts the page's img-src allows: anything else would be refused and draw a broken image (2026-11-02).
assert.strictEqual(safeImage('https://images.mapillary.com/6Vri6u81GdjqVKBQTbQYBg/thumb-2048.jpg'), null, 'Mapillary is not allowed');
assert.strictEqual(safeImage('https://images.mapillary.com/6Vri6u81GdjqVKBQTbQYBg/thumb-2048.jpg'), null, 'Mapillary is not allowed');
assert.strictEqual(safeImage('https://example.com/photos/tower.webp'), null, 'an image on another host');
assert.strictEqual(safeImage('https://example.com/photos/tower.webp'), null, 'an image on another host');