insert copyright headers for images
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { APP_IMAGE_COPYRIGHT_NOTICE, GROUP_SYMBOL_COPYRIGHT_NOTICE, UPLOAD_COPYRIGHT_NOTICE, COPYRIGHT_HEADER_LIMITS, ENVATO_SYMBOL_SOURCE, setAppImageCopyright } from '../src/services/copyright.js';
|
||||
|
||||
test('image notices distinguish Envato symbols from other assets and skip non-image files', () => {
|
||||
const headers = new Map();
|
||||
const res = { setHeader: (name, value) => headers.set(name, value) };
|
||||
for (const extension of ['png', 'jpg', 'jpeg', 'webp', 'gif', 'svg', 'ico', 'avif', 'PNG']) {
|
||||
headers.clear();
|
||||
setAppImageCopyright(res, path.join('public', 'assets', `graphic.${extension}`));
|
||||
assert.equal(headers.get('X-Copyright'), APP_IMAGE_COPYRIGHT_NOTICE);
|
||||
assert.equal(headers.get('X-Copyright-Notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(headers.has('X-Copyright-Source'), false);
|
||||
}
|
||||
setAppImageCopyright(res, path.join('public', 'images', 'group-symbols', '🐦.png'));
|
||||
assert.equal(headers.get('X-Copyright'), GROUP_SYMBOL_COPYRIGHT_NOTICE);
|
||||
assert.equal(headers.get('X-Copyright-Notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(headers.get('X-Copyright-Source'), ENVATO_SYMBOL_SOURCE);
|
||||
for (const extension of ['js', 'css', 'html', 'txt', 'mp3']) {
|
||||
headers.clear();
|
||||
setAppImageCopyright(res, path.join('public', 'assets', `file.${extension}`));
|
||||
assert.equal(headers.has('X-Copyright'), false);
|
||||
}
|
||||
for (const value of [APP_IMAGE_COPYRIGHT_NOTICE, GROUP_SYMBOL_COPYRIGHT_NOTICE, UPLOAD_COPYRIGHT_NOTICE, COPYRIGHT_HEADER_LIMITS, ENVATO_SYMBOL_SOURCE]) {
|
||||
assert.match(value, /^[\x20-\x7e]+$/, 'Header values must not contain Unicode or line breaks');
|
||||
}
|
||||
});
|
||||
@@ -225,6 +225,7 @@ function buildEditorContext() {
|
||||
'editor-route-list-view',
|
||||
'editor-route-name',
|
||||
'editor-route-upload',
|
||||
'editor-rights-open',
|
||||
'editor-status'
|
||||
];
|
||||
const elements = new Map(ids.map(id => [id, new FakeElement(id)]));
|
||||
@@ -365,6 +366,7 @@ function buildEditorContext() {
|
||||
Wegwichtel: {
|
||||
App: { currentPosition: () => ({ lat: 50.123456789, lon: 10.987654321, accuracy: 4.321 }) },
|
||||
Config: { apiBase: '/api/' },
|
||||
LegalUi: { open: name => { controls.legalPage = name; } },
|
||||
RouteRecorder: {
|
||||
beginEditing(id) { controls.recording = id; },
|
||||
endEditing() { controls.recording = null; },
|
||||
@@ -396,6 +398,9 @@ test('public editor workflow saves new route and GPX Station locally without POS
|
||||
assert.equal(elements.get('editor-button').attributes.get('aria-expanded'), 'true');
|
||||
assert.ok(calls.some(call => call.method === 'GET' && call.url === '/api/routes?activatedOnly=true'));
|
||||
|
||||
elements.get('editor-rights-open').dispatchEvent(pointerEvent());
|
||||
assert.equal(controls.legalPage, 'terms');
|
||||
|
||||
await context.Wegwichtel.EditorUi.openNewLocalRoute();
|
||||
assert.equal(elements.get('editor-route-gpx-group').hidden, true);
|
||||
elements.get('editor-route-name').value = 'Neue lokale Strecke';
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ test('German legal pages and contacts remain static when the app uses English',
|
||||
const i18n = await loadI18n(context);
|
||||
const html = await fs.readFile(path.join(root, 'public/index.html'), 'utf8');
|
||||
const englishHtml = localizeHtml(html, key => i18n.t(key));
|
||||
for (const name of ['privacy', 'imprint']) {
|
||||
for (const name of ['privacy', 'imprint', 'terms']) {
|
||||
const pattern = new RegExp(`<article[^>]+data-info-page="${name}"[^>]*>[\\s\\S]*?</article>`);
|
||||
const page = pattern.exec(html)?.[0];
|
||||
assert.ok(page, name);
|
||||
@@ -22,7 +22,7 @@ test('German legal pages and contacts remain static when the app uses English',
|
||||
assert.equal(pattern.exec(englishHtml)?.[0], page);
|
||||
}
|
||||
for (const catalog of context.Wegwichtel.TextCatalogs.values()) {
|
||||
for (const key of ['privacy', 'imprint', 'contacts']) {
|
||||
for (const key of ['privacy', 'imprint', 'terms', 'contacts']) {
|
||||
assert.equal(Object.hasOwn(catalog, key), false, key);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -31,7 +31,7 @@ test('start screen exposes cookie status, optional local progress and deliberate
|
||||
assert.match(storage, /cookiesEnabled: \(\) => false/);
|
||||
});
|
||||
|
||||
test('information button opens scrollable overlay pages for privacy, imprint and technology', async () => {
|
||||
test('information button opens scrollable overlay pages for privacy, imprint, terms and technology', async () => {
|
||||
const html = await read('public/index.html');
|
||||
const css = await read('public/css/app.css');
|
||||
const ui = await read('public/js/legal-ui.js');
|
||||
@@ -41,6 +41,7 @@ test('information button opens scrollable overlay pages for privacy, imprint and
|
||||
assert.match(html, /<dialog id="info-dialog"/);
|
||||
assert.match(html, /data-info-target="privacy"/);
|
||||
assert.match(html, /data-info-target="imprint"/);
|
||||
assert.match(html, /data-info-target="terms"/);
|
||||
assert.match(html, /data-info-target="technology"/);
|
||||
assert.match(html, /<h3>Technisch verantwortlich<\/h3>\s*<p>Florian Zumpe<br>/);
|
||||
assert.match(html, /\[Anschrift von Florian Zumpe ergänzen\]/);
|
||||
@@ -137,7 +138,8 @@ test('legal overlay handlers synchronize switches without advancing the start sc
|
||||
const privacyPage = new FakeElement();
|
||||
const imprintPage = new FakeElement();
|
||||
const technologyPage = new FakeElement();
|
||||
const pages = { privacy: privacyPage, imprint: imprintPage, technology: technologyPage };
|
||||
const termsPage = new FakeElement();
|
||||
const pages = { privacy: privacyPage, imprint: imprintPage, terms: termsPage, technology: technologyPage };
|
||||
const targetButtons = Object.keys(pages).map(name => {
|
||||
const button = new FakeElement();
|
||||
button.dataset.infoTarget = name;
|
||||
@@ -234,6 +236,18 @@ test('legal overlay handlers synchronize switches without advancing the start sc
|
||||
assert.equal(elements.get('info-dialog-title').textContent, 'Impressum');
|
||||
assert.equal(elements.get('info-dialog-title').attributes.get('lang'), 'de');
|
||||
|
||||
targetButtons[2].dispatchEvent(pointerEvent());
|
||||
assert.equal(termsPage.hidden, false);
|
||||
assert.equal(imprintPage.hidden, true);
|
||||
assert.equal(elements.get('info-dialog-title').textContent, 'Nutzungsvereinbarung');
|
||||
assert.equal(elements.get('info-dialog-title').attributes.get('lang'), 'de');
|
||||
assert.equal(Object.hasOwn(elements.get('info-dialog-title').dataset, 'i18n'), false);
|
||||
|
||||
targetButtons[3].dispatchEvent(pointerEvent());
|
||||
assert.equal(technologyPage.hidden, false);
|
||||
assert.equal(elements.get('info-dialog-title').textContent, 'Saving and permissions');
|
||||
assert.equal(elements.get('info-dialog-title').attributes.get('lang'), 'en');
|
||||
|
||||
progressToggles[0].checked = true;
|
||||
progressToggles[0].dispatchEvent(new Event('change'));
|
||||
assert.deepEqual(changes, [true]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseGpx } from '../src/services/gpx.js';
|
||||
import { UPLOAD_COPYRIGHT_NOTICE, APP_IMAGE_COPYRIGHT_NOTICE, GROUP_SYMBOL_COPYRIGHT_NOTICE, COPYRIGHT_HEADER_LIMITS, ENVATO_SYMBOL_SOURCE } from '../src/services/copyright.js';
|
||||
import { SAMPLE_GPX } from '../test-support/gpx-fixtures.js';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
@@ -144,6 +145,7 @@ test('the npm-started local API is exercised through a write-auth proxy on loopb
|
||||
|
||||
const indexResponse = await fetch(`${baseUrl}/`);
|
||||
const indexHtml = await indexResponse.text();
|
||||
assert.equal(indexResponse.headers.get('x-copyright'), null);
|
||||
assert.equal(indexResponse.status, 200);
|
||||
assert.match(indexResponse.headers.get('cache-control') || '', /no-store/);
|
||||
assert.match(indexHtml, /css\/app\.css\?v=0\.12\.14/);
|
||||
@@ -164,6 +166,35 @@ test('the npm-started local API is exercised through a write-auth proxy on loopb
|
||||
assert.match(appStyleResponse.headers.get('cache-control') || '', /no-store/);
|
||||
await appStyleResponse.arrayBuffer();
|
||||
|
||||
assert.equal(appStyleResponse.headers.get('x-copyright'), null);
|
||||
const iconResponse = await fetch(`${baseUrl}/images/icons/play.svg`);
|
||||
assert.equal(iconResponse.status, 200);
|
||||
assert.equal(iconResponse.headers.get('x-copyright'), APP_IMAGE_COPYRIGHT_NOTICE);
|
||||
assert.equal(iconResponse.headers.get('x-copyright-notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(iconResponse.headers.get('x-copyright-source'), null);
|
||||
await iconResponse.arrayBuffer();
|
||||
const symbols = await fs.readdir(path.join(root, 'public/images/group-symbols'));
|
||||
for (const name of symbols.filter(name => name.endsWith('.png'))) {
|
||||
const symbolUrl = `${baseUrl}/images/group-symbols/${encodeURIComponent(name)}`;
|
||||
const symbol = await fetch(symbolUrl);
|
||||
assert.equal(symbol.status, 200, name);
|
||||
assert.equal(symbol.headers.get('x-copyright'), GROUP_SYMBOL_COPYRIGHT_NOTICE, name);
|
||||
assert.equal(symbol.headers.get('x-copyright-notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(symbol.headers.get('x-copyright-source'), ENVATO_SYMBOL_SOURCE);
|
||||
const etag = symbol.headers.get('etag');
|
||||
await symbol.arrayBuffer();
|
||||
const symbolHead = await fetch(symbolUrl, { method: 'HEAD' });
|
||||
assert.equal(symbolHead.status, 200);
|
||||
assert.equal(symbolHead.headers.get('x-copyright'), GROUP_SYMBOL_COPYRIGHT_NOTICE);
|
||||
assert.equal(symbolHead.headers.get('x-copyright-notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(symbolHead.headers.get('x-copyright-source'), ENVATO_SYMBOL_SOURCE);
|
||||
const symbolCached = await fetch(symbolUrl, { headers: { 'If-None-Match': etag, 'Cache-Control': 'max-age=0' } });
|
||||
assert.equal(symbolCached.status, 304);
|
||||
assert.equal(symbolCached.headers.get('x-copyright'), GROUP_SYMBOL_COPYRIGHT_NOTICE);
|
||||
assert.equal(symbolCached.headers.get('x-copyright-notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(symbolCached.headers.get('x-copyright-source'), ENVATO_SYMBOL_SOURCE);
|
||||
}
|
||||
|
||||
const pythonHealthCheck = await runProcess('python3', ['-c', `
|
||||
from common import ApiClient
|
||||
result = ApiClient('${LOCAL_API_URL}').request('GET', '/api/health')
|
||||
@@ -328,6 +359,32 @@ assert result['ok'] is True
|
||||
const { response: pictureFileResponse, body: pictureFile } = await requestBuffer(`${baseUrl}${picture.url}`);
|
||||
assert.match(pictureFileResponse.headers.get('content-type') || '', /image\/png/);
|
||||
assert.deepEqual(pictureFile, PNG_FILE);
|
||||
assert.equal(pictureFileResponse.headers.get('x-copyright'), UPLOAD_COPYRIGHT_NOTICE);
|
||||
assert.equal(pictureFileResponse.headers.get('x-copyright-notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(pictureFileResponse.headers.get('x-copyright-source'), null);
|
||||
const pictureHead = await fetch(`${baseUrl}${picture.url}`, { method: 'HEAD' });
|
||||
assert.equal(pictureHead.status, 200);
|
||||
assert.equal(pictureHead.headers.get('x-copyright'), UPLOAD_COPYRIGHT_NOTICE);
|
||||
assert.equal(pictureHead.headers.get('x-copyright-notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(pictureHead.headers.get('x-copyright-source'), null);
|
||||
const pictureRange = await fetch(`${baseUrl}${picture.url}`, { headers: { Range: 'bytes=0-7' } });
|
||||
assert.equal(pictureRange.status, 206);
|
||||
assert.equal(pictureRange.headers.get('x-copyright'), UPLOAD_COPYRIGHT_NOTICE);
|
||||
assert.equal(pictureRange.headers.get('x-copyright-notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(pictureRange.headers.get('x-copyright-source'), null);
|
||||
assert.deepEqual(Buffer.from(await pictureRange.arrayBuffer()), PNG_FILE.subarray(0, 8));
|
||||
const pictureCached = await fetch(`${baseUrl}${picture.url}`, { headers: { 'If-None-Match': pictureFileResponse.headers.get('etag'), 'Cache-Control': 'max-age=0' } });
|
||||
assert.equal(pictureCached.status, 304);
|
||||
assert.equal(pictureCached.headers.get('x-copyright'), UPLOAD_COPYRIGHT_NOTICE);
|
||||
assert.equal(pictureCached.headers.get('x-copyright-notice'), COPYRIGHT_HEADER_LIMITS);
|
||||
assert.equal(pictureCached.headers.get('x-copyright-source'), null);
|
||||
const wrongPicture = await fetch(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/999999`);
|
||||
assert.equal(wrongPicture.status, 404);
|
||||
assert.equal(wrongPicture.headers.get('x-copyright'), null);
|
||||
await wrongPicture.arrayBuffer();
|
||||
const metadataHeaders = await fetch(`${baseUrl}${picture.url}?metadata=true`);
|
||||
assert.equal(metadataHeaders.headers.get('x-copyright'), null);
|
||||
await metadataHeaders.arrayBuffer();
|
||||
const { body: pictureMetadata } = await requestJson(`${baseUrl}${picture.url}?metadata=true`);
|
||||
assert.equal(pictureMetadata.caption, 'Erste Bildbeschreibung');
|
||||
assert.equal(pictureMetadata.poiId, poi.id);
|
||||
|
||||
Reference in New Issue
Block a user