Compare commits

..

1 Commits

Author SHA1 Message Date
bfb0dc18cd Evaluate file access permissions 2026-01-04 17:49:33 +01:00
24 changed files with 131 additions and 272 deletions

6
.gitignore vendored
View File

@@ -127,12 +127,6 @@ dmypy.json
# Pyre type checker
.pyre/
# node.js / npm stuff
node_modules
package.json
package-lock.json
# our own stuff
*.swp
ram/storage/
!ram/storage/.gitignore

View File

@@ -1,43 +0,0 @@
server {
listen [::]:443 ssl;
listen 443 ssl;
server_name myhost;
# ssl_certificate ...;
add_header X-Xss-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=15768000";
add_header Permissions-Policy "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()";
add_header Content-Security-Policy "child-src 'none'; object-src 'none'";
client_max_body_size 250M;
error_page 403 404 https://$server_name/404;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http:// https://;
proxy_connect_timeout 1800;
proxy_read_timeout 1800;
proxy_max_temp_file_size 8192m;
}
# static files
location /static {
root /myroot/ram/storage;
}
# media files
location ~ ^/media/(images|uploads) {
root /myroot/ram/storage;
}
# protected filed to be served via X-Accel-Redirect
location /private {
internal;
alias /myroot/ram/storage/media;
}
}

View File

@@ -29,7 +29,7 @@ from bookshelf.models import (
class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
model = BaseBookImage
min_num = 0
extra = 1
extra = 0
readonly_fields = ("image_thumbnail",)
classes = ["collapse"]
verbose_name = "Image"
@@ -47,7 +47,7 @@ class BookPropertyInline(admin.TabularInline):
class BookDocInline(admin.TabularInline):
model = BookDocument
min_num = 0
extra = 1
extra = 0
classes = ["collapse"]

View File

@@ -47,7 +47,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"creation_time",
"updated_time",
)
list_filter = ("published", "company__name", "era", "scale__scale")
list_filter = ("published", "company__name", "era", "scale")
list_display = (
"__str__",
"company__name",

View File

@@ -56,15 +56,6 @@ class Consist(BaseModel):
order=models.Max("order"),
).order_by("order")
def get_cover(self):
if self.image:
return self.image
else:
consist_item = self.consist_item.first()
if consist_item and consist_item.rolling_stock.image.exists():
return consist_item.rolling_stock.image.first().image
return None
@property
def country(self):
return self.company.country

View File

@@ -24,7 +24,7 @@ class PropertyAdmin(admin.ModelAdmin):
class DecoderDocInline(admin.TabularInline):
model = DecoderDocument
min_num = 0
extra = 1
extra = 0
classes = ["collapse"]

View File

