Kevin Veen-Birkenbach efabbd3b2e fix(modal): stop untrusted content reaching innerHTML and the iframe
Every one of these paths checked a string that the browser reinterprets
afterwards. isSafeUrl now hangs the value on an <a> and reads back
probe.protocol, so the check sees what the browser will see: a pre-parse
test reads "&#106;avascript:" as a relative path and passes it, and the
HTML parser then decodes it to "javascript:".

marked passes raw HTML through and emits hrefs unescaped. renderMarkdown
escapes the angle brackets before parsing, parses into an inert DOMParser
document where no script runs and no image loads, and drops anchors and
images whose scheme is not http, https or mailto. Blockquotes and
<autolinks> stop working as a result; neither appears in the configuration.

modalTitle and the alternatives list interpolated subitem.name and
icon.class into innerHTML. Both are built as nodes now. name is a
translatable key, so it arrives from the machine-written catalogues.

The link kept its click handler and its class across popups, because one
anchor serves all of them: a later, unrelated click opened whatever an
earlier popup pointed at, and addEventListener stacked one handler per
open. Both are reset per popup and the handler is assigned, not added.

openIframe guards its own argument. Removing the href alone left the
handler passing the raw URL on, and ?iframe= in the query string reaches
the same sink with no configuration involved at all.

Verified in headless Chromium: decimal and hex character references,
&Tab;- and &NewLine;-split schemes, reference-style links, raw HTML as a
link's text, and the two name sinks all executed before these changes.
injection.spec.js keeps all fifteen payloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:18:29 +02:00
2026-05-18 12:26:26 +02:00
2020-10-08 15:06:16 +02:00
2026-03-30 10:46:39 +02:00

PortUI 🖥️

GitHub Sponsors Patreon Buy Me a Coffee PayPal

A lightweight, Docker-powered portfolio/landing-page generator—fully customizable via YAML! Showcase your projects, skills, and online presence in minutes.

PortUI screenshot

🚀 You can also pair PortUI with JavaScript for sleek, web-based desktop-style interfaces.
💻 Example in action: CyMaIS.Cloud (demo)
🌐 Another live example: veen.world (Kevins personal site)


Key Features

  • Dynamic Navigation
    Create dropdowns & nested menus with ease.
  • Customizable Cards
    Highlight skills, projects, or services—with icons, titles, and links.
  • Smart Cache Management
    Auto-cache assets for lightning-fast loading.
  • Responsive Design
    Built on Bootstrap; looks great on desktop, tablet & mobile.
  • 30 Languages
    Browser-negotiated, RTL-aware, with machine translation for your own content.
  • YAML-Driven
    All content & structure defined in a simple config.yaml.
  • CLI Control
    Manage Docker containers via the portfolio command.

🌐 Quick Access


🏁 Getting Started

🔧 Prerequisites

  • Docker & Docker Compose
  • Basic Python & YAML knowledge

🛠️ Installation via Git

  1. Clone & enter repo

    git clone <repository_url>
    cd <repository_directory>
    
  2. Configure Copy config.sample.yamlconfig.yaml & customize.

  3. Build & run

    docker-compose up --build
    
  4. Browse Open http://localhost:5000

📦 Installation via Kevins Package Manager

pkgmgr install portui

Once installed, the portui CLI is available system-wide.


🖥️ CLI Commands

portui --help
  • buildBuild the Docker image
  • upStart containers (with build)
  • downStop & remove containers
  • run-devDev mode (hot-reload)
  • run-prodProduction mode
  • logsView container logs
  • devDocker-Compose dev environment
  • prodDocker-Compose prod environment
  • cleanupPrune stopped containers

🔧 YAML Configuration Guide

Define your sites structure in config.yaml:

accounts:
  name: Online Accounts
  description: Discover my online presence.
  icon:
    class: fa-solid fa-users
  children:
    - name: Channels
      description: Platforms where I share content.
      icon:
        class: fas fa-newspaper
      children:
        - name: Mastodon
          description: Follow me on Mastodon.
          icon:
            class: fa-brands fa-mastodon
          url: https://microblog.veen.world/@kevinveenbirkenbach
          identifier: "@kevinveenbirkenbach@microblog.veen.world"
  cards:
    - icon:
        source: https://cloud.veen.world/s/logo_agile_coach_512x512/download
      title: Agile Coach
      text: I lead agile transformations and improve team dynamics through Scrum and Agile Coaching.
      url: https://www.agile-coach.world
      link_text: www.agile-coach.world

company:
  title: Kevin Veen-Birkenbach
  subtitle: Consulting & Coaching Solutions
  logo:
    source: https://cloud.veen.world/s/logo_face_512x512/download
  favicon:
    source: https://cloud.veen.world/s/veen_world_favicon/download
  address:
    street: Afrikanische Straße 43
    postal_code: DE-13351
    city: Berlin
    country: Germany
  imprint_url: https://s.veen.world/imprint
  • children enables multi-level menus.
  • link references other YAML paths to avoid duplication.

🌍 Languages

The interface ships in 30 languages. / serves the best match for the visitor's Accept-Language header, /<code>/ forces one, and a switcher in the navbar lists them all. Right-to-left languages (ar, fa, he, ur) get dir="rtl" and Bootstrap's RTL stylesheet automatically.

Translations live in two catalogues, both keyed by the English source string:

Path Tracked Holds
app/i18n/ui/<code>.yaml yes Interface strings, shipped complete for all 29 non-English languages. English is the source and has no file.
app/i18n/content/<code>.yaml no Your config.yaml prose, generated per deployment.

A string with no catalogue entry falls back to English, so a half-filled catalogue degrades instead of breaking.

Fill the content catalogues from a LibreTranslate instance — set LIBRETRANSLATE_URL in .env, then:

make i18n

Existing entries are never overwritten, so corrections you make by hand survive later runs. Only prose (description, text, warning, info, subtitel) is filled automatically; name and title are left to you, because a machine cannot tell the menu label "Pictures" from the brand "Mastodon". Write those into the content catalogue yourself when you want them translated.


🚢 Production Deployment

  • Use a reverse proxy (NGINX/Apache).
  • Secure with SSL/TLS.
  • Swap to a production database if needed.

Because every page carries a canonical URL and 30 hreflang alternates, two details of the proxy setup now matter:

  • Set TRUSTED_HOSTS in .env to your public hostname(s), comma-separated. Left empty, the app reflects whatever Host header arrives into its canonical, hreflang and redirect URLs — so a shared cache in front of it can be made to store a redirect pointing somewhere else.
  • Have the proxy send X-Forwarded-Proto. Without it the app cannot know TLS terminated upstream and every canonical URL claims http://. X-Forwarded-Host is deliberately not trusted; set Host to the public name instead.

📜 License

Licensed under GNU AGPLv3. See LICENSE for details.


✍️ Author

Created by Kevin Veen-Birkenbach

Enjoy building your portfolio! 🌟

Description
No description provided
Readme AGPL-3.0 5.3 MiB
Languages
Python 53.4%
JavaScript 33.5%
Jinja 5.9%
Makefile 3.3%
CSS 2.5%
Other 1.4%