<?php
/**
 * CSP entry point for Assets Hub.
 *
 * Sets Content-Security-Policy headers based on the environment
 * (local, review, or production) and serves index.html.
 *
 * Local env uses 'unsafe-inline' for dev convenience.
 * Dynamic/production envs use a per-request nonce for inline scripts/styles.
 */

$host = $_SERVER['HTTP_HOST'] ?? '';

// --- Per-request nonce (used in review & production) ---
$nonce = base64_encode(random_bytes(16));

// --- CSP directive values ---
// 'wasm-unsafe-eval' is required by the IFC loader of the 3D viewer (web-ifc compiles a WASM
// module). It only permits WebAssembly compilation, not JS eval.
$scriptSrc        = "script-src 'self' 'nonce-{$nonce}' 'wasm-unsafe-eval'";

// HubSpot embed (FRA-10863) — banners, CTAs, and pop-up forms, loaded at runtime behind
// `hubspot-banner-enabled`. Per-origin rationale: docs/csp.md.
// HubSpot reference: https://knowledge.hubspot.com/domains-and-urls/ssl-and-domain-security-in-hubspot
//
// Scope: tracking, banners/CTAs, forms, and ad pixels.
// Chat, surveys, and video are excluded and require CSP updates if enabled.
// hscollectedforms and hs-banner are required by the base loader.
//
// Banner images use both numbered f.hubspotusercontentNN.net and regional
// <hubId>.hubspotusercontent-eu1.net origins, so both are allowlisted.
$hubspotUserContent = "https://*.hubspotusercontent-eu1.net https://*.hubspotusercontent00.net https://*.hubspotusercontent10.net https://*.hubspotusercontent20.net https://*.hubspotusercontent30.net https://*.hubspotusercontent40.net";

// HubSpot-installed ad pixels ship with the tracking code and run wherever it does.
// Grouped in $hubspotAds* so the ad surface can be reverted as one change.
// Installed pixel: Google Ads (AW-378391257), loaded by HubSpot via Tag Manager.
//
// ACCEPTED RISK — www.googletagmanager.com in script-src-elem can load an arbitrary GTM
// container. This is required for in-app remarketing; removing remarketing removes this risk.
// See docs/csp.md.
//
// Consent must match HUBSPOT_CONSENT in src/features/hubspot/hubspot.constants.ts,
// which sets advertisement: true before the loader starts.
//
// Used in script-src-elem (loader), connect-src (.js.map), and img-src (blank001.gif).
// The GIF is HubSpot's tracker, not Google's.
$hubspotAdsLoaderHost = "https://static.hsappstatic.net";

$hubspotAdsScriptSrc  = "https://*.hsadspixel.net {$hubspotAdsLoaderHost} https://www.googletagmanager.com https://www.googleadservices.com https://pagead2.googlesyndication.com https://googleads.g.doubleclick.net https://www.google.com";
$hubspotAdsFrameSrc   = "https://www.googletagmanager.com";
$hubspotAdsConnectSrc = "{$hubspotAdsLoaderHost} https://pagead2.googlesyndication.com https://www.googleadservices.com https://googleads.g.doubleclick.net https://ad.doubleclick.net https://www.google.com";