@@ -1 +0,0 @@
html[data-bs-theme=dark] .navbar svg{fill:#fff}.card>a>img{width:100%}td>img.logo{max-width:200px;max-height:48px}td>img.logo-xl{max-width:400px;max-height:96px}td>p:last-child{margin-bottom:0}.btn>span{display:inline-block}a.badge,a.badge:hover{text-decoration:none;color:#fff}.img-thumbnail{padding:0}.w-33{width:33%!important}.table-group-divider{border-top:calc(var(--bs-border-width) * 3) solid var(--bs-border-color)}#nav-journal ol,#nav-journal ul{padding-left:1rem}#nav-journal ol:last-child,#nav-journal p:last-child,#nav-journal ul:last-child{margin-bottom:0}#footer>p{display:inline}

View File

@@ -1,7 +0,0 @@
# Compile main.min.css
```bash
$ npm install clean-css-cli
$ npx cleancss -o ../main.min.css main.css
```

View File

@@ -1,6 +0,0 @@
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(()=>{"use strict";const e=()=>localStorage.getItem("theme"),t=()=>{const t=e();return t||(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light")},a=e=>{"auto"===e&&window.matchMedia("(prefers-color-scheme: dark)").matches?document.documentElement.setAttribute("data-bs-theme","dark"):document.documentElement.setAttribute("data-bs-theme",e)};a(t());const r=(e,t=!1)=>{const a=document.querySelector("#bd-theme");if(!a)return;const r=document.querySelector(".theme-icon-active i"),o=document.querySelector(`[data-bs-theme-value="${e}"]`),s=o.querySelector(".theme-icon i").getAttribute("class");document.querySelectorAll("[data-bs-theme-value]").forEach(e=>{e.classList.remove("active"),e.setAttribute("aria-pressed","false")}),o.classList.add("active"),o.setAttribute("aria-pressed","true"),r.setAttribute("class",s),t&&a.focus()};window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{const r=e();"light"!==r&&"dark"!==r&&a(t())}),window.addEventListener("DOMContentLoaded",()=>{r(t()),document.querySelectorAll("[data-bs-theme-value]").forEach(e=>{e.addEventListener("click",()=>{const t=e.getAttribute("data-bs-theme-value");(e=>{localStorage.setItem("theme",e)})(t),a(t),r(t,!0)})})})})(),document.addEventListener("DOMContentLoaded",function(){const e=window.location.hash.substring(1);if(e){const t=document.querySelector(`[data-bs-target="#nav-${e}"]`);t&&bootstrap.Tab.getOrCreateInstance(t).show()}document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(e=>{e.addEventListener("shown.bs.tab",e=>{const t=e.target.getAttribute("data-bs-target").replace("nav-","");history.replaceState(null,null,t)})});const t=document.getElementById("tabSelector");t&&(t.addEventListener("change",function(){const e=this.value,t=document.querySelector(`[data-bs-target="#${e}"]`);if(t){bootstrap.Tab.getOrCreateInstance(t).show()}}),document.querySelectorAll('[data-bs-toggle="tab"]').forEach(e=>{e.addEventListener("shown.bs.tab",e=>{const a=e.target.getAttribute("data-bs-target");t.value=a.substring(1)})}))});

View File

@@ -1,7 +0,0 @@
# Compile main.min.js
```bash
$ npm install terser
$ npx terser theme_selector.js tabs_selector.js -c -m -o ../main.min.js
```

View File

@@ -1,39 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
// code to handle tab selection and URL hash synchronization
const hash = window.location.hash.substring(1) // remove the '#' prefix
if (hash) {
const trigger = document.querySelector(`[data-bs-target="#nav-${hash}"]`);
if (trigger) {
bootstrap.Tab.getOrCreateInstance(trigger).show();
}
}
//
// update the URL hash when a tab is shown
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(btn => {
btn.addEventListener('shown.bs.tab', event => {
const newHash = event.target.getAttribute('data-bs-target').replace('nav-', '');
history.replaceState(null, null, newHash);
});
});
// allow tab selection via a dropdown on small screens
const selectElement = document.getElementById('tabSelector');
if (!selectElement) return;
selectElement.addEventListener('change', function () {
const targetSelector = this.value;
const triggerEl = document.querySelector(`[data-bs-target="#${targetSelector}"]`);
if (triggerEl) {
// Use Bootstrap 5.3's API — ensures transitions + ARIA updates
const tabInstance = bootstrap.Tab.getOrCreateInstance(triggerEl);
tabInstance.show();
}
});
// keep the dropdown in sync if the user clicks a tab button
document.querySelectorAll('[data-bs-toggle="tab"]').forEach(btn => {
btn.addEventListener('shown.bs.tab', event => {
const target = event.target.getAttribute('data-bs-target');
selectElement.value = target.substring(1); // remove the '#' character
});
});
});

View File

@@ -1,76 +0,0 @@
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
'use strict'
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark')
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')
if (!themeSwitcher) {
return
}
const activeThemeIcon = document.querySelector('.theme-icon-active i')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const biOfActiveBtn = btnToActive.querySelector('.theme-icon i').getAttribute('class')
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
})
btnToActive.classList.add('active')
btnToActive.setAttribute('aria-pressed', 'true')
activeThemeIcon.setAttribute('class', biOfActiveBtn)
if (focus) {
themeSwitcher.focus()
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value')
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
})
})
})
})()

View File

@@ -22,8 +22,114 @@
<link href="{% static "bootstrap@5.3.8/dist/css/bootstrap.min.css" %}" rel="stylesheet">
<link href="{% static "bootstrap-icons@1.13.1/font/bootstrap-icons.css" %}" rel="stylesheet">
{% endif %}
<link href="{% static "css/main.min.css" %}?v={{ site_conf.version }}" rel="stylesheet">
<script src="{% static "js/main.min.js" %}"></script>
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<script>
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
'use strict'
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark')
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')
if (!themeSwitcher) {
return
}
const activeThemeIcon = document.querySelector('.theme-icon-active i')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const biOfActiveBtn = btnToActive.querySelector('.theme-icon i').getAttribute('class')
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
})
btnToActive.classList.add('active')
btnToActive.setAttribute('aria-pressed', 'true')
activeThemeIcon.setAttribute('class', biOfActiveBtn)
if (focus) {
themeSwitcher.focus()
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value')
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
})
})
})
})()
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
var selectElement = document.getElementById('tabSelector');
try {
selectElement.addEventListener('change', function () {
var selectedTabId = this.value;
var tabs = document.querySelectorAll('.tab-pane');
tabs.forEach(function (tab) {
tab.classList.remove('show', 'active');
});
document.getElementById(selectedTabId).classList.add('show', 'active');
});
} catch (TypeError) { /* pass */ }
});
</script>
{% block extra_head %}
{{ site_conf.extra_head | safe }}
{% endblock %}

View File

@@ -1,13 +1,12 @@
{% load static %}
<div class="col">
<div class="card shadow-sm">
<a href="{{ d.get_absolute_url }}">
{% if d.get_cover %}
<img class="card-img-top" src="{{ d.get_cover.url }}" alt="{{ d }}">
{% if d.image %}
<img class="card-img-top" src="{{ d.image.url }}" alt="{{ d }}">
{% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
{% with d.consist_item.first.rolling_stock as r %}
<img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d }}">
{% endwith %}
{% endif %}
</a>
<div class="card-body">

