Merge branch 'Develop'

# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
This commit is contained in:
Ozzie Isaacs
2026-01-24 20:50:59 +01:00
7 changed files with 84 additions and 49 deletions

View File

@@ -25,7 +25,8 @@ import sys
import os import os
import mimetypes import mimetypes
from flask import Flask from flask import Flask, request
from flask.sessions import SecureCookieSessionInterface
from .MyLoginManager import MyLoginManager from .MyLoginManager import MyLoginManager
from flask_principal import Principal from flask_principal import Principal
@@ -114,8 +115,14 @@ if limiter_present:
else: else:
limiter = None limiter = None
class ScriptNameSessionInterface(SecureCookieSessionInterface):
def get_cookie_path(self, app):
# Called once per response, after request context exists
return app.wsgi_app.script_name.rstrip("/") or "/"
def create_app(): def create_app():
app.session_interface = ScriptNameSessionInterface()
if csrf: if csrf:
csrf.init_app(app) csrf.init_app(app)

View File

@@ -7,7 +7,6 @@ from flask import abort
from flask import current_app from flask import current_app
from flask import flash from flask import flash
from flask import g from flask import g
from flask import has_app_context
from flask import redirect from flask import redirect
from flask import request from flask import request
from flask import session from flask import session
@@ -469,7 +468,7 @@ class LoginManager:
config = current_app.config config = current_app.config
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
domain = config.get("REMEMBER_COOKIE_DOMAIN") domain = config.get("REMEMBER_COOKIE_DOMAIN")
path = config.get("REMEMBER_COOKIE_PATH", "/") path = config.get("REMEMBER_COOKIE_PATH", current_app.wsgi_app.script_name)
secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE) secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)
httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY) httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)
@@ -520,36 +519,5 @@ class LoginManager:
config = current_app.config config = current_app.config
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
domain = config.get("REMEMBER_COOKIE_DOMAIN") domain = config.get("REMEMBER_COOKIE_DOMAIN")
path = config.get("REMEMBER_COOKIE_PATH", "/") path = config.get("REMEMBER_COOKIE_PATH", current_app.wsgi_app.script_name)
response.delete_cookie(cookie_name, domain=domain, path=path) response.delete_cookie(cookie_name, domain=domain, path=path)
@property
def _login_disabled(self):
"""Legacy property, use app.config['LOGIN_DISABLED'] instead."""
import warnings
warnings.warn(
"'_login_disabled' is deprecated and will be removed in"
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
" instead.",
DeprecationWarning,
stacklevel=2,
)
if has_app_context():
return current_app.config.get("LOGIN_DISABLED", False)
return False
@_login_disabled.setter
def _login_disabled(self, newvalue):
"""Legacy property setter, use app.config['LOGIN_DISABLED'] instead."""
import warnings
warnings.warn(
"'_login_disabled' is deprecated and will be removed in"
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
" instead.",
DeprecationWarning,
stacklevel=2,
)
current_app.config["LOGIN_DISABLED"] = newvalue

View File

@@ -47,7 +47,7 @@ def get_sidebar_config(kwargs=None):
if current_user.role_admin(): if current_user.role_admin():
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list', sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous), "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'), "page": "download", "show_text": _('Show Downloaded Books'), "no_param":True,
"config_show": content}) "config_show": content})
else: else:
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
@@ -69,27 +69,27 @@ def get_sidebar_config(kwargs=None):
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover", "visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
"show_text": _('Show Random Books'), "config_show": True}) "show_text": _('Show Random Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat", sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", "visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", "no_param":True,
"show_text": _('Show Category Section'), "config_show": True}) "show_text": _('Show Category Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie", sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", "visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", "no_param":True,
"show_text": _('Show Series Section'), "config_show": True}) "show_text": _('Show Series Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author", sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", "visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", "no_param":True,
"show_text": _('Show Author Section'), "config_show": True}) "show_text": _('Show Author Section'), "config_show": True})
sidebar.append( sidebar.append(
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher", {"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", "visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", "no_param":True,
"show_text": _('Show Publisher Section'), "config_show":True}) "show_text": _('Show Publisher Section'), "config_show":True})
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang", sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'), "visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'),
"page": "language", "page": "language", "no_param":True,
"show_text": _('Show Language Section'), "config_show": True}) "show_text": _('Show Language Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate", sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
"visibility": constants.SIDEBAR_RATING, 'public': True, "visibility": constants.SIDEBAR_RATING, 'public': True, "no_param":True,
"page": "rating", "show_text": _('Show Ratings Section'), "config_show": True}) "page": "rating", "show_text": _('Show Ratings Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format", sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
"visibility": constants.SIDEBAR_FORMAT, 'public': True, "visibility": constants.SIDEBAR_FORMAT, 'public': True, "no_param":True,
"page": "format", "show_text": _('Show File Formats Section'), "config_show": True}) "page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
sidebar.append( sidebar.append(
{"glyph": "glyphicon-folder-open", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived", {"glyph": "glyphicon-folder-open", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
@@ -98,8 +98,8 @@ def get_sidebar_config(kwargs=None):
if not simple: if not simple:
sidebar.append( sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list", {"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous), "page": "list", "visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous),
"show_text": _('Show Books List'), "config_show": content}) "show_text": _('Show Books List'), "config_show": content, "no_param":True})
g.shelves_access = ub.session.query(ub.Shelf).filter( g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all() or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()

View File

@@ -61,11 +61,13 @@ class ReverseProxied(object):
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
self.proxied = False self.proxied = False
self.script_name = "/"
script_name = environ.get('HTTP_X_SCRIPT_NAME', '') script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
if script_name: if script_name:
self.proxied = True self.proxied = True
environ['SCRIPT_NAME'] = script_name environ['SCRIPT_NAME'] = script_name
path_info = environ.get('PATH_INFO', '') path_info = environ.get('PATH_INFO', '')
self.script_name = script_name
if path_info and path_info.startswith(script_name): if path_info and path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):] environ['PATH_INFO'] = path_info[len(script_name):]

