1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-26 18:21:01 +00:00
miniflux-v2/internal/template/templates/common/layout.html
jvoisin da9c3a4032 feat(js): tighten the trusted types policy
- Implement a better/simpler polyfill for web browsers that don't supported
  trusted types yet
- Use two separate policies: one to create HTML, another to create/use script
  urls
- Instead of having the policy live in the top-level scope, they're now
  declared at the lowest possible scope, right before they're used, making them
  inaccessible outside of it. This puts their usage completely out of reach of
  an attacker unable to gain some control outside of those two (small) scopes,
  and thus removes the need to tighten the policies.
- Remove the now-unused tt.js file

This has been tested on Firefox (doesn't support trusted types) and on Chromium
(does support trusted types).
2025-08-21 19:41:45 -07:00

199 lines
12 KiB
HTML

{{ define "base" }}
<!DOCTYPE html>
<html lang="{{ replace .language "_" "-"}}">
<head>
<meta charset="utf-8">
<title>{{template "title" .}} - Miniflux</title>
<meta name="apple-mobile-web-app-title" content="Miniflux">
<meta name="googlebot" content="notranslate">
<meta name="mobile-web-app-capable" content="yes">
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noindex,nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="{{ theme_color .theme "light" }}" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="{{ theme_color .theme "dark" }}" media="(prefers-color-scheme: dark)">
<link rel="manifest" href="{{ route "webManifest" }}" crossorigin="use-credentials">
<link rel="icon" type="image/png" sizes="16x16" href="{{ route "appIcon" "filename" "icon-16.png" }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ route "appIcon" "filename" "icon-32.png" }}">
<link rel="icon" type="image/png" sizes="128x128" href="{{ route "appIcon" "filename" "icon-128.png" }}">
<link rel="icon" type="image/png" sizes="192x192" href="{{ route "appIcon" "filename" "icon-192.png" }}">
<link rel="apple-touch-icon" sizes="120x120" href="{{ route "appIcon" "filename" "icon-120.png" }}">
<link rel="apple-touch-icon" sizes="152x152" href="{{ route "appIcon" "filename" "icon-152.png" }}">
<link rel="apple-touch-icon" sizes="167x167" href="{{ route "appIcon" "filename" "icon-167.png" }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ route "appIcon" "filename" "icon-180.png" }}">
<link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" .theme "checksum" .theme_checksum }}">
{{ if .user }}
{{ $cspNonce := nonce }}
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; {{ if .user.ExternalFontHosts }}font-src {{ .user.ExternalFontHosts }}; {{ end }}style-src 'self'{{ if .user.Stylesheet }}{{ if .user.ExternalFontHosts }} {{ .user.ExternalFontHosts }}{{ end }} 'nonce-{{ $cspNonce }}'{{ end }}{{ if .user.CustomJS }}; script-src 'self' 'nonce-{{ $cspNonce }}'{{ end }}; require-trusted-types-for 'script'; trusted-types html url;">
{{ if .user.Stylesheet -}}
<style nonce="{{ $cspNonce }}">{{ .user.Stylesheet | safeCSS }}</style>
{{ end -}}
{{ if .user.CustomJS -}}
<script type="module" nonce="{{ $cspNonce }}">{{ .user.CustomJS | safeJS }}</script>
{{ end -}}
{{ else -}}
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; require-trusted-types-for 'script'; trusted-types html url;">
{{ end -}}
<script src="{{ route "javascript" "name" "app" "checksum" .app_js_checksum }}" type="module"></script>
</head>
<body
data-service-worker-url="{{ route "javascript" "name" "service-worker" "checksum" .sw_js_checksum }}"
{{ if .csrf }}data-csrf-token="{{ .csrf }}"{{ end }}
data-add-subscription-url="{{ route "addSubscription" }}"
data-entries-status-url="{{ route "updateEntriesStatus" }}"
data-refresh-all-feeds-url="{{ route "refreshAllFeeds" }}"
{{ if .webAuthnEnabled }}
data-webauthn-register-begin-url="{{ route "webauthnRegisterBegin" }}"
data-webauthn-register-finish-url="{{ route "webauthnRegisterFinish" }}"
data-webauthn-login-begin-url="{{ route "webauthnLoginBegin" }}"
data-webauthn-login-finish-url="{{ route "webauthnLoginFinish" }}"
data-webauthn-delete-all-url="{{ route "webauthnDeleteAll" }}"
{{ end }}
{{ if .user }}
{{ if not .user.KeyboardShortcuts }}data-disable-keyboard-shortcuts="true"{{ end }}
data-mark-as-read-on-view="{{ if .user.MarkReadOnView }}true{{ else }}false{{ end }}"
{{ end }}>
{{ if .user }}
<a class="skip-to-content-link" href="#main">{{ t "skip_to_content" }}</a>
<header class="header">
<nav>
<div class="logo" data-toggle-button-label="{{ t "menu.title" }}">
<a aria-label="{{ t "menu.home_page" }}" href="{{ route .user.DefaultHomePage }}">
Mini<span>flux</span>
</a>
{{ icon "chevron-down"}}
</div>
<ul id="header-menu">
<li {{ if eq .menu "unread" }}class="active"{{ end }} title="{{ t "tooltip.keyboard_shortcuts" "g u" }}">
<a href="{{ route "unread" }}"
data-page="unread"
{{ if gt .countUnread 0 }}
aria-label="{{ t "menu.unread" }}, {{ plural "page.unread_entry_count" .countUnread .countUnread }}"
{{ end }}
>
{{ icon "entries" }}{{ t "menu.unread" }}
{{ if gt .countUnread 0 }}
<span class="unread-counter-wrapper" aria-hidden="true">(<span class="unread-counter">{{ .countUnread }}</span>)</span>
{{ end }}
</a>
</li>
<li {{ if eq .menu "starred" }}class="active"{{ end }} title="{{ t "tooltip.keyboard_shortcuts" "g b" }}">
<a href="{{ route "starred" }}" data-page="starred">{{ icon "star" }}{{ t "menu.starred" }}</a>
</li>
<li {{ if eq .menu "history" }}class="active"{{ end }} title="{{ t "tooltip.keyboard_shortcuts" "g h" }}">
<a href="{{ route "history" }}" data-page="history">{{ icon "history" }}{{ t "menu.history" }}</a>
</li>
<li {{ if eq .menu "feeds" }}class="active"{{ end }} title="{{ t "tooltip.keyboard_shortcuts" "g f" }}">
<a href="{{ route "feeds" }}" data-page="feeds">{{ icon "feeds" }}{{ t "menu.feeds" }}
{{ if gt .countErrorFeeds 0 }}
<span class="error-feeds-counter-wrapper">(<span class="error-feeds-counter">{{ .countErrorFeeds }}</span>)</span>
{{ end }}
</a>
<a href="{{ route "addSubscription" }}" title="{{ t "tooltip.keyboard_shortcuts" "+" }}" aria-label="{{ t "menu.add_feed" }}">
(+)
</a>
</li>
<li {{ if eq .menu "categories" }}class="active"{{ end }} title="{{ t "tooltip.keyboard_shortcuts" "g c" }}">
<a href="{{ route "categories" }}" data-page="categories">{{ icon "categories" }}{{ t "menu.categories" }}</a>
</li>
<li {{ if eq .menu "search" }}class="active"{{ end }} title="{{ t "tooltip.keyboard_shortcuts" "/" }}">
<a href="{{ route "search" }}" data-page="search">{{ icon "search" }}{{ t "menu.search" }}</a>
</li>
<li {{ if eq .menu "settings" }}class="active"{{ end }} title="{{ t "tooltip.keyboard_shortcuts" "g s" }}">
<a href="{{ route "settings" }}" data-page="settings">{{ icon "settings" }}{{ t "menu.settings" }}</a>
</li>
{{ if not hasAuthProxy }}
<li>
<a href="{{ route "logout" }}" title="{{ t "tooltip.logged_user" .user.Username }}">{{ icon "logout" }}{{ t "menu.logout" }}</a>
</li>
{{ end }}
</ul>
</nav>
</header>
{{ end }}
{{ if .flashMessage }}
<div role="alert" class="flash-message alert alert-success">{{ .flashMessage }}</div>
{{ end }}
{{ if .flashErrorMessage }}
<div role="alert" class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
{{ end }}
{{template "page_header" .}}
<main id="main">
{{template "content" .}}
</main>
<template id="keyboard-shortcuts">
<div id="modal-left">
<button class="btn-close-modal" aria-label="Close">x</button>
<h3 tabindex="-1" id="dialog-title">{{ t "page.keyboard_shortcuts.title" }}</h3>
<div class="keyboard-shortcuts">
<p>{{ t "page.keyboard_shortcuts.subtitle.sections" }}</p>
<ul>
<li>{{ t "page.keyboard_shortcuts.go_to_unread" }} = <strong>g + u</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_starred" }} = <strong>g + b</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_history" }} = <strong>g + h</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_feeds" }} = <strong>g + f</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_categories" }} = <strong>g + c</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_settings" }} = <strong>g + s</strong></li>
<li>{{ t "page.keyboard_shortcuts.show_keyboard_shortcuts" }} = <strong>?</strong></li>
<li>{{ t "menu.add_feed" }} = <strong>+</strong></li>
</ul>
<p>{{ t "page.keyboard_shortcuts.subtitle.items" }}</p>
<ul>
<li>{{ t "page.keyboard_shortcuts.go_to_previous_item" }} = <strong>p</strong>, <strong>k</strong>, <strong>&#x23F4;</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_next_item" }} = <strong>n</strong>, <strong>j</strong>, <strong>&#x23F5;</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_feed" }} = <strong>F</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_top_item" }} = <strong>g + g</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_bottom_item" }} = <strong>G</strong></li>
</ul>
<p>{{ t "page.keyboard_shortcuts.subtitle.pages" }}</p>
<ul>
<li>{{ t "page.keyboard_shortcuts.go_to_previous_page" }} = <strong>h</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_next_page" }} = <strong>l</strong></li>
</ul>
<p>{{ t "page.keyboard_shortcuts.subtitle.actions" }}</p>
<ul>
<li>{{ t "page.keyboard_shortcuts.open_item" }} = <strong>o</strong>, <strong>Enter</strong></li>
<li>{{ t "page.keyboard_shortcuts.open_original" }} = <strong>v</strong></li>
<li>{{ t "page.keyboard_shortcuts.open_original_same_window" }} = <strong>V</strong></li>
<li>{{ t "page.keyboard_shortcuts.open_comments" }} = <strong>c</strong></li>
<li>{{ t "page.keyboard_shortcuts.open_comments_same_window" }} = <strong>C</strong></li>
<li>{{ t "page.keyboard_shortcuts.toggle_read_status_next" }} = <strong>m</strong></li>
<li>{{ t "page.keyboard_shortcuts.toggle_read_status_prev" }} = <strong>M</strong></li>
<li>{{ t "page.keyboard_shortcuts.mark_page_as_read" }} = <strong>A</strong></li>
<li>{{ t "page.keyboard_shortcuts.download_content" }} = <strong>d</strong></li>
<li>{{ t "page.keyboard_shortcuts.toggle_star_status" }} = <strong>f</strong></li>
<li>{{ t "page.keyboard_shortcuts.save_article" }} = <strong>s</strong></li>
<li>{{ t "page.keyboard_shortcuts.toggle_entry_attachments" }} = <strong>a</strong></li>
<li>{{ t "page.keyboard_shortcuts.scroll_item_to_top" }} = <strong>z + t</strong></li>
<li>{{ t "page.keyboard_shortcuts.refresh_all_feeds" }} = <strong>R</strong></li>
<li>{{ t "page.keyboard_shortcuts.remove_feed" }} = <strong>#</strong></li>
<li>{{ t "page.keyboard_shortcuts.go_to_search" }} = <strong>/</strong></li>
<li>{{ t "page.keyboard_shortcuts.close_modal" }} = <strong>Esc</strong></li>
</ul>
</div>
</div>
</template>
<template id="icon-read">{{ icon "read" }}</template>
<template id="icon-unread">{{ icon "unread" }}</template>
<template id="icon-star">{{ icon "star" }}</template>
<template id="icon-unstar">{{ icon "unstar" }}</template>
<template id="icon-save">{{ icon "save" }}</template>
</body>
</html>
{{ end }}