// Remarketing beacons redirect to country-specific Google TLDs, so each TLD must be allowlisted.
//
// Intentionally trimmed to stay below Fastly/Varnish's 8192 B response-header limit.
// The 69 supported markets keep the CSP at ~5.7 KB; re-measure before adding more.
// Unlisted markets lose the beacon and appear as img-src violations in CSP reports.
//
// img-src only: the redirect applies to image beacons; other Google requests use www.google.com.
$googleAdsTlds = [
    // Global / US
    'com',
    // EU 27
    'at', 'be', 'bg', 'hr', 'com.cy', 'cz', 'dk', 'ee', 'fi', 'fr', 'de', 'gr', 'hu', 'ie', 'it', 'lv',
    'lt', 'lu', 'com.mt', 'nl', 'pl', 'pt', 'ro', 'sk', 'si', 'es', 'se',
    // EFTA + UK
    'ch', 'no', 'is', 'li', 'co.uk',
    // Other Europe
    'com.ua', 'rs', 'ba', 'mk', 'me', 'md', 'al',
    // North America
    'ca', 'com.mx',
    // LATAM
    'com.br', 'com.ar', 'cl', 'com.co', 'com.pe',
    // APAC
    'com.au', 'co.nz', 'co.jp', 'co.kr', 'com.sg', 'com.hk', 'co.in', 'com.my', 'com.ph', 'co.th',
    'com.vn', 'co.id', 'com.tw',
    // MEA
    'ae', 'com.sa', 'co.il', 'com.tr', 'com.eg', 'co.za', 'com.ng', 'co.ke', 'co.ma',
];
// Closure form, not fn() — the deploy target's PHP version is not pinned and arrow fns need 7.4+.
$googleAdsTldSrc = implode(' ', array_map(function ($tld) {
    return "https://www.google.{$tld}";
}, $googleAdsTlds));

// 'com' is already in $googleAdsTlds, so www.google.com is not repeated here.
$hubspotAdsImgSrc = "{$hubspotAdsLoaderHost} https://www.googletagmanager.com https://googleads.g.doubleclick.net https://www.googleadservices.com https://pagead2.googlesyndication.com {$googleAdsTldSrc}";

$hubspotScriptSrc  = "https://*.hs-scripts.com https://*.hs-analytics.net https://*.hs-banner.com https://*.hscollectedforms.net https://*.hsleadflows.net https://*.hsforms.net https://*.hsforms.com https://*.hscta.net https://*.hubspot.com {$hubspotAdsScriptSrc}";
$hubspotConnectSrc = "https://*.hubapi.com https://*.hubspot.com https://*.hs-banner.com https://*.hscollectedforms.net https://*.hsforms.com https://*.hscta.net https://*.hs-analytics.net {$hubspotAdsConnectSrc}";
$hubspotImgSrc     = "https://*.hubspot.com https://*.hubspot.net https://*.hsforms.com https://*.hscta.net https://cdn2.hubspot.net {$hubspotUserContent} {$hubspotAdsImgSrc}";
$hubspotFrameSrc   = "https://*.hubspot.com https://*.hs-sites.com https://*.hs-sites-eu1.com https://*.hsforms.com https://*.hsforms.net {$hubspotAdsFrameSrc}";
$hubspotStyleSrc   = "https://*.hubspot.com https://cdn2.hubspot.net {$hubspotUserContent}";
$hubspotFontSrc    = "https://*.hubspot.com https://*.hsforms.net";

