diff --git a/cps/clean_html.py b/cps/clean_html.py index 3436f43a5..06c3ee8a6 100644 --- a/cps/clean_html.py +++ b/cps/clean_html.py @@ -36,7 +36,12 @@ def clean_string(unsafe_text, book_id=0): if bleach: allowed_tags = list(ALLOWED_TAGS) allowed_tags.extend(["p", "span", "div", "pre", "br", "h1", "h2", "h3", "h4", "h5", "h6", "img"]) - safe_text = clean_html(unsafe_text, tags=set(allowed_tags)) + allowed_attributes = { + "*": ["class", "style"], + "a": ["href", "title", "rel"], + "img": ["src", "alt", "title", "width", "height"], + } + safe_text = clean_html(unsafe_text, tags=set(allowed_tags), attributes=allowed_attributes) else: safe_text = clean_html(unsafe_text) except ParserError as e: diff --git a/cps/db.py b/cps/db.py index feee7a8fc..92e6380ed 100644 --- a/cps/db.py +++ b/cps/db.py @@ -753,7 +753,8 @@ class CalibreDB: .filter(self.common_filters(allow_show_archived)).first()) def get_book_by_uuid(self, book_uuid): - return self.session.query(Books).filter(Books.uuid == book_uuid).first() + return self.session.query(Books).filter(Books.uuid == book_uuid). \ + filter(self.common_filters()).first() def get_book_format(self, book_id, file_format): return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first() @@ -1125,7 +1126,7 @@ class CalibreDB: .filter(self.common_filters()) .count()) if no_lang_count: - tags.append([Category(_("None"), None, "none"), no_lang_count]) + tags.append([Category(_("None"), "None", "none"), no_lang_count]) return sorted(tags, key=lambda x: x[0].name.lower(), reverse=reverse_order) else: if not languages: diff --git a/cps/server.py b/cps/server.py index bf53a3d46..afdf4fa65 100644 --- a/cps/server.py +++ b/cps/server.py @@ -41,7 +41,7 @@ except ImportError: VERSION = 'Tornado ' + _version _GEVENT = False -from . import logger +from . import logger, constants log = logger.create() @@ -216,9 +216,10 @@ class WebServer(object): try: sock, output = self._make_gevent_listener() log.info('Starting Gevent server on %s', output) - # Also print to stdout so interactive terminals show a clear success message try: - print(f"Calibre-Web: server started on {output}") + # Also print to stdout so interactive terminals show a clear success message + if constants.APP_MODE not in ['development', 'test']: + print(f"Calibre-Web: server started on {output}") except Exception: print(f"Calibre-Web: error {output}") pass @@ -274,7 +275,8 @@ class WebServer(object): log.info('Starting Tornado server on %s', output) # Also print to stdout so interactive terminals show a clear success message try: - print(f"Calibre-Web: server started on {output}") + if constants.APP_MODE not in ['development', 'test']: + print(f"Calibre-Web: server started on {output}") except Exception: print(f"Calibre-Web: error {output}") pass diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 164ac424d..638b28bf7 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -232,7 +232,7 @@ {% elif c.datatype == 'datetime' %} {{ column.value|formatdate }} {% elif c.datatype == 'comments' %} - {{ column.value|safe }} + {{ column.value|clean_string|safe }} {% elif c.datatype == 'series' %} {{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }} {% elif c.datatype == 'text' %} diff --git a/cps/templates/feed.xml b/cps/templates/feed.xml index 6627daac4..70ebc14b7 100644 --- a/cps/templates/feed.xml +++ b/cps/templates/feed.xml @@ -94,7 +94,7 @@ {% elif c.datatype == 'datetime' %} {{ column.value|formatdate }} {% elif c.datatype == 'comments' %} - {{ column.value|safe }} + {{ column.value|clean_string|safe }} {% elif c.datatype == 'series' %} {{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }} {% elif c.datatype == 'text' %} diff --git a/cps/templates/listenmp3.html b/cps/templates/listenmp3.html index 375a871b4..836c80928 100644 --- a/cps/templates/listenmp3.html +++ b/cps/templates/listenmp3.html @@ -134,7 +134,7 @@ {% elif c.datatype == 'datetime' %} {{ column.value|formatdate }} {% elif c.datatype == 'comments' %} - {{column.value|safe}} + {{column.value|clean_string|safe}} {% elif c.datatype == 'series' %} {{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }} {% elif c.datatype == 'text' %} diff --git a/cps/ub.py b/cps/ub.py index 20f0ac911..3aa8a9324 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -537,7 +537,7 @@ class RemoteAuthToken(Base): def __init__(self): super().__init__() - self.auth_token = (hexlify(os.urandom(4))).decode('utf-8') + self.auth_token = (hexlify(os.urandom(16))).decode('utf-8') self.expiration = datetime.now() + timedelta(minutes=10) # 10 min from now def __repr__(self): diff --git a/cps/web.py b/cps/web.py index 6c3f1f76c..fc5288591 100644 --- a/cps/web.py +++ b/cps/web.py @@ -711,7 +711,7 @@ def render_language_books(page, name, order): lang_name = _("None") except KeyError: abort(404) - if name == "none": + if name.lower() == "none": entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Languages.lang_code == None, diff --git a/pyproject.toml b/pyproject.toml index 67a7acca7..55fbe7b3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ description = "Web app for browsing, reading and downloading eBooks stored in a authors = [{name = "@OzzieIsaacs", email = "Ozzie.Fernandez.Isaacs@googlemail.com"}] maintainers = [{name = "@OzzieIsaacs"}] license = "GPL-3.0-or-later" +license-files = ["LICENSE"] classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", @@ -17,6 +18,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", ] keywords = [ @@ -40,7 +42,7 @@ dependencies = [ "tornado>=6.4.2,<6.6", "Wand>=0.4.4,<0.8.0", "unidecode>=0.04.19,<1.5.0", - "lxml>=4.9.1,<5.4.0", + "lxml>=4.9.1,<6.2.0", "flask-wtf>=0.14.2,<1.3.0", "chardet>=3.0.0,<5.3.0", "netifaces-plus>=0.12.0,<0.13.0", @@ -118,12 +120,10 @@ kobo = [ ] [project.scripts] -cps = "calibreweb:main" +cps = "calibreweb.__main__:main" [tool.setuptools] include-package-data = true -license-files = ["LICENSE"] [tool.setuptools.dynamic] version = {attr = "calibreweb.cps.constants.STABLE_VERSION"} - diff --git a/requirements.txt b/requirements.txt index 8ac124e2a..80060e288 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ SQLAlchemy>=1.3.0,<2.1.0 tornado>=6.4.2,<6.6 Wand>=0.4.4,<0.8.0 unidecode>=0.04.19,<1.5.0 -lxml>=4.9.1,<5.4.0 +lxml>=4.9.1,<6.2.0 flask-wtf>=0.14.2,<1.3.0 chardet>=3.0.0,<5.3.0 netifaces-plus>=0.12.0,<0.13.0 diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 7a0eb6695..d1e2a406d 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2026-04-25 16:06:06