View File

@@ -34,7 +34,6 @@
{% endfor %}
{% endblock %}
</div>
{% if loads %}
<div class="accordion shadow-sm mt-4" id="accordionLoads">
<div class="accordion-item">
<h2 class="accordion-header">
@@ -53,7 +52,6 @@
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block pagination %}
{% if data.has_other_pages %}

View File

@@ -6,7 +6,6 @@ from urllib.parse import unquote
from django.conf import settings
from django.views import View
from django.urls import Resolver404
from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError
from django.db.models import F, Q, Count
@@ -64,18 +63,7 @@ def get_items_ordering(config="items_ordering"):
class Render404(View):
def get(self, request, exception):
generic_message = "Page not found"
if isinstance(exception, Resolver404):
message = generic_message
else:
message = str(exception) if exception else generic_message
return render(
request,
"base.html",
{"title": message},
status=404,
)
return render(request, "base.html", {"title": "404 page not found"})
class GetData(View):
@@ -773,7 +761,7 @@ class GetMagazine(View):
return render(
request,
"bookshelf/magazine.html",
"magazine.html",
{
"title": magazine,
"magazine": magazine,

View File

@@ -1,4 +1,4 @@
from ram.utils import git_suffix
__version__ = "0.19.9"
__version__ = "0.19.6"
__version__ += git_suffix(__file__)

View File

@@ -34,4 +34,3 @@ ALLOWED_HOSTS = ["127.0.0.1", "myhost"]
CSRF_TRUSTED_ORIGINS = ["https://myhost"]
STATIC_URL = "static/"
MEDIA_URL = "media/"
USE_X_ACCEL_REDIRECT = True

View File

@@ -206,19 +206,6 @@ ROLLING_STOCK_TYPES = [
FEATURED_ITEMS_MAX = 6
# If True, use X-Accel-Redirect (Nginx)
# when using X-Accel-Redirect, we don't serve the file
# directly from Django, but let Nginx handle it
# in Nginx config, we need to map /private/ to
# the actual media files location with internal directive
# eg:
# location /private {
# internal;
# alias /path/to/media;
# }
# make also sure that the entire /media is _not_ mapped directly in Nginx
USE_X_ACCEL_REDIRECT = False
try:
from ram.local_settings import *
except ImportError:

View File

@@ -28,15 +28,11 @@ handler404 = Render404.as_view()
urlpatterns = [
path("", lambda r: redirect("portal/")),
path("admin/", admin.site.urls),
path("tinymce/", include("tinymce.urls")),
path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"),
path(
"media/files/<path:filename>",
DownloadFile.as_view(),
name="download_file",
),
path("portal/", include("portal.urls")),
path("admin/", admin.site.urls),
path("media/files/<path:filename>", DownloadFile.as_view(), name="download_file"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Enable the "/dcc" routing only if the "driver" app is active
@@ -60,7 +56,6 @@ if settings.DEBUG:
if settings.REST_ENABLED:
from django.views.generic import TemplateView
from rest_framework.schemas import get_schema_view
urlpatterns += [
path(
"swagger/",

View File

@@ -9,7 +9,6 @@ from django.apps import apps
from django.conf import settings
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
FileResponse,
@@ -77,7 +76,7 @@ class UploadImage(View):
class DownloadFile(View):
def get(self, request, filename, disposition="inline"):
def get(self, request, filename):
# Clean up the filename to prevent directory traversal attacks
filename = os.path.basename(filename)
@@ -88,33 +87,15 @@ class DownloadFile(View):
try:
doc = model.objects.get(file__endswith=filename)
if doc.private and not request.user.is_staff:
break
raise Http404("File not found")
file = doc.file
if not os.path.exists(file.path):
break
# in Nginx config, we need to map /private/ to
# the actual media files location with internal directive
# eg:
# location /private {
# internal;
# alias /path/to/media;
# }
if getattr(settings, "USE_X_ACCEL_REDIRECT", False):
response = HttpResponse()
response["Content-Type"] = ""
response["X-Accel-Redirect"] = f"/private/{file.name}"
else:
response = FileResponse(
open(file.path, "rb"), as_attachment=True
)
file_path = doc.file.path
if not os.path.exists(file_path):
raise Http404("File not found")
response = FileResponse(open(file_path, "rb"), as_attachment=True)
response["Content-Disposition"] = (
'{}; filename="{}"'.format(
disposition,
smart_str(os.path.basename(file.path))
)
f'attachment; filename="{smart_str(os.path.basename(file_path))}"'
)
return response
except model.DoesNotExist:

View File

@@ -53,14 +53,14 @@ class RollingClass(admin.ModelAdmin):
class RollingStockDocInline(admin.TabularInline):
model = RollingStockDocument
min_num = 0
extra = 1
extra = 0
classes = ["collapse"]
class RollingStockImageInline(SortableInlineAdminMixin, admin.TabularInline):
model = RollingStockImage
min_num = 0
extra = 1
extra = 0
readonly_fields = ("image_thumbnail",)
classes = ["collapse"]