// Our own GTM snippet (vite.transformHtmlPlugin.js) is gated on
// REACT_APP_INSERT_GOOGLE_TAG_MANAGER_SCRIPT, false in every env file — but the ad pixel above already
// allowlists googletagmanager.com in script-src-elem and frame-src, so re-enabling it would silently
// start working.
$scriptSrcElem    = "script-src-elem 'self' 'nonce-{$nonce}' https://*.cloudimage.io https://*.cloudimg.io https://cdn.air360tracker.net https://hnh35r7q8vyd.statuspage.io https://*.scaleflex.cloudimg.io https://*.scaleflex.com https://*.scal3fl3x.com https://*.scaleflex.it https://*.stripe.com https://sdk.canva.com https://*.dashmax.io https://accounts.google.com https://apis.google.com {$hubspotScriptSrc}";
$styleSrc         = "style-src 'self' 'nonce-{$nonce}'";
$styleSrcElem     = "style-src-elem 'self' 'nonce-{$nonce}' https://*.cloudimage.io https://*.cloudimg.io https://*.scaleflex.cloudimg.io https://fonts.googleapis.com https://*.canva.com https://*.dashmax.io {$hubspotStyleSrc}";
$fontSrc          = "font-src 'self' data: https://fonts.gstatic.com https://*.scaleflex.it https://*.canva.com https://*.dashmax.io {$hubspotFontSrc}";
$imgSrc           = "img-src 'self' data: blob: https://*.cloudimage.io https://*.cloudimg.io https://*.scaleflex.com https://*.scal3fl3x.com https://*.scaleflex.cloudimg.io https://*.filerobot.com https://*.airstore.io https://*.stripe.com https://*.scaleflex.it https://*.canva.com {$hubspotImgSrc}";
$connectSrc       = "connect-src 'self' data: https://neo.wordplex.io https://*.cloudimage.io https://*.cloudimage.com https://*.cloudimg.io https://*.filerobot.com wss://*.filerobot.com https://*.scaleflex.com https://*.scal3fl3x.com wss://*.scal3fl3x.com https://*.ultrafast.io https://*.air360tracker.net https://*.ingest.sentry.io https://*.ingest.us.sentry.io wss://*.statuspage.io https://ifconfig.rest https://scaleflex.zendesk.com https://api.phonecheck.rest https://report.centralcsp.com https://*.scaleflex.cloud https://*.sfxconnector.com https://qualitycontrol.scaleflexhq.com https://*.canva.com https://*.dashmax.io wss://*.dashmax.io https://*.googleapis.com {$hubspotConnectSrc}";
$mediaSrc         = "media-src 'self' blob: https://*.filerobot.com https://*.cloudimg.io https://*.scaleflex.cloudimg.io";
$frameSrc         = "frame-src 'self' https://app.dataflake.co https://*.statuspage.io https://*.stripe.com https://*.sfxconnector.com https://*.scaleflex.com https://*.scal3fl3x.com https://calendly.com https://*.canva.com https://*.live.com https://*.dashmax.io https://*.filerobot.com https://*.cloudimg.io https://accounts.google.com https://docs.google.com {$hubspotFrameSrc}";
$workerSrc        = "worker-src 'self'";
$objectSrc        = "object-src 'none'";
$baseUri          = "base-uri 'self'";
$formAction       = "form-action 'self'";
$frameAncestors   = "frame-ancestors 'none'";
$reportUri        = "report-uri https://report.centralcsp.com/69928e4dc262ad153c791372";
$reportTo         = "report-to csp-endpoint";

// --- Environment detection ---
$isLocal   = preg_match('/^(localhost|127\.0\.0\.1)(:\d+)?$/', $host);
$isDynamic = preg_match('/\.hub-review-dev\.filerobot\.com$/', $host) || preg_match('/\.hub-review-dev\.scaleflex\.com$/', $host);

// Dynamic envs that need 'unsafe-inline' (e.g. client-injected inline scripts/styles).
// 'unsafe-inline' is ignored by browsers when a nonce is present in the same
// directive, so the nonce is replaced rather than appended.
$unsafeInlineHosts = [
    'fra-10324-custom-analytics.hub-review-dev.scaleflex.com',
    'adeo.hub-review-dev.scaleflex.com',
    'adeo.hub.scaleflex.com',
];

if (in_array($host, $unsafeInlineHosts, true)) {
    $scriptSrc     = str_replace("'nonce-{$nonce}'", "'unsafe-inline' 'unsafe-eval'", $scriptSrc);
    $scriptSrcElem = str_replace("'nonce-{$nonce}'", "'unsafe-inline'", $scriptSrcElem);
    $styleSrc      = str_replace("'nonce-{$nonce}'", "'unsafe-inline'", $styleSrc);
    $styleSrcElem  = str_replace("'nonce-{$nonce}'", "'unsafe-inline'", $styleSrcElem);
}