+

Start Time: 2026-05-16 12:48:02

-

Stop Time: 2026-04-25 23:02:36

+

Stop Time: 2026-05-16 20:11:29

-

Duration: 5h 43 min

+

Duration: 6h 9 min

@@ -102,15 +102,15 @@ - + TestAnonymous - 21 - 5 - 6 - 10 + 13 + 13 + 0 + 0 0 - Detail + Detail @@ -161,476 +161,74 @@ - +
TestAnonymous - test_guest_change_visibility_language
- -
- ERROR -
- - - - + PASS - - -
TestAnonymous - test_guest_change_visibility_language
- - -
- ERROR -
- - - - - - - - - +
TestAnonymous - test_guest_change_visibility_publisher
- -
- FAIL -
- - - - + PASS - - -
TestAnonymous - test_guest_change_visibility_publisher
- - -
- ERROR -
- - - - - - - - - +
TestAnonymous - test_guest_change_visibility_rated
- -
- FAIL -
- - - - + PASS - - -
TestAnonymous - test_guest_change_visibility_rated
- - -
- ERROR -
- - - - - - - - - +
TestAnonymous - test_guest_change_visibility_rating
- -
- FAIL -
- - - - + PASS - - -
TestAnonymous - test_guest_change_visibility_rating
- - -
- ERROR -
- - - - - - - - - +
TestAnonymous - test_guest_change_visibility_series
- -
- FAIL -
- - - - + PASS - - -
TestAnonymous - test_guest_change_visibility_series
- - -
- ERROR -
- - - - - - - - - +
TestAnonymous - test_guest_random_books_available
- -
- FAIL -
- - - - + PASS - - -
TestAnonymous - test_guest_random_books_available
- - -
- ERROR -
- - - - - - - - - +
TestAnonymous - test_guest_restricted_settings_visibility
- -
- ERROR -
- - - - + PASS - - -
TestAnonymous - test_guest_restricted_settings_visibility
- - -
- ERROR -
- - - - - - - - - +
TestAnonymous - test_guest_visibility_sidebar
- -
- FAIL -
- - - - - - - - - - -
TestAnonymous - test_guest_visibility_sidebar
- - -
- ERROR -
- - - - + PASS @@ -1602,12 +1200,12 @@ AttributeError: 'bool' object has no attribute 'click' - + TestEditBooks 38 - 36 + 37 + 0 0 - 1 1 Detail @@ -1733,32 +1331,11 @@ AttributeError: 'bool' object has no attribute 'click' - +
TestEditBooks - test_edit_language
- -
- ERROR -
- - - - + PASS @@ -2267,12 +1844,12 @@ IndexError: list index out of range - + TestEditBooksList 20 - 12 - 3 - 5 + 20 + 0 + 0 0 Detail @@ -2389,234 +1966,114 @@ IndexError: list index out of range - +
TestEditBooksList - test_bookslist_edit_languages
- -
- FAIL -
- - - - + PASS - +
TestEditBooksList - test_bookslist_edit_publisher
- -
- ERROR -
- - - - + PASS - +
TestEditBooksList - test_bookslist_edit_series
- -
- ERROR -
- - - - + PASS - +
TestEditBooksList - test_bookslist_edit_seriesindex
- -
- ERROR -
- - - - + PASS - +
TestEditBooksList - test_bookslist_edit_title
- -
- ERROR -
- - - - + PASS - +
TestEditBooksList - test_list_visibility
- -
- FAIL -
- - - - + PASS - +
TestEditBooksList - test_restricted_rights
- -
- ERROR -
- - - - + PASS - +
TestEditBooksList - test_search_books_list
+ PASS + + + + + + + TestLoadMetadata + 1 + 0 + 0 + 1 + 0 + + Detail + + + + + + + +
TestLoadMetadata - test_load_metadata
+
- FAIL + ERROR
-