Changeset a85f539 in Klonkt


Ignore:
Timestamp:
07/16/2026 12:20:51 PM (8 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
e276d03
Parents:
8e7f0ec
git-author:
Robin <roboburr@…> (07/12/2026 10:30:07 PM)
git-committer:
Robin <roboburr@…> (07/16/2026 12:20:51 PM)
Message:

Fix: bare .webm/.mp4/.mp3 URLs render a native player

autoembed() and [[embed:]] now detect direct media-file URLs and emit a
<video>/<audio> element (was: left as a plain link). Sanitizer allows
video/audio/source with a tight attr + http(s)-scheme allowlist so
hand-authored and federated-in players survive. detectProvider() is left
untouched so the timeline/cover callers that switch on provider slugs are
unaffected.

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

Location:
src/services
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/services/AudioEmbedService.js

    r8e7f0ec ra85f539  
    8282
    8383    return null;
     84  }
     85
     86  // Direct media files (video/audio) hosted anywhere → a native <video>/<audio>
     87  // player. Kept OUT of detectProvider() on purpose: the timeline/cover callers
     88  // switch on provider slugs (youtube/spotify/…) and a bare file has none, so
     89  // overloading detectProvider would suppress e.g. a PeerTube fallback. Only
     90  // autoembed() and [[embed:…]] use this.
     91  static MEDIA_FILE_EXT = {
     92    video: ['mp4', 'webm', 'm4v', 'mov', 'ogv'],
     93    audio: ['mp3', 'ogg', 'oga', 'wav', 'm4a', 'flac', 'opus', 'aac'],
     94  };
     95
     96  static detectMediaFile(url) {
     97    if (!url || typeof url !== 'string') return null;
     98    if (!/^https?:\/\//i.test(url)) return null;
     99    let pathname;
     100    try { pathname = new URL(url).pathname.toLowerCase(); } catch { return null; }
     101    const ext = (pathname.match(/\.([a-z0-9]+)$/) || [])[1];
     102    if (!ext) return null;
     103    if (this.MEDIA_FILE_EXT.video.includes(ext)) return { kind: 'video', url };
     104    if (this.MEDIA_FILE_EXT.audio.includes(ext)) return { kind: 'audio', url };
     105    return null;
     106  }
     107
     108  static mediaFileEmbed(url) {
     109    const m = this.detectMediaFile(url);
     110    if (!m) return null;
     111    const src = this.escape(m.url);
     112    if (m.kind === 'video') {
     113      return `<figure class="folio-embed folio-embed--video"><video src="${src}" controls preload="metadata" playsinline></video></figure>`;
     114    }
     115    return `<figure class="folio-embed folio-embed--audio"><audio src="${src}" controls preload="metadata"></audio></figure>`;
    84116  }
    85117
     
    238270          return iframe || match;
    239271        }
     272        // Bare media file (…/clip.webm, …/song.mp3) → native player.
     273        const media = this.mediaFileEmbed(url);
     274        if (media) return media;
    240275        return match;
    241276      }
     
    255290      const detected = this.detectProvider(url);
    256291      if (!detected) {
     292        // Bare media file (…/clip.webm, …/song.mp3) → native player.
     293        const media = this.mediaFileEmbed(url);
     294        if (media) return media;
    257295        return `<div class="post-embed-missing"><em>Embed: niet-ondersteunde of ongeldige URL.</em></div>`;
    258296      }
  • src/services/HtmlSanitizerService.js

    r8e7f0ec ra85f539  
    2525  'strong', 'em', 'b', 'i', 'u', 's', 'mark', 'small', 'sub', 'sup',
    2626  'code', 'a', 'span', 'img',
     27  // Native media (bare .webm/.mp4/.mp3 embeds + federated-in players)
     28  'video', 'audio', 'source',
    2729];
    2830
    2931// Per-tag attribute allowlist. '*' applies to every tag.
    3032const ALLOWED_ATTRS = {
    31   '*': ['class', 'id', 'dir', 'lang', 'data-sc'],
    32   a:   ['href', 'title', 'target', 'rel'],
    33   img: ['src', 'alt', 'title', 'width', 'height', 'loading'],
     33  '*':   ['class', 'id', 'dir', 'lang', 'data-sc'],
     34  a:     ['href', 'title', 'target', 'rel'],
     35  img:   ['src', 'alt', 'title', 'width', 'height', 'loading'],
     36  video: ['src', 'controls', 'preload', 'poster', 'width', 'height', 'loop', 'muted', 'autoplay', 'playsinline'],
     37  audio: ['src', 'controls', 'preload', 'loop', 'muted', 'autoplay'],
     38  source: ['src', 'type'],
    3439};
    3540
    3641const ALLOWED_SCHEMES = ['http', 'https', 'mailto', 'tel'];
    3742const ALLOWED_SCHEMES_BY_TAG = {
    38   img: ['http', 'https', 'data'],
    39   a:   ['http', 'https', 'mailto', 'tel'],
     43  img:    ['http', 'https', 'data'],
     44  a:      ['http', 'https', 'mailto', 'tel'],
     45  video:  ['http', 'https'],
     46  audio:  ['http', 'https'],
     47  source: ['http', 'https'],
    4048};
    4149
Note: See TracChangeset for help on using the changeset viewer.