if ($isLocal) {
    // Localhost: report-only, relaxed for dev tooling (unsafe-inline, unsafe-eval, ws/wss)
    $localScriptSrc     = "script-src 'self' 'unsafe-inline' 'unsafe-eval'";
    $localScriptSrcElem = "script-src-elem 'self' 'unsafe-inline' https://*.cloudimage.io https://*.cloudimg.io https://cdn.air360tracker.net https://hnh35r7q8vyd.statuspage.io https://*.scaleflex.cloudimg.io https://*.scaleflex.com https://*.scal3fl3x.com https://*.scaleflex.it https://*.stripe.com https://sdk.canva.com https://accounts.google.com https://apis.google.com {$hubspotScriptSrc}";
    $localStyleSrc      = "style-src 'self' 'unsafe-inline'";
    $localStyleSrcElem  = "style-src-elem 'self' 'unsafe-inline' https://*.cloudimage.io https://*.cloudimg.io https://*.scaleflex.cloudimg.io https://fonts.googleapis.com https://*.canva.com https://*.dashmax.io {$hubspotStyleSrc}";
    $localConnectSrc    = "connect-src 'self' data: ws: wss: https://neo.wordplex.io https://*.cloudimage.io https://*.cloudimage.com https://*.cloudimg.io https://*.filerobot.com https://*.scaleflex.com https://*.scal3fl3x.com https://*.ultrafast.io https://*.air360tracker.net https://*.ingest.sentry.io https://*.ingest.us.sentry.io wss://*.statuspage.io https://ifconfig.rest https://scaleflex.zendesk.com https://api.phonecheck.rest https://report.centralcsp.com https://*.scaleflex.cloud https://*.sfxconnector.com https://qualitycontrol.scaleflexhq.com https://*.canva.com https://*.dashmax.io https://*.googleapis.com {$hubspotConnectSrc}";

    $csp = "default-src 'self'; {$localScriptSrc}; {$localScriptSrcElem}; {$localStyleSrc}; {$localStyleSrcElem}; {$fontSrc}; {$imgSrc}; {$localConnectSrc}; {$mediaSrc}; {$frameSrc}; {$workerSrc}; {$objectSrc}; {$baseUri}; {$formAction}; {$frameAncestors}";
    header("Content-Security-Policy-Report-Only: {$csp}");

} elseif ($isDynamic) {
    // Review/dynamic env: enforced with nonce
    $csp = "default-src 'self'; {$scriptSrc}; {$scriptSrcElem}; {$styleSrc}; {$styleSrcElem}; {$fontSrc}; {$imgSrc}; {$connectSrc}; {$mediaSrc}; {$frameSrc}; {$workerSrc}; {$objectSrc}; {$baseUri}; {$formAction}; {$frameAncestors}; upgrade-insecure-requests; {$reportUri}; {$reportTo}";
    header("Content-Security-Policy: {$csp}");

} else {
    // Production: enforced with nonce
    $csp = "default-src 'self'; {$scriptSrc}; {$scriptSrcElem}; {$styleSrc}; {$styleSrcElem}; {$fontSrc}; {$imgSrc}; {$connectSrc}; {$mediaSrc}; {$frameSrc}; {$workerSrc}; {$objectSrc}; {$baseUri}; {$formAction}; {$frameAncestors}; upgrade-insecure-requests; {$reportUri}; {$reportTo}";
    header("Content-Security-Policy: {$csp}");
}

// --- Serve index.html with nonce injection ---
header('Content-Type: text/html; charset=UTF-8');

$htmlPath = __DIR__ . '/index.html';
if (!file_exists($htmlPath)) {
    http_response_code(500);
    exit('index.html not found');
}

$html = file_get_contents($htmlPath);

// Replace Vite's CSP_NONCE_PLACEHOLDER with the real nonce.
// This covers Vite-generated script/link/style tags AND the
// <meta property="csp-nonce"> tag that Vite injects for runtime CSS injection.
$html = str_replace('CSP_NONCE_PLACEHOLDER', $nonce, $html);

// Add nonce to any remaining <script> tags that don't have one
// (e.g. custom inline scripts from vite.transformHtmlPlugin.js)
$html = preg_replace('/<script(?![^>]*\bnonce=)/', '<script nonce="' . $nonce . '"', $html);

// Add nonce to any remaining <style> tags that don't have one
$html = preg_replace('/<style(?![^>]*\bnonce=)/', '<style nonce="' . $nonce . '"', $html);

echo $html;
