mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-09-23 19:03:18 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d386160237 | |||
| 7de3c74f3c | |||
| 7353e8d96a | |||
| 4cc8052a88 | |||
| 87c4405aa4 | |||
| ff6e7d3689 | |||
| 4eb57011ba | |||
| 70d5871e94 | |||
| 10ea289563 | |||
| 62a2aa5827 | |||
| b0c8316b0d | |||
|
|
d5104f5680 | ||
| fc646d8826 | |||
| 1bff38ed64 | |||
| dd0454274b | |||
| fd4d1e77c8 | |||
| d1043c4cb0 | |||
|
|
62a6a8cb66 | ||
| 4d1092fe5e | |||
| 2ed4164dc3 | |||
| 85e1f437e0 | |||
|
|
0733af88ba | ||
| 31bed38478 | |||
| 3f75ebd327 | |||
| 169891fcf0 | |||
|
|
578d84de0c | ||
| 20b36e518d | |||
| d3a37f2268 | |||
|
|
fe6c2a0f11 | ||
|
|
21f24a3c0d | ||
|
|
20566ca0c4 | ||
|
|
f998ceb298 | ||
|
|
abc17e8b1e | ||
|
|
c188a6089d | ||
|
|
b76bbba4a3 | ||
|
|
362ca6f499 | ||
|
|
13e0331460 | ||
|
|
5d39720d54 | ||
| 49a0d2fe6d |
47
.github/dependabot.yml
vendored
Normal file
47
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
time: "00:00"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "ci"
|
||||||
|
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
time: "00:00"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "docker"
|
||||||
|
|
||||||
|
- package-ecosystem: "docker-compose"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
time: "00:00"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "docker"
|
||||||
|
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
time: "00:00"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "python"
|
||||||
|
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/app"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
time: "00:00"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "javascript"
|
||||||
30
.github/workflows/cancel.yml
vendored
Normal file
30
.github/workflows/cancel.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Cancel runs
|
||||||
|
|
||||||
|
on:
|
||||||
|
delete:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cancel:
|
||||||
|
name: Cancel the runs of a closed branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request' || github.event.ref_type == 'branch'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Cancel every queued or running workflow of the branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.event.ref }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gh run list --branch "${BRANCH}" --limit 100 \
|
||||||
|
--json databaseId,status \
|
||||||
|
--jq '.[] | select(.status != "completed") | .databaseId' \
|
||||||
|
| sed "/^${GITHUB_RUN_ID}$/d" \
|
||||||
|
| xargs -r -n1 gh run cancel
|
||||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -8,6 +8,10 @@ on:
|
|||||||
tags-ignore:
|
tags-ignore:
|
||||||
- "**"
|
- "**"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.repository }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -89,10 +93,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
@@ -102,7 +106,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and publish image
|
- name: Build and publish image
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -122,7 +126,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
|
|||||||
20
.github/workflows/lint.yml
vendored
20
.github/workflows/lint.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Run actionlint
|
- name: Run actionlint
|
||||||
run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest
|
run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest
|
||||||
@@ -25,10 +25,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -46,12 +46,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "24"
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: app/package.json
|
cache-dependency-path: app/package.json
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Run shellcheck
|
- name: Run shellcheck
|
||||||
run: docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:stable scripts/*.sh
|
run: docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:stable scripts/*.sh
|
||||||
@@ -80,10 +80,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -107,12 +107,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Run hadolint
|
- name: Run hadolint
|
||||||
id: hadolint
|
id: hadolint
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5
|
uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387
|
||||||
with:
|
with:
|
||||||
dockerfile: ./Dockerfile
|
dockerfile: ./Dockerfile
|
||||||
format: sarif
|
format: sarif
|
||||||
|
|||||||
2
.github/workflows/security.yml
vendored
2
.github/workflows/security.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@v4
|
||||||
|
|||||||
28
.github/workflows/tests.yml
vendored
28
.github/workflows/tests.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -56,10 +56,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -77,10 +77,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -104,10 +104,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -135,10 +135,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "24"
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: app/package.json
|
cache-dependency-path: app/package.json
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ jobs:
|
|||||||
xvfb
|
xvfb
|
||||||
|
|
||||||
- name: Run Cypress tests
|
- name: Run Cypress tests
|
||||||
uses: cypress-io/github-action@f790eee7a50d9505912f50c2095510be7de06aa7 # v6.10.9
|
uses: cypress-io/github-action@01e3b659a495b41649cdd0aa82e1d1624e26520b # v7.4.4
|
||||||
with:
|
with:
|
||||||
working-directory: app
|
working-directory: app
|
||||||
install: false
|
install: false
|
||||||
|
|||||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,5 +1,24 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [2.2.2] - 2026-09-23
|
||||||
|
|
||||||
|
* Vendor assets: marked 18 is found again, so the image build stops failing
|
||||||
|
* Iframe: a page without an iframe parameter no longer frames itself
|
||||||
|
* Security: the iframe and the new tab only open a URL the validator returned
|
||||||
|
* Tests: the inline-script check parses the page instead of matching a regex
|
||||||
|
* Tests: a regression spec guards the page that carries no iframe parameter
|
||||||
|
* CI: the Node jobs run the version Cypress 16 supports
|
||||||
|
|
||||||
|
## [2.2.1] - 2026-09-23
|
||||||
|
|
||||||
|
* Images: releases ship arm64 next to amd64, for ARM servers and Apple Silicon
|
||||||
|
* CI: each architecture builds on its own native runner, no QEMU emulation
|
||||||
|
* CI: one run per branch, so two pushes stop racing through the same jobs
|
||||||
|
* CI: closing a pull request or deleting a branch cancels its pending runs
|
||||||
|
* Dependencies: Dependabot watches actions, images, compose, pip and npm daily
|
||||||
|
* Runtime: Python 3.14 and Node 25 base images
|
||||||
|
* Frontend: Bootstrap 5.3.8, jQuery 4, Font Awesome 7, marked 18, Cypress 16
|
||||||
|
|
||||||
## [2.2.0] - 2026-09-23
|
## [2.2.0] - 2026-09-23
|
||||||
|
|
||||||
* Images: releases ship arm64 next to amd64, for ARM servers and Apple Silicon
|
* Images: releases ship arm64 next to amd64, for ARM servers and Apple Silicon
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
FROM node:22-slim AS assets
|
FROM node:25-slim AS assets
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY app/package.json ./
|
COPY app/package.json ./
|
||||||
COPY app/scripts ./scripts
|
COPY app/scripts ./scripts
|
||||||
RUN npm install --omit=dev --no-audit --no-fund
|
RUN npm install --omit=dev --no-audit --no-fund
|
||||||
|
|
||||||
FROM python:3.12-slim AS base
|
FROM python:3.14-slim AS base
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
|
|||||||
31
app/cypress/e2e/iframe_param_absent.spec.js
Normal file
31
app/cypress/e2e/iframe_param_absent.spec.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
describe('A page without an iframe parameter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stays out of fullscreen', () => {
|
||||||
|
cy.get('body').should('not.have.class', 'fullscreen');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('frames nothing', () => {
|
||||||
|
cy.get('#main').find('iframe').should('not.exist');
|
||||||
|
cy.url().should('not.include', 'iframe=');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never grows an iframe parameter out of its own URL', () => {
|
||||||
|
cy.wait(2000);
|
||||||
|
|
||||||
|
cy.url().then((url) => {
|
||||||
|
expect((url.match(/iframe/g) || []).length, 'iframe parameters').to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats an absent URL as unsafe', () => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
expect(win.safeUrl(null), 'null').to.equal(null);
|
||||||
|
expect(win.safeUrl(''), 'empty string').to.equal(null);
|
||||||
|
expect(win.safeUrl('javascript:alert(1)'), 'script URL').to.equal(null);
|
||||||
|
expect(win.safeUrl('/de/'), 'relative path').to.equal(`${win.location.origin}/de/`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ const SHARED = {
|
|||||||
openDynamicPopup: 'readonly',
|
openDynamicPopup: 'readonly',
|
||||||
closeAllModals: 'readonly',
|
closeAllModals: 'readonly',
|
||||||
isSafeUrl: 'readonly',
|
isSafeUrl: 'readonly',
|
||||||
|
safeUrl: 'readonly',
|
||||||
openIframe: 'readonly',
|
openIframe: 'readonly',
|
||||||
enterFullscreen: 'readonly',
|
enterFullscreen: 'readonly',
|
||||||
exitFullscreen: 'readonly',
|
exitFullscreen: 'readonly',
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"bootstrap": "5.3.8",
|
||||||
"bootstrap": "5.2.2",
|
"@fortawesome/fontawesome-free": "^7.3.1",
|
||||||
"bootstrap-icons": "1.9.1",
|
"bootstrap-icons": "1.13.1",
|
||||||
"jquery": "3.6.0",
|
"jquery": "4.0.0",
|
||||||
"marked": "^4.3.0"
|
"marked": "^18.0.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"cypress": "^14.5.1",
|
"cypress": "^16.1.0",
|
||||||
"eslint": "^10.9.0",
|
"eslint": "^10.9.0",
|
||||||
"globals": "^17.11.0"
|
"globals": "^17.11.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const markedCandidates = [
|
|||||||
path.join(NM, 'marked', 'marked.min.js'), // v4.x
|
path.join(NM, 'marked', 'marked.min.js'), // v4.x
|
||||||
path.join(NM, 'marked', 'lib', 'marked.umd.min.js'), // v5.x
|
path.join(NM, 'marked', 'lib', 'marked.umd.min.js'), // v5.x
|
||||||
path.join(NM, 'marked', 'dist', 'marked.min.js'), // v9+
|
path.join(NM, 'marked', 'dist', 'marked.min.js'), // v9+
|
||||||
|
path.join(NM, 'marked', 'lib', 'marked.umd.js'), // v16+
|
||||||
];
|
];
|
||||||
const markedSrc = markedCandidates.find(p => fs.existsSync(p));
|
const markedSrc = markedCandidates.find(p => fs.existsSync(p));
|
||||||
if (!markedSrc) throw new Error('marked: no browser UMD build found in node_modules');
|
if (!markedSrc) throw new Error('marked: no browser UMD build found in node_modules');
|
||||||
|
|||||||
@@ -2,19 +2,20 @@
|
|||||||
let mainElement, originalContent, originalMainStyle, container, customScrollbar, scrollbarContainer;
|
let mainElement, originalContent, originalMainStyle, container, customScrollbar, scrollbarContainer;
|
||||||
let currentIframeUrl = null;
|
let currentIframeUrl = null;
|
||||||
|
|
||||||
function isAllowedIframeUrl(url) {
|
function allowedIframeUrl(url) {
|
||||||
if (!isSafeUrl(url)) {
|
const candidate = safeUrl(url);
|
||||||
return false;
|
if (candidate === null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
const allowedOrigins = new Set([window.location.origin]);
|
const allowedOrigins = new Set([window.location.origin]);
|
||||||
document.querySelectorAll('a.iframe-link[href]').forEach((link) => allowedOrigins.add(link.origin));
|
document.querySelectorAll('a.iframe-link[href]').forEach((link) => allowedOrigins.add(link.origin));
|
||||||
return allowedOrigins.has(new URL(url, window.location.href).origin);
|
return allowedOrigins.has(new URL(candidate).origin) ? candidate : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Auto-open iframe if URL parameter is present ===
|
// === Auto-open iframe if URL parameter is present ===
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
const paramUrl = new URLSearchParams(window.location.search).get('iframe');
|
const paramUrl = new URLSearchParams(window.location.search).get('iframe');
|
||||||
if (paramUrl && isAllowedIframeUrl(paramUrl)) {
|
if (paramUrl && allowedIframeUrl(paramUrl)) {
|
||||||
currentIframeUrl = paramUrl;
|
currentIframeUrl = paramUrl;
|
||||||
enterFullscreen();
|
enterFullscreen();
|
||||||
openIframe(paramUrl);
|
openIframe(paramUrl);
|
||||||
@@ -43,7 +44,8 @@ function syncIframeHeight() {
|
|||||||
|
|
||||||
// Function to open a URL in an iframe (jQuery version mit 1500 ms Fade)
|
// Function to open a URL in an iframe (jQuery version mit 1500 ms Fade)
|
||||||
function openIframe(url) {
|
function openIframe(url) {
|
||||||
if (!isSafeUrl(url)) {
|
const target = safeUrl(url);
|
||||||
|
if (target === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ function openIframe(url) {
|
|||||||
|
|
||||||
// Quelle setzen und mit 1500 ms einblenden
|
// Quelle setzen und mit 1500 ms einblenden
|
||||||
$iframe
|
$iframe
|
||||||
.attr('src', url)
|
.attr('src', target)
|
||||||
.fadeIn(1500, function() {
|
.fadeIn(1500, function() {
|
||||||
syncIframeHeight();
|
syncIframeHeight();
|
||||||
observeIframeNavigation();
|
observeIframeNavigation();
|
||||||
@@ -148,8 +150,8 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
*/
|
*/
|
||||||
function openIframeInNewTab() {
|
function openIframeInNewTab() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const iframeUrl = params.get('iframe');
|
const iframeUrl = allowedIframeUrl(params.get('iframe'));
|
||||||
if (iframeUrl && isAllowedIframeUrl(iframeUrl)) {
|
if (iframeUrl) {
|
||||||
window.open(iframeUrl, '_blank');
|
window.open(iframeUrl, '_blank');
|
||||||
} else {
|
} else {
|
||||||
alert('No iframe is currently open.');
|
alert('No iframe is currently open.');
|
||||||
|
|||||||
@@ -7,13 +7,20 @@ function t(source) {
|
|||||||
|
|
||||||
const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:'];
|
const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:'];
|
||||||
|
|
||||||
function isSafeUrl(url) {
|
function safeUrl(url) {
|
||||||
try {
|
if (url == null || String(url) === '') {
|
||||||
const parsed = new URL(String(url == null ? '' : url), window.location.href);
|
return null;
|
||||||
return SAFE_URL_SCHEMES.includes(parsed.protocol);
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(String(url), window.location.href);
|
||||||
|
return SAFE_URL_SCHEMES.includes(parsed.protocol) ? parsed.href : null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSafeUrl(url) {
|
||||||
|
return safeUrl(url) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function iconAndName(item) {
|
function iconAndName(item) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "portfolio-ui"
|
name = "portfolio-ui"
|
||||||
version = "2.2.0"
|
version = "2.2.2"
|
||||||
description = "A lightweight YAML-driven portfolio and landing-page generator."
|
description = "A lightweight YAML-driven portfolio and landing-page generator."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from html.parser import HTMLParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
@@ -112,18 +113,29 @@ class TestEscaping(AppRouteMixin, unittest.TestCase):
|
|||||||
self.assertIn("<script>alert('config')", body)
|
self.assertIn("<script>alert('config')", body)
|
||||||
|
|
||||||
|
|
||||||
|
class InlineScriptCollector(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.inline = []
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
if tag != "script":
|
||||||
|
return
|
||||||
|
attributes = dict(attrs)
|
||||||
|
if "src" in attributes or attributes.get("type") == "application/json":
|
||||||
|
return
|
||||||
|
self.inline.append(self.get_starttag_text())
|
||||||
|
|
||||||
|
|
||||||
class TestContentSecurityPolicy(AppRouteMixin, unittest.TestCase):
|
class TestContentSecurityPolicy(AppRouteMixin, unittest.TestCase):
|
||||||
def test_page_ships_no_executable_inline_script(self):
|
def test_page_ships_no_executable_inline_script(self):
|
||||||
body = self.client.get("/de/").get_data(as_text=True)
|
body = self.client.get("/de/").get_data(as_text=True)
|
||||||
|
|
||||||
inline = [
|
collector = InlineScriptCollector()
|
||||||
tag
|
collector.feed(body)
|
||||||
for tag in re.findall(r"<script\b[^>]*>", body)
|
|
||||||
if "src=" not in tag and 'type="application/json"' not in tag
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
inline,
|
collector.inline,
|
||||||
[],
|
[],
|
||||||
"a host CSP can only hash an inline script whose content it knows, "
|
"a host CSP can only hash an inline script whose content it knows, "
|
||||||
"and this one changes with every language",
|
"and this one changes with every language",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
import unittest.mock
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ class TestCatalogMerge(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_an_unsupported_code_never_becomes_a_path(self):
|
def test_an_unsupported_code_never_becomes_a_path(self):
|
||||||
with mock.patch.object(i18n, "read_catalog") as read:
|
with unittest.mock.patch.object(i18n, "read_catalog") as read:
|
||||||
self.assertEqual(i18n.catalog("../content/de"), {})
|
self.assertEqual(i18n.catalog("../content/de"), {})
|
||||||
|
|
||||||
read.assert_not_called()
|
read.assert_not_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user