Fix security issues.

This commit is contained in:
2025-08-04 13:45:13 +02:00
committed by Jens Luedicke
parent f98e8f3e71
commit 64b8c3fccb
7 changed files with 1100 additions and 174 deletions

View File

@@ -2,99 +2,156 @@
{% block content %}
<div class="page-container">
<!-- Page Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">{{ note.title }}</h1>
<div class="page-meta">
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
{% if note.visibility.value == 'Private' %}<i class="ti ti-lock"></i>{% elif note.visibility.value == 'Team' %}<i class="ti ti-users"></i>{% else %}<i class="ti ti-building"></i>{% endif %}
{{ note.visibility.value }}
</span>
{% if note.is_pinned %}
<span class="pin-badge">
<span class="icon"><i class="ti ti-pin"></i></span>
Pinned
</span>
{% endif %}
<span class="meta-divider"></span>
<span class="author">
<span class="icon"><i class="ti ti-user"></i></span>
{{ note.created_by.username }}
</span>
<span class="meta-divider"></span>
<span class="date">
<span class="icon"><i class="ti ti-calendar"></i></span>
Created {{ note.created_at|format_date }}
</span>
{% if note.updated_at > note.created_at %}
<span class="meta-divider"></span>
<span class="date">
<span class="icon"><i class="ti ti-refresh"></i></span>
Updated {{ note.updated_at|format_date }}
</span>
{% endif %}
{% if note.folder %}
<span class="meta-divider"></span>
<span class="folder">
<span class="icon"><i class="ti ti-folder"></i></span>
<a href="{{ url_for('notes.notes_list', folder=note.folder) }}" class="folder-link">
{{ note.folder }}
</a>
</span>
{% endif %}
</div>
</div>
<!-- Compact Unified Header -->
<div class="note-header-compact">
<!-- Title Bar -->
<div class="header-title-bar">
<button class="btn-icon" onclick="window.location.href='{{ url_for('notes.notes_list') }}'">
<i class="ti ti-arrow-left"></i>
</button>
<h1 class="note-title">{{ note.title }}</h1>
<div class="header-actions">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="downloadDropdown" data-toggle="dropdown">
<span class="icon"><i class="ti ti-download"></i></span>
Download
<!-- Context-Specific Primary Actions -->
{% if note.is_file_based and note.file_type == 'document' and note.original_filename.endswith('.pdf') %}
<!-- PDF Actions -->
<div class="zoom-controls">
<button class="btn-icon" onclick="pdfZoomOut()" title="Zoom Out">
<i class="ti ti-zoom-out"></i>
</button>
<span class="zoom-level" id="zoom-level">100%</span>
<button class="btn-icon" onclick="pdfZoomIn()" title="Zoom In">
<i class="ti ti-zoom-in"></i>
</button>
</div>
<a href="{{ note.file_url }}" class="btn btn-primary btn-sm" download>
<i class="ti ti-download"></i>
<span class="btn-text">Download PDF</span>
</a>
{% elif note.is_image %}
<!-- Image Actions -->
<button class="btn-icon" onclick="toggleFullscreen()" title="Fullscreen">
<i class="ti ti-maximize"></i>
</button>
<div class="dropdown-menu">
<a href="{{ note.file_url }}" class="btn btn-primary btn-sm" download>
<i class="ti ti-download"></i>
<span class="btn-text">Download</span>
</a>
{% else %}
<!-- Markdown/Text Actions -->
{% if note.can_user_edit(g.user) %}
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn btn-primary btn-sm">
<i class="ti ti-pencil"></i>
<span class="btn-text">Edit</span>
</a>
{% endif %}
<a href="{{ url_for('notes.view_note_mindmap', slug=note.slug) }}" class="btn btn-secondary btn-sm">
<i class="ti ti-brain"></i>
<span class="btn-text">Mind Map</span>
</a>
{% endif %}
<!-- Common Actions -->
{% if note.can_user_edit(g.user) %}
<button class="btn btn-secondary btn-sm" onclick="showShareModal()">
<i class="ti ti-share"></i>
<span class="btn-text">Share</span>
</button>
{% endif %}
<!-- More Actions Dropdown -->
<div class="dropdown">
<button class="btn-icon" data-toggle="dropdown" title="More actions">
<i class="ti ti-dots-vertical"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
{% if not (note.is_file_based and note.file_type == 'document' and note.original_filename.endswith('.pdf')) %}
<!-- Download options for non-PDF -->
<h6 class="dropdown-header">Download as</h6>
<a class="dropdown-item" href="{{ url_for('notes_download.download_note', slug=note.slug, format='md') }}">
<span class="icon"><i class="ti ti-file-text"></i></span>
Markdown (.md)
<i class="ti ti-file-text"></i> Markdown
</a>
<a class="dropdown-item" href="{{ url_for('notes_download.download_note', slug=note.slug, format='html') }}">
<span class="icon"><i class="ti ti-world"></i></span>
HTML (.html)
<i class="ti ti-world"></i> HTML
</a>
<a class="dropdown-item" href="{{ url_for('notes_download.download_note', slug=note.slug, format='txt') }}">
<span class="icon"><i class="ti ti-file"></i></span>
Plain Text (.txt)
<i class="ti ti-file"></i> Plain Text
</a>
<div class="dropdown-divider"></div>
{% endif %}
{% if note.is_pinned %}
<a class="dropdown-item" href="#">
<i class="ti ti-pin-filled"></i> Pinned
</a>
{% else %}
<a class="dropdown-item" href="#">
<i class="ti ti-pin"></i> Pin Note
</a>
{% endif %}
<a class="dropdown-item" onclick="window.print()">
<i class="ti ti-printer"></i> Print
</a>
{% if note.can_user_edit(g.user) %}
<div class="dropdown-divider"></div>
<form method="POST" action="{{ url_for('notes.delete_note', slug=note.slug) }}"
style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this note?')">
<button type="submit" class="dropdown-item text-danger">
<i class="ti ti-trash"></i> Delete Note
</button>
</form>
{% endif %}
</div>
</div>
<a href="{{ url_for('notes.view_note_mindmap', slug=note.slug) }}" class="btn btn-secondary">
<span class="icon"><i class="ti ti-brain"></i></span>
Mind Map
</a>
{% if note.can_user_edit(g.user) %}
<button type="button" class="btn btn-secondary" onclick="showShareModal()">
<span class="icon"><i class="ti ti-link"></i></span>
Share
</button>
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn btn-primary">
<span class="icon"><i class="ti ti-pencil"></i></span>
Edit
</a>
<form method="POST" action="{{ url_for('notes.delete_note', slug=note.slug) }}"
style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this note?')">
<button type="submit" class="btn btn-danger">
<span class="icon"><i class="ti ti-trash"></i></span>
Delete
</button>
</form>
{% endif %}
<a href="{{ url_for('notes.notes_list') }}" class="btn btn-secondary">
<span class="icon"><i class="ti ti-arrow-left"></i></span>
Back to Notes
</a>
</div>
</div>
<!-- Metadata Bar -->
<div class="header-meta-bar">
{% if note.folder %}
<span class="meta-item">
<i class="ti ti-folder"></i>
<a href="{{ url_for('notes.notes_list', folder=note.folder) }}">{{ note.folder }}</a>
</span>
{% endif %}
{% if note.tags %}
<span class="meta-item">
<i class="ti ti-tag"></i>
{% for tag in note.get_tags_list() %}
<a href="{{ url_for('notes.notes_list', tag=tag) }}" class="tag-link">{{ tag }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</span>
{% endif %}
<span class="meta-item">
<i class="ti ti-user"></i> {{ note.created_by.username }}
</span>
<span class="meta-item">
<i class="ti ti-clock"></i>
{% if note.updated_at > note.created_at %}
Updated {{ note.updated_at|format_date }}
{% else %}
Created {{ note.created_at|format_date }}
{% endif %}
</span>
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
{% if note.visibility.value == 'Private' %}
<i class="ti ti-lock"></i>
{% elif note.visibility.value == 'Team' %}
<i class="ti ti-users"></i>
{% else %}
<i class="ti ti-building"></i>
{% endif %}
{{ note.visibility.value }}
</span>
</div>
</div>
<!-- Note Metadata Card -->
@@ -153,24 +210,15 @@
<!-- Note Content -->
<div class="content-card">
{% if note.is_file_based and note.file_type == 'document' and note.original_filename.endswith('.pdf') %}
<!-- PDF Preview -->
<!-- PDF Preview (toolbar moved to unified header) -->
<div class="pdf-preview-container">
<div class="pdf-toolbar">
<button class="btn btn-sm btn-secondary" onclick="pdfZoomIn()">
<i class="ti ti-zoom-in"></i> Zoom In
</button>
<button class="btn btn-sm btn-secondary" onclick="pdfZoomOut()">
<i class="ti ti-zoom-out"></i> Zoom Out
</button>
<button class="btn btn-sm btn-secondary" onclick="pdfZoomReset()">
<i class="ti ti-zoom-reset"></i> Reset
</button>
<a href="{{ note.file_url }}" class="btn btn-sm btn-primary" download>
<i class="ti ti-download"></i> Download PDF
</a>
</div>
<iframe id="pdf-viewer" src="{{ note.file_url }}" class="pdf-viewer"></iframe>
</div>
{% elif note.is_image %}
<!-- Image Preview -->
<div class="image-preview-container">
<img src="{{ note.file_url }}" alt="{{ note.title }}" class="note-image" id="note-image">
</div>
{% else %}
<!-- Regular Content -->
<div class="markdown-content">
@@ -311,14 +359,197 @@
margin: 0 auto;
}
/* Page Header - Time Tracking style */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
/* Compact Unified Header */
.note-header-compact {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 1.5rem;
position: sticky;
top: 10px;
z-index: 100;
}
.header-title-bar {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
min-height: 60px;
}
.note-title {
flex: 1;
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-icon {
width: 40px;
height: 40px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #dee2e6;
border-radius: 8px;
background: white;
color: #495057;
transition: all 0.2s;
cursor: pointer;
}
.btn-icon:hover {
background: #f8f9fa;
border-color: #adb5bd;
transform: translateY(-1px);
}
.zoom-controls {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
background: #f8f9fa;
border-radius: 8px;
margin-right: 0.5rem;
}
.zoom-level {
min-width: 60px;
text-align: center;
font-size: 0.875rem;
font-weight: 500;
color: #495057;
}
.header-meta-bar {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 0.75rem 1.5rem;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
border-radius: 0 0 12px 12px;
font-size: 0.875rem;
color: #6c757d;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.375rem;
}
.meta-item i {
font-size: 1rem;
opacity: 0.7;
}
.meta-item a {
color: inherit;
text-decoration: none;
}
.meta-item a:hover {
color: #495057;
text-decoration: underline;
}
.tag-link {
color: #667eea;
}
.tag-link:hover {
color: #5a67d8;
}
/* Updated button styles for compact header */
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
border-radius: 6px;
display: inline-flex;
align-items: center;
gap: 0.375rem;
}
.btn-primary.btn-sm {
background: #667eea;
border-color: #667eea;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.btn-primary.btn-sm:hover {
background: #5a67d8;
border-color: #5a67d8;
}
.btn-secondary.btn-sm {
background: white;
border: 1px solid #dee2e6;
color: #495057;
}
.btn-secondary.btn-sm:hover {
background: #f8f9fa;
border-color: #adb5bd;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.header-title-bar {
padding: 0.75rem;
gap: 0.5rem;
}
.note-title {
font-size: 1.25rem;
}
.header-actions {
gap: 0.25rem;
}
.btn-sm {
padding: 0.375rem 0.5rem;
}
/* Hide button text on mobile */
.btn-text {
display: none;
}
.btn-sm i {
margin: 0;
}
.header-meta-bar {
flex-wrap: wrap;
gap: 0.75rem;
padding: 0.75rem;
font-size: 0.8125rem;
}
.zoom-controls {
padding: 0.125rem;
}
.btn-icon {
width: 36px;
height: 36px;
}
}
.header-content {
@@ -745,6 +976,32 @@
font-size: 0.875rem;
line-height: 1.5;
margin: 0 0 0.5rem 0;
{% if g.user.preferences and g.user.preferences.note_preview_font and g.user.preferences.note_preview_font != 'system' %}
{% set font = g.user.preferences.note_preview_font %}
{% if font == 'sans-serif' %}
font-family: Arial, Helvetica, sans-serif;
{% elif font == 'serif' %}
font-family: "Times New Roman", Times, serif;
{% elif font == 'monospace' %}
font-family: "Courier New", Courier, monospace;
{% elif font == 'georgia' %}
font-family: Georgia, serif;
{% elif font == 'palatino' %}
font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif;
{% elif font == 'garamond' %}
font-family: Garamond, serif;
{% elif font == 'bookman' %}
font-family: "Bookman Old Style", serif;
{% elif font == 'comic-sans' %}
font-family: "Comic Sans MS", cursive;
{% elif font == 'trebuchet' %}
font-family: "Trebuchet MS", sans-serif;
{% elif font == 'arial-black' %}
font-family: "Arial Black", sans-serif;
{% elif font == 'impact' %}
font-family: Impact, sans-serif;
{% endif %}
{% endif %}
}
.linked-note-meta {
@@ -1013,6 +1270,27 @@
background: #f8f9fa;
}
/* Image preview styles */
.image-preview-container {
text-align: center;
padding: 1rem;
}
.note-image {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: zoom-in;
}
.note-image:fullscreen {
cursor: zoom-out;
object-fit: contain;
padding: 2rem;
background: black;
}
/* Responsive PDF viewer */
@media (max-width: 768px) {
.pdf-viewer {
@@ -1048,13 +1326,37 @@ function pdfZoomReset() {
function updatePdfZoom() {
const viewer = document.getElementById('pdf-viewer');
const zoomLevel = document.getElementById('zoom-level');
if (viewer) {
viewer.style.transform = `scale(${pdfZoom})`;
viewer.style.transformOrigin = 'top center';
}
if (zoomLevel) {
zoomLevel.textContent = Math.round(pdfZoom * 100) + '%';
}
}
// Image viewer functions
function toggleFullscreen() {
const image = document.getElementById('note-image');
if (image) {
if (!document.fullscreenElement) {
image.requestFullscreen().catch(err => {
console.error(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
}
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize zoom level display for PDFs
const zoomLevel = document.getElementById('zoom-level');
if (zoomLevel) {
zoomLevel.textContent = '100%';
}
// Download dropdown functionality
const downloadBtn = document.getElementById('downloadDropdown');
const downloadMenu = downloadBtn.nextElementSibling;