diff --git a/gdpr-content-blocker/gdpr-content-blocker.php b/gdpr-content-blocker/gdpr-content-blocker.php index 9c2a8db..5f03471 100644 --- a/gdpr-content-blocker/gdpr-content-blocker.php +++ b/gdpr-content-blocker/gdpr-content-blocker.php @@ -3,7 +3,7 @@ * Plugin Name: GDPR Content Blocker * Plugin URI: https://lucas-orth.de/wp-plugins/gdpr-content-blocker/ * Description: DSGVO-konformer Consent-Blocker für externe iframes. Lädt Drittinhalte erst nach aktiver Einwilligung. - * Version: 1.5.4 + * Version: 1.6 * Author: Lucas Orth * Author URI: https://lucas-orth.de * Text Domain: gdpr-content-blocker diff --git a/license-backend/src/server.js b/license-backend/src/server.js index c7c3557..782e96a 100644 --- a/license-backend/src/server.js +++ b/license-backend/src/server.js @@ -632,6 +632,25 @@ app.get('/api/v1/releases/:product', adminOnly, (req, res) => { return res.json({ ok: true, product: product.slug, releases: rows }); }); +// Download a release ZIP directly with the admin token (no license needed) — +// handy for n8n/CI. Defaults to the latest version; ?version=1.2.3 for a +// specific one. Returns the raw application/zip stream. +app.get('/api/v1/releases/:product/download', adminOnly, (req, res) => { + const product = Q.productBySlug.get(req.params.product); + if (!product) return fail(res, 404, 'unknown product'); + + const version = String(req.query.version || '').trim(); + const rel = version ? Q.releaseByVersion.get(product.id, version) : latestRelease(product.id); + if (!rel) return fail(res, 404, version ? 'version not found' : 'no release yet'); + if (!existsSync(rel.zip_path)) return fail(res, 404, 'release file missing on disk'); + + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Length', statSync(rel.zip_path).size); + res.setHeader('X-Release-Version', rel.version); + res.setHeader('Content-Disposition', `attachment; filename="${product.slug}-${rel.version}.zip"`); + createReadStream(rel.zip_path).pipe(res); +}); + // Generate a license key. This is the endpoint the n8n workflow calls. diff --git a/license-backend/test/integration.mjs b/license-backend/test/integration.mjs index 155e1e1..ad9c411 100644 --- a/license-backend/test/integration.mjs +++ b/license-backend/test/integration.mjs @@ -214,6 +214,17 @@ try { const relList = await admin('GET', '/api/v1/releases/gdpr-content-blocker'); ok('release list shows 1.1.0', relList.json.ok && relList.json.releases.some((r) => r.version === '1.1.0'), JSON.stringify(relList.json)); + // Admin direct download (for n8n/CI) — latest version, no license needed + const dlA = await fetch(BASE + '/api/v1/releases/gdpr-content-blocker/download', { headers: { 'X-Admin-Token': TOKEN } }); + const dlABuf = Buffer.from(await dlA.arrayBuffer()); + ok('admin download latest 200 zip', dlA.status === 200 && dlA.headers.get('content-type') === 'application/zip', String(dlA.status)); + ok('admin download version header = 1.1.0', dlA.headers.get('x-release-version') === '1.1.0'); + ok('admin download is a ZIP', dlABuf[0] === 0x50 && dlABuf[1] === 0x4b); + const dlNoAuth = await fetch(BASE + '/api/v1/releases/gdpr-content-blocker/download'); + ok('admin download without token → 401', dlNoAuth.status === 401); + const dlBadVer = await admin('GET', '/api/v1/releases/gdpr-content-blocker/download?version=9.9.9'); + ok('admin download unknown version → 404', dlBadVer.status === 404); + // ── Release from URL: guards (real fetch not exercised offline) ── const fuProd = await admin('POST', '/api/v1/releases/from-url', { product: 'nope', version: '1.2.0', zip_url: 'https://example.com/a.zip' }); ok('from-url unknown product → 404', fuProd.status === 404, JSON.stringify(fuProd.json));