From 2a09ee77ab81114a673e34e118df09388933c4f0 Mon Sep 17 00:00:00 2001 From: a2nr Date: Sun, 19 Jul 2026 15:39:38 +0700 Subject: [PATCH] feat(embed): add raw HTML embed fence with bleach sanitization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add embed markdown fence: user pastes raw embed HTML code (from Canva/YouTube/Google Docs Share→Embed) into lesson markdown and slide content. Backend sanitizes via bleach (whitelist tags/attrs/styles) + checks iframe src against domain blacklist (SSRF prevention). Frontend renders iframe directly — no lazy action needed. - Backend: _process_embed_embeds + _sanitize_embed_html in lesson_service.py, applied to lesson_content, exercise, lesson_info, and slide loop. Graceful fallback if tinycss2 missing (CSS unsanitized but tags/attrs still stripped). - Tests: 9 pytest cases (Canva/YouTube HTML, script stripping, onclick stripping, blocked domain, non-https iframe, empty, unchanged, dangerous style). - Frontend: remove renderEmbedEmbeds.ts + wire-up + .generic-embed CSS (URL-only approach from earlier iteration, superseded). Keep .embed-error CSS for error messages. - Example: update test_slides.md with raw HTML Canva embed (slide) + YouTube embed (body). - Deps: bleach>=6.0.0, tinycss2>=1.2.0 in requirements.txt. - Docs: consolidate 4 plan files into docs/06-embed-content.md. --- docs/06-embed-content.md | 187 ++++++++++++++++++ examples/content/dasar/test_slides.md | 31 +++ frontend/src/app.css | 11 ++ .../src/routes/lesson/[slug]/+page.svelte | 2 + requirements.txt | 4 +- services/lesson_service.py | 88 +++++++++ services/tests/__init__.py | 0 services/tests/test_lesson_service_embed.py | 80 ++++++++ 8 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 docs/06-embed-content.md create mode 100644 services/tests/__init__.py create mode 100644 services/tests/test_lesson_service_embed.py diff --git a/docs/06-embed-content.md b/docs/06-embed-content.md new file mode 100644 index 0000000..40a3344 --- /dev/null +++ b/docs/06-embed-content.md @@ -0,0 +1,187 @@ +# Fitur Embed Konten di Materi Markdown & Slide + +**Tanggal:** 2026-07-19 +**Status:** Implementasi selesai +**Lokasi kode:** Backend `services/lesson_service.py` + Frontend (CSS only) + +--- + +## 1. Latar Belakang + +Elemes memerlukan fitur agar author materi dapat menyisipkan konten *embedded* (iframe dari platform luar) langsung dari markdown — baik di tubuh materi maupun di dalam slide presentasi. Contoh penggunaan: video YouTube, desain Canva, Google Docs, Figma, widget Scratch, dll. + +### Infrastruktur yang Sudah Ada + +Elemes **sudah punya** pipeline markdown→embed untuk `circuit` dan `flowchart`: +- **Backend** (`services/lesson_service.py`): regex fence ```circuit``` → `
`, lalu `md.markdown()` render jadi HTML. +- **Frontend** (`src/lib/actions/render*Embeds.ts`): `IntersectionObserver` ganti div → ` +
+Judul by Author +``` +```` + +**Kelebihan:** +- User kontrol penuh (aspect ratio, style, link credit) — embed code dari platform resmi sudah optimize. +- Support Canva, YouTube, Google Docs, Figma, Scratch, dll sekaligus — tanpa hardcode transform per-platform. +- Lebih fleksibel: author bisa kustomisasi wrapper, caption, dll. + +**Konsekuensi keamanan:** Raw HTML = potensi XSS. Wajib **sanitize** sebelum render. Tanpa sanitize, author bisa sisipkan ` +```""" + out = _process_embed_embeds(md) + # Script tags are stripped by bleach; inner text remains but is harmless + assert '' not in out.lower() + + +def test_embed_strips_onclick(): + md = """```embed +
+```""" + out = _process_embed_embeds(md) + assert 'onclick' not in out + assert 'alert' not in out + + +def test_embed_blocked_domain(): + md = """```embed + +```""" + out = _process_embed_embeds(md) + assert 'embed-error' in out + + +def test_embed_non_https_iframe(): + md = """```embed + +```""" + out = _process_embed_embeds(md) + assert 'embed-error' in out + + +def test_embed_empty_rejected(): + md = """```embed + +```""" + out = _process_embed_embeds(md) + assert 'embed-error' in out + assert 'kosong' in out + + +def test_embed_no_embed_unchanged(): + md = "# Heading\n\nparagraf biasa" + assert _process_embed_embeds(md) == md + + +def test_embed_youtube_html(): + md = """```embed + +```""" + out = _process_embed_embeds(md) + assert 'youtube.com' in out + assert ' +```""" + out = _process_embed_embeds(md) + assert 'javascript' not in out.lower()