Changeset 4c9f29a in Klonkt for src/services/AudioEmbedService.js


Ignore:
Timestamp:
06/15/2026 03:23:14 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
3e86f1c
Parents:
650b601
git-author:
roboburr <roboburr@…> (06/15/2026 03:22:47 AM)
git-committer:
roboburr <roboburr@…> (06/15/2026 03:23:14 AM)
Message:

feat: on-brand media embeds via the real player APIs

Replaces bare platform iframes with on-brand cards, powered by the
official JS APIs so play/pause/progress are in our own hands:

  • YouTube (IFrame Player API) + SoundCloud (Widget API): fully custom controls, native chrome hidden.
  • Spotify (iFrame API): our frame around it + controls (their player UI remains; restyling not possible without Premium+OAuth).
  • Shared PlaybackRegistry: mutual exclusion -- only 1 thing plays at a time (incl. the site audio player). Replaces the focus/blur heuristic with real play events (blur stays as fallback for iframe-only embeds).
  • Progressive enhancement: if an ad-blocker blocks the platform API, falls back seamlessly to the bare platform iframe (autoplay). The resting-state card is our brand for everyone.

AudioEmbedService now renders a placeholder div (data-embed-*) for YT/SC/
Spotify instead of an iframe; embed-player.js builds the card client-side.
CSP scriptSrc extended with the player API hosts.

Adversarial review (workflow) -> 6 bugs fixed: HTMX swap leak (poll timers/
adapters -> MutationObserver teardown + adapter.destroy()), javascript: URL XSS
(scheme guard in detectProvider + safeHref client-side), Spotify ended
misdetection (no more reset-to-0), ytId/server regex on exact 11, blur scope
limited to .folio-embed.

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/AudioEmbedService.js

    r650b601 r4c9f29a  
    1616    url = url.trim();
    1717
     18    // Alleen http(s)-URL's embedden. De provider-regexes hieronder zijn NIET
     19    // verankerd, dus zonder deze check zou bv. `javascript:alert(1)//youtu.be/x`
     20    // matchen en als embed-URL belanden (stored XSS via een [[embed:...]]-
     21    // shortcode — die tekst gaat niet langs de HTML-sanitizer omdat 'ie in een
     22    // text-node zit). De scheme-guard sluit javascript:/data:/vbscript: enz. uit.
     23    if (!/^https?:\/\//i.test(url)) return null;
     24
    1825    // Spotify
    1926    if (/open\.spotify\.com\/(track|album|playlist|episode|show)\/([A-Za-z0-9]+)/i.test(url)) {
    2027      const match = url.match(/\/(track|album|playlist|episode|show)\/([A-Za-z0-9]+)/i);
    21       return { provider: 'spotify', type: match[1], id: match[2] };
     28      return { provider: 'spotify', type: match[1], id: match[2], url };
    2229    }
    2330
     
    3744    }
    3845
    39     // YouTube
    40     if (/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{6,20})/i.test(url)) {
    41       const match = url.match(/(?:v=|youtu\.be\/|embed\/)([A-Za-z0-9_-]{6,20})/i);
    42       return { provider: 'youtube', id: match[1] };
     46    // YouTube — video-id is altijd exact 11 tekens (lijnt uit met de client-side
     47    // ytId() in embed-player.js, die ook {11} verwacht).
     48    if (/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/|youtube\.com\/live\/)([A-Za-z0-9_-]{11})/i.test(url)) {
     49      const match = url.match(/(?:v=|youtu\.be\/|embed\/|shorts\/|live\/)([A-Za-z0-9_-]{11})/i);
     50      return { provider: 'youtube', id: match[1], url };
    4351    }
    4452
     
    4654    if (/vimeo\.com\/(?:video\/)?(\d+)/i.test(url)) {
    4755      const match = url.match(/\d+/);
    48       return { provider: 'vimeo', id: match[0] };
     56      return { provider: 'vimeo', id: match[0], url };
    4957    }
    5058
     
    5462  static generateIframe(provider, config) {
    5563    switch (provider) {
     64      // Eigen custom-spelers (client-side via embed-player.js + de echte
     65      // platform-API's). We renderen een placeholder met data-attributen i.p.v.
     66      // het kale platform-iframe, zodat de embed in ÓNZE huisstijl verschijnt.
     67      case 'youtube':
     68        return this.embedPlaceholder('youtube', config.id, 'video',
     69          config.url || `https://youtu.be/${config.id}`);
     70      case 'soundcloud':
     71        return this.embedPlaceholder('soundcloud', config.url, 'track', config.url);
    5672      case 'spotify':
    57         return this.spotifyIframe(config);
     73        return this.embedPlaceholder('spotify', `spotify:${config.type}:${config.id}`,
     74          config.type, config.url || `https://open.spotify.com/${config.type}/${config.id}`);
     75      // Geen JS-API (Bandcamp/Apple) of niet-prioritair (Vimeo): blijven een
     76      // iframe; mutual-exclusion loopt voor deze via de blur-fallback.
    5877      case 'bandcamp':
    5978        return this.bandcampIframe(config);
    60       case 'soundcloud':
    61         return this.soundcloudIframe(config);
    6279      case 'applemusic':
    6380        return this.applemusicIframe(config);
    64       case 'youtube':
    65         return this.youtubeIframe(config);
    6681      case 'vimeo':
    6782        return this.vimeoIframe(config);
     
    6984        return null;
    7085    }
     86  }
     87
     88  /**
     89   * Placeholder voor een eigen custom-speler. embed-player.js pikt
     90   * .folio-embed[data-embed-provider] op en bouwt de kaart + speler client-side.
     91   * ALLE waarden via escape() — post.content_html wordt ongeescaped uitgevoerd.
     92   */
     93  static embedPlaceholder(provider, ref, type, url) {
     94    const attrs = [
     95      `data-embed-provider="${this.escape(provider)}"`,
     96      `data-embed-ref="${this.escape(ref)}"`,
     97      type ? `data-embed-type="${this.escape(type)}"` : '',
     98      `data-embed-url="${this.escape(url)}"`,
     99    ].filter(Boolean).join(' ');
     100    return `<div class="folio-embed folio-embed--${this.escape(provider)} pcms-embed pcms-embed-card pcms-embed-loading" ${attrs}></div>`;
    71101  }
    72102
Note: See TracChangeset for help on using the changeset viewer.