mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix(wildlife): point the Acartia card link only at acartia.io
The card's "Acartia" link was built from the contributor-supplied photo_url, so any https site could appear labelled as Acartia. Acartia has no documented per-sighting page, so the source link is now always the Acartia site, on the server projection and again in the card. The separate "Open on Acartia" fallback link is no longer needed.
This commit is contained in:
1 parent
82138478d0
commit
3cb30c3f97
16 files changed
+45
-29
No files matched your search
@@ -174,7 +174,8 @@ compact anchored photo card. Selection stays pinned across camera, filter and
|
|||||||
sample changes until explicit deselection or layer disable. Its medium image
|
sample changes until explicit deselection or layer disable. Its medium image
|
||||||
uses `object-fit: contain`, with names, compact observed age and creator/licence.
|
uses `object-fit: contain`, with names, compact observed age and creator/licence.
|
||||||
The card is the single source link to iNaturalist. Obscured sightings also stay
|
The card is the single source link to iNaturalist. Obscured sightings also stay
|
||||||
text-only; Acartia stays text-only with provider credit and whale guidance.
|
text-only; Acartia stays text-only with provider credit and whale guidance,
|
||||||
|
and its source link is always `https://acartia.io/`, never a contributed URL.
|
||||||
Placement avoids actual HUD, toolbar and rail rectangles, panning only when
|
Placement avoids actual HUD, toolbar and rail rectangles, panning only when
|
||||||
necessary while preserving zoom. Escape, close or an outside click closes the
|
necessary while preserving zoom. Escape, close or an outside click closes the
|
||||||
card and clears selection. Camera/layout changes reposition the card without
|
card and clears selection. Camera/layout changes reposition the card without
|
||||||
|
|||||||
@@ -3453,7 +3453,6 @@
|
|||||||
"Loading photo...": "جارٍ تحميل الصورة…",
|
"Loading photo...": "جارٍ تحميل الصورة…",
|
||||||
"Photo unavailable": "الصورة غير متاحة",
|
"Photo unavailable": "الصورة غير متاحة",
|
||||||
"Location obscured to a 0.2° cell": "الموقع مخفي ضمن خلية بمقدار 0.2°",
|
"Location obscured to a 0.2° cell": "الموقع مخفي ضمن خلية بمقدار 0.2°",
|
||||||
"Open on Acartia ↗": "فتح على Acartia ↗",
|
|
||||||
"Photo of {name}": "صورة {name}",
|
"Photo of {name}": "صورة {name}",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3153,7 +3153,6 @@
|
|||||||
"Loading photo...": "Foto wird geladen…",
|
"Loading photo...": "Foto wird geladen…",
|
||||||
"Photo unavailable": "Foto nicht verfügbar",
|
"Photo unavailable": "Foto nicht verfügbar",
|
||||||
"Location obscured to a 0.2° cell": "Standort auf eine 0,2°-Rasterzelle verschleiert",
|
"Location obscured to a 0.2° cell": "Standort auf eine 0,2°-Rasterzelle verschleiert",
|
||||||
"Open on Acartia ↗": "Auf Acartia öffnen ↗",
|
|
||||||
"Photo of {name}": "Foto von {name}",
|
"Photo of {name}": "Foto von {name}",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3228,7 +3228,6 @@
|
|||||||
"Loading photo...": "Cargando foto…",
|
"Loading photo...": "Cargando foto…",
|
||||||
"Photo unavailable": "Foto no disponible",
|
"Photo unavailable": "Foto no disponible",
|
||||||
"Location obscured to a 0.2° cell": "Ubicación oculta en una celda de 0,2°",
|
"Location obscured to a 0.2° cell": "Ubicación oculta en una celda de 0,2°",
|
||||||
"Open on Acartia ↗": "Abrir en Acartia ↗",
|
|
||||||
"Photo of {name}": "Foto de {name}",
|
"Photo of {name}": "Foto de {name}",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3228,7 +3228,6 @@
|
|||||||
"Loading photo...": "Chargement de la photo…",
|
"Loading photo...": "Chargement de la photo…",
|
||||||
"Photo unavailable": "Photo indisponible",
|
"Photo unavailable": "Photo indisponible",
|
||||||
"Location obscured to a 0.2° cell": "Localisation masquée dans une cellule de 0,2°",
|
"Location obscured to a 0.2° cell": "Localisation masquée dans une cellule de 0,2°",
|
||||||
"Open on Acartia ↗": "Ouvrir sur Acartia ↗",
|
|
||||||
"Photo of {name}": "Photo de {name}",
|
"Photo of {name}": "Photo de {name}",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3228,7 +3228,6 @@
|
|||||||
"Loading photo...": "Caricamento della foto…",
|
"Loading photo...": "Caricamento della foto…",
|
||||||
"Photo unavailable": "Foto non disponibile",
|
"Photo unavailable": "Foto non disponibile",
|
||||||
"Location obscured to a 0.2° cell": "Posizione oscurata in una cella di 0,2°",
|
"Location obscured to a 0.2° cell": "Posizione oscurata in una cella di 0,2°",
|
||||||
"Open on Acartia ↗": "Apri su Acartia ↗",
|
|
||||||
"Photo of {name}": "Foto di {name}",
|
"Photo of {name}": "Foto di {name}",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3078,7 +3078,6 @@
|
|||||||
"Loading photo...": "写真を読み込み中…",
|
"Loading photo...": "写真を読み込み中…",
|
||||||
"Photo unavailable": "写真を表示できません",
|
"Photo unavailable": "写真を表示できません",
|
||||||
"Location obscured to a 0.2° cell": "位置は0.2°のセル内に秘匿されています",
|
"Location obscured to a 0.2° cell": "位置は0.2°のセル内に秘匿されています",
|
||||||
"Open on Acartia ↗": "Acartiaで開く ↗",
|
|
||||||
"Photo of {name}": "{name}の写真",
|
"Photo of {name}": "{name}の写真",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3228,7 +3228,6 @@
|
|||||||
"Loading photo...": "Carregando foto…",
|
"Loading photo...": "Carregando foto…",
|
||||||
"Photo unavailable": "Foto indisponível",
|
"Photo unavailable": "Foto indisponível",
|
||||||
"Location obscured to a 0.2° cell": "Localização ocultada em uma célula de 0,2°",
|
"Location obscured to a 0.2° cell": "Localização ocultada em uma célula de 0,2°",
|
||||||
"Open on Acartia ↗": "Abrir no Acartia ↗",
|
|
||||||
"Photo of {name}": "Foto de {name}",
|
"Photo of {name}": "Foto de {name}",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3303,7 +3303,6 @@
|
|||||||
"Loading photo...": "Загрузка фото…",
|
"Loading photo...": "Загрузка фото…",
|
||||||
"Photo unavailable": "Фото недоступно",
|
"Photo unavailable": "Фото недоступно",
|
||||||
"Location obscured to a 0.2° cell": "Местоположение скрыто в ячейке 0,2°",
|
"Location obscured to a 0.2° cell": "Местоположение скрыто в ячейке 0,2°",
|
||||||
"Open on Acartia ↗": "Открыть на Acartia ↗",
|
|
||||||
"Photo of {name}": "Фото {name}",
|
"Photo of {name}": "Фото {name}",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3303,7 +3303,6 @@
|
|||||||
"Loading photo...": "Завантаження фото…",
|
"Loading photo...": "Завантаження фото…",
|
||||||
"Photo unavailable": "Фото недоступне",
|
"Photo unavailable": "Фото недоступне",
|
||||||
"Location obscured to a 0.2° cell": "Місцеположення приховано в комірці 0,2°",
|
"Location obscured to a 0.2° cell": "Місцеположення приховано в комірці 0,2°",
|
||||||
"Open on Acartia ↗": "Відкрити на Acartia ↗",
|
|
||||||
"Photo of {name}": "Фото {name}",
|
"Photo of {name}": "Фото {name}",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3078,7 +3078,6 @@
|
|||||||
"Loading photo...": "正在加载照片…",
|
"Loading photo...": "正在加载照片…",
|
||||||
"Photo unavailable": "照片不可用",
|
"Photo unavailable": "照片不可用",
|
||||||
"Location obscured to a 0.2° cell": "位置已模糊至0.2°网格",
|
"Location obscured to a 0.2° cell": "位置已模糊至0.2°网格",
|
||||||
"Open on Acartia ↗": "在Acartia上打开 ↗",
|
|
||||||
"Photo of {name}": "{name}的照片",
|
"Photo of {name}": "{name}的照片",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -3078,7 +3078,6 @@
|
|||||||
"Loading photo...": "正在載入照片…",
|
"Loading photo...": "正在載入照片…",
|
||||||
"Photo unavailable": "照片無法使用",
|
"Photo unavailable": "照片無法使用",
|
||||||
"Location obscured to a 0.2° cell": "位置已模糊至0.2°網格",
|
"Location obscured to a 0.2° cell": "位置已模糊至0.2°網格",
|
||||||
"Open on Acartia ↗": "在Acartia上開啟 ↗",
|
|
||||||
"Photo of {name}": "{name}的照片",
|
"Photo of {name}": "{name}的照片",
|
||||||
"Acartia": "Acartia",
|
"Acartia": "Acartia",
|
||||||
"CC BY 4.0": "CC BY 4.0",
|
"CC BY 4.0": "CC BY 4.0",
|
||||||
|
|||||||
@@ -247,18 +247,9 @@ export function normalizeInatObservations(
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
function acartiaLink(value) {
|
// Acartia has no documented per-sighting page, and photo_url is supplied by the
|
||||||
if (typeof value !== 'string' || value.length > 500)
|
// contributor. The source link is therefore always the Acartia site itself.
|
||||||
return 'https://acartia.io';
|
export const ACARTIA_SITE = 'https://acartia.io/';
|
||||||
try {
|
|
||||||
const url = new URL(value);
|
|
||||||
return url.protocol === 'https:' && !url.username && !url.password
|
|
||||||
? url.href
|
|
||||||
: 'https://acartia.io';
|
|
||||||
} catch {
|
|
||||||
return 'https://acartia.io';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Acartia documents flat rows only. Name and entity are text fields; unknown
|
/** Acartia documents flat rows only. Name and entity are text fields; unknown
|
||||||
* shapes are skipped rather than guessing nested provider data. */
|
* shapes are skipped rather than guessing nested provider data. */
|
||||||
@@ -315,7 +306,7 @@ export function normalizeAcartiaSightings(payload, nowMs) {
|
|||||||
cetacean: speciesGroup === 'cetacean',
|
cetacean: speciesGroup === 'cetacean',
|
||||||
threatened: false,
|
threatened: false,
|
||||||
provider: { name, entity: cleanText(raw.data_source_entity, 60) },
|
provider: { name, entity: cleanText(raw.data_source_entity, 60) },
|
||||||
link: acartiaLink(raw.photo_url),
|
link: ACARTIA_SITE,
|
||||||
photo: null,
|
photo: null,
|
||||||
favesCount: 0,
|
favesCount: 0,
|
||||||
identificationAgreements: 0,
|
identificationAgreements: 0,
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ test('Acartia requires flat provider text, handles trust, and sanitizes type', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Acartia rejects invalid times/positions and unsafe photo links become the canonical page', () => {
|
test('Acartia rejects invalid times/positions and its link is always the Acartia site', () => {
|
||||||
for (const overrides of [
|
for (const overrides of [
|
||||||
{ created: new Date(now - 8 * 24 * hour - 1).toISOString() },
|
{ created: new Date(now - 8 * 24 * hour - 1).toISOString() },
|
||||||
{ created: new Date(now + 10 * 60_000 + 1).toISOString() },
|
{ created: new Date(now + 10 * 60_000 + 1).toISOString() },
|
||||||
@@ -341,6 +341,10 @@ test('Acartia rejects invalid times/positions and unsafe photo links become the
|
|||||||
])
|
])
|
||||||
assert.deepEqual(normalizeAcartiaSightings([sighting(overrides)], now), []);
|
assert.deepEqual(normalizeAcartiaSightings([sighting(overrides)], now), []);
|
||||||
for (const photo_url of [
|
for (const photo_url of [
|
||||||
|
// A contributor-supplied URL is never labelled as the Acartia source.
|
||||||
|
'https://provider.example/photo.jpg',
|
||||||
|
'https://phishing.example/acartia.io/login',
|
||||||
|
'https://acartia.io.attacker.example/',
|
||||||
'http://provider.example/p.jpg',
|
'http://provider.example/p.jpg',
|
||||||
'https://user:[email protected]/p.jpg',
|
'https://user:[email protected]/p.jpg',
|
||||||
'javascript:bad()',
|
'javascript:bad()',
|
||||||
@@ -349,7 +353,7 @@ test('Acartia rejects invalid times/positions and unsafe photo links become the
|
|||||||
]) {
|
]) {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
normalizeAcartiaSightings([sighting({ photo_url })], now)[0].link,
|
normalizeAcartiaSightings([sighting({ photo_url })], now)[0].link,
|
||||||
'https://acartia.io',
|
'https://acartia.io/',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, shallowRef, useTemplateRef, watch } from 'vue';
|
import { computed, shallowRef, useTemplateRef, watch } from 'vue';
|
||||||
import { formatDate, t, translateKnown } from '../../../i18n/index.js';
|
import { formatDate, t, translateKnown } from '../../../i18n/index.js';
|
||||||
import { photoCreator, formatAge } from '../../../layers/wildlifeSightings/photoText.js';
|
import { photoCreator, formatAge } from '../../../layers/wildlifeSightings/photoText.js';
|
||||||
import { inatLicenceLabel, inatPhotoUrl, isOpenPhoto } from '../../../layers/wildlifeSightings/records.js';
|
import { ACARTIA_SITE, inatLicenceLabel, inatPhotoUrl, isOpenPhoto } from '../../../layers/wildlifeSightings/records.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
sighting: { type: Object, required: true },
|
sighting: { type: Object, required: true },
|
||||||
@@ -36,6 +36,8 @@ const age = computed(() => {
|
|||||||
});
|
});
|
||||||
const observedDate = computed(() => observedDay.value ? formatDate(Date.parse(`${observedDay.value}T00:00:00Z`), { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' }) : '');
|
const observedDate = computed(() => observedDay.value ? formatDate(Date.parse(`${observedDay.value}T00:00:00Z`), { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' }) : '');
|
||||||
const link = computed(() => {
|
const link = computed(() => {
|
||||||
|
// The "Acartia" label only ever points at the Acartia site, never a row URL.
|
||||||
|
if (acartia.value) return ACARTIA_SITE;
|
||||||
try {
|
try {
|
||||||
const url = new URL(props.sighting.link);
|
const url = new URL(props.sighting.link);
|
||||||
return url.protocol === 'https:' && !url.username && !url.password ? url.href : null;
|
return url.protocol === 'https:' && !url.username && !url.password ? url.href : null;
|
||||||
@@ -89,7 +91,6 @@ watch([() => props.sighting.stableId, () => photo.value?.src], () => { loaded.va
|
|||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
<p v-if="acartia || taxa === 'whales'" class="wildlife-sighting-card-guidance">
|
<p v-if="acartia || taxa === 'whales'" class="wildlife-sighting-card-guidance">
|
||||||
<a v-if="acartia && !/^https:\/\/acartia\.io\/?$/.test(link || '')" href="https://acartia.io" target="_blank" rel="noopener noreferrer">{{ t('Open on Acartia ↗') }}</a>
|
|
||||||
<a href="https://www.bewhalewise.org" target="_blank" rel="noopener noreferrer">{{ t('Be Whale Wise') }}</a>
|
<a href="https://www.bewhalewise.org" target="_blank" rel="noopener noreferrer">{{ t('Be Whale Wise') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -373,6 +373,36 @@ test('missing, restricted, obscured and Acartia photos all use the same compact
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an Acartia card labels only the Acartia site as Acartia', async (context) => {
|
||||||
|
for (const link of [
|
||||||
|
'https://provider.example/photos/synthetic.jpg',
|
||||||
|
'https://acartia.io.attacker.example/',
|
||||||
|
'https://acartia.io/',
|
||||||
|
])
|
||||||
|
await context.test(link, async (context) => {
|
||||||
|
const f = fixture(context, {
|
||||||
|
...row,
|
||||||
|
origin: 'acartia',
|
||||||
|
stableId: 'acartia:456',
|
||||||
|
provider: { name: 'Synthetic network' },
|
||||||
|
link,
|
||||||
|
});
|
||||||
|
await f.click();
|
||||||
|
const card = f.cardElement();
|
||||||
|
const hrefs = [...card.querySelectorAll('a')].map((a) =>
|
||||||
|
a.getAttribute('href'),
|
||||||
|
);
|
||||||
|
assert.deepEqual(hrefs, [
|
||||||
|
'https://acartia.io/',
|
||||||
|
'https://www.bewhalewise.org',
|
||||||
|
]);
|
||||||
|
assert.equal(
|
||||||
|
card.querySelector('.wildlife-sighting-card-source').textContent,
|
||||||
|
'Acartia ↗',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('safe placements near each viewport edge exclude actual HUD, toolbar and rail rects', () => {
|
test('safe placements near each viewport edge exclude actual HUD, toolbar and rail rects', () => {
|
||||||
const obstacles = [
|
const obstacles = [
|
||||||
rect(0, 0, 260, 160),
|
rect(0, 0, 260, 160),
|
||||||
|
|||||||
Reference in new issue
Block a user