From cf3b619c7315ff7ad5458d8df8796907116a4885 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 18 Jan 2026 12:59:40 +0100 Subject: [PATCH 1/4] Fix for #3520 (Mass remove books from shelf) --- cps/shelf.py | 50 ++++++++++++++++++++++++++++++++++++--- cps/templates/search.html | 14 +++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/cps/shelf.py b/cps/shelf.py index e4f93909e..e5fb46707 100644 --- a/cps/shelf.py +++ b/cps/shelf.py @@ -102,6 +102,52 @@ def add_to_shelf(shelf_id, book_id): return "", 204 +@shelf.route("/shelf/massremove/", 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/", methods=["POST"]) @user_login_required def search_to_shelf(shelf_id): @@ -120,9 +166,7 @@ def search_to_shelf(shelf_id): books_for_shelf = list() books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all() if books_in_shelf: - book_ids = list() - for book_id in books_in_shelf: - book_ids.append(book_id.book_id) + book_ids = [book_id.book_id for book_id in books_in_shelf] for searchid in ub.searched_ids[current_user.id]: if searchid not in book_ids: books_for_shelf.append(searchid) diff --git a/cps/templates/search.html b/cps/templates/search.html index b406ebe3f..803c7dd47 100644 --- a/cps/templates/search.html +++ b/cps/templates/search.html @@ -24,6 +24,20 @@ {%endfor%} +
+ + +
+ {% endif %} {% endif %} From c13eac91c886cdfb6af0cb50ddcc9c4403b68fcd Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 18 Jan 2026 15:38:44 +0100 Subject: [PATCH 2/4] Generate valid session-cookie-path(s) (fix for #3459) --- cps/__init__.py | 9 ++++++++- cps/cw_login/login_manager.py | 36 ++--------------------------------- cps/reverseproxy.py | 2 ++ 3 files changed, 12 insertions(+), 35 deletions(-) diff --git a/cps/__init__.py b/cps/__init__.py index c41509424..890d5867d 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -25,7 +25,8 @@ import sys import os import mimetypes -from flask import Flask +from flask import Flask, request +from flask.sessions import SecureCookieSessionInterface from .MyLoginManager import MyLoginManager from flask_principal import Principal @@ -114,8 +115,14 @@ if limiter_present: else: 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(): + app.session_interface = ScriptNameSessionInterface() if csrf: csrf.init_app(app) diff --git a/cps/cw_login/login_manager.py b/cps/cw_login/login_manager.py index a3714af60..268b92855 100644 --- a/cps/cw_login/login_manager.py +++ b/cps/cw_login/login_manager.py @@ -7,7 +7,6 @@ from flask import abort from flask import current_app from flask import flash from flask import g -from flask import has_app_context from flask import redirect from flask import request from flask import session @@ -469,7 +468,7 @@ class LoginManager: config = current_app.config cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) 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) httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY) @@ -520,36 +519,5 @@ class LoginManager: config = current_app.config cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) 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) - - @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 diff --git a/cps/reverseproxy.py b/cps/reverseproxy.py index 887590bf1..1c98c5d1a 100644 --- a/cps/reverseproxy.py +++ b/cps/reverseproxy.py @@ -61,11 +61,13 @@ class ReverseProxied(object): def __call__(self, environ, start_response): self.proxied = False + self.script_name = "/" script_name = environ.get('HTTP_X_SCRIPT_NAME', '') if script_name: self.proxied = True environ['SCRIPT_NAME'] = script_name path_info = environ.get('PATH_INFO', '') + self.script_name = script_name if path_info and path_info.startswith(script_name): environ['PATH_INFO'] = path_info[len(script_name):] From 8db0d004b62671f9b290c9cb4a25174b6c1320a0 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 18 Jan 2026 16:07:04 +0100 Subject: [PATCH 3/4] Remove Get Parameter sort_param, where it is not valid (#3447) --- cps/render_template.py | 20 ++++++++++---------- cps/templates/layout.html | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cps/render_template.py b/cps/render_template.py index 0f0a2894c..03230ecd8 100644 --- a/cps/render_template.py +++ b/cps/render_template.py @@ -47,7 +47,7 @@ def get_sidebar_config(kwargs=None): if current_user.role_admin(): sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list', "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}) else: 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", "show_text": _('Show Random Books'), "config_show": True}) 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}) 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}) 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}) sidebar.append( {"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}) sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang", "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}) 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}) 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}) sidebar.append( {"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: sidebar.append( {"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", - "show_text": _('Show Books List'), "config_show": content}) + "visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous), + "show_text": _('Show Books List'), "config_show": content, "no_param":True}) 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() diff --git a/cps/templates/layout.html b/cps/templates/layout.html index f826aa51b..f0503b15b 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -147,7 +147,7 @@ {% for element in sidebar %} {% if current_user.check_visibility(element['visibility']) and element['public'] %} - + {% endif %} {% endfor %} {% if current_user.is_authenticated or g.allow_anonymous %} From d47af66499f27e094c4a4200c881b7af04584a73 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 24 Jan 2026 13:01:12 +0100 Subject: [PATCH 4/4] Bugfix after testrun --- cps/templates/layout.html | 2 +- test/Calibre-Web TestSummary_Linux.html | 886 +++++++++++++----------- 2 files changed, 469 insertions(+), 419 deletions(-) diff --git a/cps/templates/layout.html b/cps/templates/layout.html index f0503b15b..b7962fc7c 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -147,7 +147,7 @@ {% for element in sidebar %} {% if current_user.check_visibility(element['visibility']) and element['public'] %} - + {% endif %} {% endfor %} {% if current_user.is_authenticated or g.allow_anonymous %} diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 3a16c8331..36d92bde6 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,14 +37,14 @@
-

Start Time: 2026-01-07 20:39:44

+

Start Time: 2026-01-18 16:10:38

-

Stop Time: 2026-01-08 04:01:04

+

Stop Time: 2026-01-18 23:30:52

@@ -102,11 +102,11 @@ - + TestAnonymous 13 - 13 - 0 + 12 + 1 0 0 @@ -206,11 +206,31 @@ - +
TestAnonymous - test_guest_random_books_available
- PASS + +
+ FAIL +
+ + + + @@ -1023,11 +1043,11 @@ - + TestEditAdditionalBooks 18 - 18 - 0 + 17 + 1 0 0 @@ -1073,11 +1093,31 @@ - +
TestEditAdditionalBooks - test_details_popup
- PASS + +
+ FAIL +
+ + + + @@ -2537,11 +2577,11 @@ - + TestErrorReadColumn 2 - 2 - 0 + 1 + 1 0 0 @@ -2560,11 +2600,31 @@ - +
TestErrorReadColumn - test_invalid_custom_read_column
- PASS + +
+ FAIL +
+ + + + @@ -3088,11 +3148,11 @@ - + TestCalibreWebListOrders 16 - 16 - 0 + 14 + 2 0 0 @@ -3156,20 +3216,64 @@ - +
TestCalibreWebListOrders - test_order_authors_all_links
- PASS + +
+ FAIL +
+ + + + - +
TestCalibreWebListOrders - test_order_series_all_links
- PASS + +
+ FAIL +
+ + + + @@ -3975,13 +4079,13 @@ - + TestUploadPDF 1 - 0 1 0 0 + 0 Detail @@ -3989,33 +4093,11 @@ - +
TestUploadPDF - test_upload_invalid_pdf
- -
- FAIL -
- - - - + PASS @@ -4321,11 +4403,11 @@ AssertionError: False is not true - + TestShelf 17 - 15 - 1 + 16 + 0 0 1 @@ -4353,31 +4435,11 @@ AssertionError: False is not true - +
TestShelf - test_adv_search_shelf
- -
- FAIL -
- - - - + PASS @@ -4885,11 +4947,11 @@ AssertionError: 7 != 2 - + TestUploadAudio 12 - 1 - 11 + 12 + 0 0 0 @@ -4899,321 +4961,101 @@ AssertionError: 7 != 2 - +
TestUploadAudio - test_upload_aac
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_aiff
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_asf
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_flac
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_m4a
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_m4b
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_mp3
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_mp4
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_oggvorbis
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_ogv
- -
- FAIL -
- - - - + PASS - +
TestUploadAudio - test_upload_opus
- -
- FAIL -
- - - - + PASS @@ -5228,11 +5070,11 @@ AssertionError: 0.004005578839723024 != 0.0 within 0.001 delta (0.00400557883972 - + TestUploadEPubs 6 - 4 - 2 + 6 + 0 0 0 @@ -5260,31 +5102,11 @@ AssertionError: 0.004005578839723024 != 0.0 within 0.001 delta (0.00400557883972 - +
TestUploadEPubs - test_upload_epub_cover_formats
- -
- FAIL -
- - - - + PASS @@ -5307,31 +5129,11 @@ AssertionError: 0.004005578839723024 != 0.0 within 0.0001 delta (0.0040055788397 - +
TestUploadEPubs - test_upload_epub_lang
- -
- FAIL -
- - - - + PASS @@ -5538,12 +5340,12 @@ AssertionError: 12 != 1 - + TestUserTemplate 21 - 21 - 0 + 20 0 + 1 0 Detail @@ -5669,11 +5471,32 @@ AssertionError: 12 != 1 - +
TestUserTemplate - test_limit_book_languages
- PASS + +
+ ERROR +
+ + + + @@ -5742,12 +5565,12 @@ AssertionError: 12 != 1 - + TestCalibreWebVisibilitys 35 - 35 - 0 - 0 + 24 + 10 + 1 0 Detail @@ -5855,11 +5678,31 @@ AssertionError: 12 != 1 - +
TestCalibreWebVisibilitys - test_admin_change_visibility_random
- PASS + +
+ FAIL +
+ + + + @@ -5882,11 +5725,33 @@ AssertionError: 12 != 1 - +
TestCalibreWebVisibilitys - test_admin_change_visibility_read
- PASS + +
+ FAIL +
+ + + + @@ -5918,20 +5783,63 @@ AssertionError: 12 != 1 - +
TestCalibreWebVisibilitys - test_archive_books
- PASS + +
+ FAIL +
+ + + + - +
TestCalibreWebVisibilitys - test_authors_max_settings
- PASS + +
+ ERROR +
+ + + + @@ -5963,29 +5871,89 @@ AssertionError: 12 != 1 - +
TestCalibreWebVisibilitys - test_link_column_to_read_status
- PASS + +
+ FAIL +
+ + + + - +
TestCalibreWebVisibilitys - test_random_books_available
- PASS + +
+ FAIL +
+ + + + - +
TestCalibreWebVisibilitys - test_read_status_visible
- PASS + +
+ FAIL +
+ + + + @@ -5999,20 +5967,60 @@ AssertionError: 12 != 1 - +
TestCalibreWebVisibilitys - test_restrict_columns
- PASS + +
+ FAIL +
+ + + + - +
TestCalibreWebVisibilitys - test_restrict_tags
- PASS + +
+ FAIL +
+ + + + @@ -6035,20 +6043,62 @@ AssertionError: 12 != 1 - +
TestCalibreWebVisibilitys - test_search_order
- PASS + +
+ FAIL +
+ + + + - +
TestCalibreWebVisibilitys - test_search_string
- PASS + +
+ FAIL +
+ + + + @@ -6233,9 +6283,9 @@ AssertionError: 12 != 1 Total 538 - 516 + 514 15 - 0 + 2 7   @@ -6438,19 +6488,19 @@ AssertionError: 12 != 1 Werkzeug - 3.1.4 + 3.1.5 Basic google-api-python-client - 2.187.0 + 2.188.0 TestBackupMetadataGdrive httplib2 - 0.31.0 + 0.31.1 TestBackupMetadataGdrive @@ -6474,13 +6524,13 @@ AssertionError: 12 != 1 google-api-python-client - 2.187.0 + 2.188.0 TestCliGdrivedb httplib2 - 0.31.0 + 0.31.1 TestCliGdrivedb @@ -6504,13 +6554,13 @@ AssertionError: 12 != 1 google-api-python-client - 2.187.0 + 2.188.0 TestEbookConvertCalibreGDrive httplib2 - 0.31.0 + 0.31.1 TestEbookConvertCalibreGDrive @@ -6534,13 +6584,13 @@ AssertionError: 12 != 1 google-api-python-client - 2.187.0 + 2.188.0 TestEbookConvertGDriveKepubify httplib2 - 0.31.0 + 0.31.1 TestEbookConvertGDriveKepubify @@ -6576,13 +6626,13 @@ AssertionError: 12 != 1 google-api-python-client - 2.187.0 + 2.188.0 TestEditAuthorsGdrive httplib2 - 0.31.0 + 0.31.1 TestEditAuthorsGdrive @@ -6612,13 +6662,13 @@ AssertionError: 12 != 1 google-api-python-client - 2.187.0 + 2.188.0 TestEditBooksOnGdrive httplib2 - 0.31.0 + 0.31.1 TestEditBooksOnGdrive @@ -6654,13 +6704,13 @@ AssertionError: 12 != 1 google-api-python-client - 2.187.0 + 2.188.0 TestEmbedMetadataGdrive httplib2 - 0.31.0 + 0.31.1 TestEmbedMetadataGdrive @@ -6684,13 +6734,13 @@ AssertionError: 12 != 1 google-api-python-client - 2.187.0 + 2.188.0 TestSetupGdrive httplib2 - 0.31.0 + 0.31.1 TestSetupGdrive @@ -6786,7 +6836,7 @@ AssertionError: 12 != 1