View File

@@ -102,6 +102,52 @@ def add_to_shelf(shelf_id, book_id):
return "", 204 return "", 204
@shelf.route("/shelf/massremove/<int:shelf_id>", methods=["POST"])
@user_login_required
def search_from_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if shelf is None:
log.error("Invalid shelf specified: {}".format(shelf_id))
flash(_("Invalid shelf specified"), category="error")
return redirect(url_for('web.index'))
if not check_shelf_edit_permissions(shelf):
log.warning("You are not allowed to remove a book from the shelf".format(shelf.name))
flash(_("You are not allowed to remove a book from the shelf"), category="error")
return redirect(url_for('web.index'))
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
books_from_shelf = list()
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
if books_in_shelf:
book_ids = [book_id.book_id for book_id in books_in_shelf]
for searchid in ub.searched_ids[current_user.id]:
if searchid in book_ids:
books_from_shelf.append(searchid)
else:
log.error("No Books are part of {}".format(shelf.name))
flash(_("No Books are part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
# maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
for book in books_from_shelf:
ub.session.delete(ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).filter(
ub.BookShelf.book_id == book).first())
shelf.last_modified = datetime.now(timezone.utc)
try:
ub.session.commit()
flash(_("Books have been removed from shelf: %(sname)s", sname=shelf.name), category="success")
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
else:
log.error("Could not remove books from shelf: {}".format(shelf.name))
flash(_("Could not remove books from shelf: %(sname)s", sname=shelf.name), category="error")
return redirect(url_for('web.index'))
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"]) @shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
@user_login_required @user_login_required
def search_to_shelf(shelf_id): def search_to_shelf(shelf_id):
@@ -120,9 +166,7 @@ def search_to_shelf(shelf_id):
books_for_shelf = list() books_for_shelf = list()
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all() books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
if books_in_shelf: if books_in_shelf:
book_ids = list() book_ids = [book_id.book_id for book_id in books_in_shelf]
for book_id in books_in_shelf:
book_ids.append(book_id.book_id)
for searchid in ub.searched_ids[current_user.id]: for searchid in ub.searched_ids[current_user.id]:
if searchid not in book_ids: if searchid not in book_ids:
books_for_shelf.append(searchid) books_for_shelf.append(searchid)

View File

@@ -147,7 +147,7 @@
<li class="nav-head hidden-xs">{{_('Browse')}}</li> <li class="nav-head hidden-xs">{{_('Browse')}}</li>
{% for element in sidebar %} {% for element in sidebar %}
{% if current_user.check_visibility(element['visibility']) and element['public'] %} {% if current_user.check_visibility(element['visibility']) and element['public'] %}
<li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{{url_for(element['link'], data=element['page'], sort_param='stored')}}"><span class="glyphicon {{element['glyph']}}"></span> {{_(element['text'])}}</a></li> <li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{% if element['no_param'] %}{{url_for(element['link'], data=element['page'])}}{%else%}{{url_for(element['link'], data=element['page'], sort_param='stored')}}{% endif %}"><span class="glyphicon {{element['glyph']}}"></span> {{_(element['text'])}}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if current_user.is_authenticated or g.allow_anonymous %} {% if current_user.is_authenticated or g.allow_anonymous %}

View File

@@ -24,6 +24,20 @@
{%endfor%} {%endfor%}
</ul> </ul>
</div> </div>
<div class="btn-group" role="group" aria-label="Remove from shelves">
<button id="remove-from-shelf" type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-list"></span> {{_('Remove from shelf')}}
<span class="caret"></span>
</button>
<ul id="remove-from-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
{% for shelf in g.shelves_access %}
{% if not shelf.is_public or current_user.role_edit_shelfs() %}
<li><a class="postAction" role="button" data-action="{{ url_for('shelf.search_from_shelf', shelf_id=shelf.id) }}"> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
{% endif %}
{%endfor%}
</ul>
</div>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}