diff --git a/SECURITY.md b/SECURITY.md index dc763184..afaf9b0b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,14 +16,14 @@ To receive fixes for security vulnerabilities it is required to always upgrade t | V 0.6.7 |Hardcoded secret key for sessions |CVE-2020-12627 | | V 0.6.13|Calibre-Web Metadata cross site scripting |CVE-2021-25964| | V 0.6.13|Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo|| -| V 0.6.13|JavaScript could get executed in the description field. Thanks to @ranjit-git || +| V 0.6.13|JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource)|| | V 0.6.13|JavaScript could get executed in a custom column of type "comment" field || | V 0.6.13|JavaScript could get executed after converting a book to another format with a title containing javascript code|| | V 0.6.13|JavaScript could get executed after converting a book to another format with a username containing javascript code|| | V 0.6.13|JavaScript could get executed in the description series, categories or publishers title|| | V 0.6.13|JavaScript could get executed in the shelf title|| | V 0.6.13|Login with the old session cookie after logout. Thanks to @ibarrionuevo|| -| V 0.6.14|CSRF was possible. Thanks to @mik317 || +| V 0.6.14|CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) || | V 0.6.14|Cross-Site Scripting vulnerability on typeahead inputs. Thanks to @notdodo|| diff --git a/cps/__init__.py b/cps/__init__.py index e50a74cf..a1a721c7 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -35,6 +35,7 @@ from flask_principal import Principal from . import config_sql, logger, cache_buster, cli, ub, db from .reverseproxy import ReverseProxied from .server import WebServer +from .dep_check import dependency_check try: import lxml @@ -100,6 +101,7 @@ _BABEL_TRANSLATIONS = set() log = logger.create() + from . import services db.CalibreDB.update_config(config) @@ -126,7 +128,11 @@ def create_app(): print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***') web_server.stop(True) sys.exit(7) - + for res in dependency_check() + dependency_check(True): + log.info('*** "{}" version does not fit the requirements. Should: {}, Found: {}, please consider updating. ***' + .format(res['name'], + res['target'], + res['found'])) app.wsgi_app = ReverseProxied(app.wsgi_app) if os.environ.get('FLASK_DEBUG'): diff --git a/cps/db.py b/cps/db.py index 456713cd..6ffc682f 100644 --- a/cps/db.py +++ b/cps/db.py @@ -780,7 +780,7 @@ class CalibreDB(): # read search results from calibre-database and return it (function is used for feed and simple search def get_search_results(self, term, offset=None, order=None, limit=None, *join): - order = order or [Books.sort] + order = order[0] or [Books.sort] pagination = None result = self.search_query(term, *join).order_by(*order).all() result_count = len(result) diff --git a/cps/dep_check.py b/cps/dep_check.py new file mode 100644 index 00000000..9a982fec --- /dev/null +++ b/cps/dep_check.py @@ -0,0 +1,83 @@ +import os +import re + +from .constants import BASE_DIR +try: + from importlib_metadata import version + importlib = True + ImportNotFound = BaseException +except ImportError: + importlib = False + + +if not importlib: + try: + import pkg_resources + from pkg_resources import DistributionNotFound as ImportNotFound + pkgresources = True + except ImportError as e: + pkgresources = False + +def dependency_check(optional=False): + dep = list() + if importlib or pkgresources: + if optional: + req_path = os.path.join(BASE_DIR, "optional-requirements.txt") + else: + req_path = os.path.join(BASE_DIR, "requirements.txt") + if os.path.exists(req_path): + try: + with open(req_path, 'r') as f: + for line in f: + if not line.startswith('#') and not line == '\n' and not line.startswith('git'): + res = re.match(r'(.*?)([<=>\s]+)([\d\.]+),?\s?([<=>\s]+)?([\d\.]+)?', line.strip()) + try: + if importlib: + dep_version = version(res.group(1)) + else: + dep_version = pkg_resources.get_distribution(res.group(1)).version + except ImportNotFound: + if optional: + continue + else: + return [{'name':res.group(1), + 'target': "available", + 'found': "Not available" + }] + + if res.group(2).strip() == "==": + if dep_version.split('.') != res.group(3).split('.'): + dep.append({'name': res.group(1), + 'found': dep_version, + "target": res.group(2) + res.group(3)}) + continue + elif res.group(2).strip() == ">=": + if dep_version.split('.') < res.group(3).split('.'): + dep.append({'name': res.group(1), + 'found': dep_version, + "target": res.group(2) + res.group(3)}) + continue + elif res.group(2).strip() == ">": + if dep_version.split('.') <= res.group(3).split('.'): + dep.append({'name': res.group(1), + 'found': dep_version, + "target": res.group(2) + res.group(3)}) + continue + if res.group(4) and res.group(5): + if res.group(4).strip() == "<": + if dep_version.split('.') >= res.group(5).split('.'): + dep.append( + {'name': res.group(1), + 'found': dep_version, + "target": res.group(4) + res.group(5)}) + continue + elif res.group(2).strip() == "<=": + if dep_version.split('.') > res.group(5).split('.'): + dep.append( + {'name': res.group(1), + 'found': dep_version, + "target": res.group(4) + res.group(5)}) + continue + except Exception as e: + print(e) + return dep diff --git a/cps/static/js/filter_grid.js b/cps/static/js/filter_grid.js index d84cf57a..14d60f27 100644 --- a/cps/static/js/filter_grid.js +++ b/cps/static/js/filter_grid.js @@ -30,6 +30,9 @@ $("#desc").click(function() { if (direction === 0) { return; } + $("#asc").removeClass("active"); + $("#desc").addClass("active"); + var page = $(this).data("id"); $.ajax({ method:"post", @@ -50,6 +53,9 @@ $("#asc").click(function() { if (direction === 1) { return; } + $("#desc").removeClass("active"); + $("#asc").addClass("active"); + var page = $(this).data("id"); $.ajax({ method:"post", @@ -66,6 +72,8 @@ $("#asc").click(function() { }); $("#all").click(function() { + $(".char").removeClass("active"); + $("#all").addClass("active"); // go through all elements and make them visible $list.isotope({ filter: function() { return true; @@ -74,6 +82,9 @@ $("#all").click(function() { }); $(".char").click(function() { + $(".char").removeClass("active"); + $(this).addClass("active"); + $("#all").removeClass("active"); var character = this.innerText; $list.isotope({ filter: function() { return this.attributes["data-id"].value.charAt(0).toUpperCase() === character; diff --git a/cps/static/js/filter_list.js b/cps/static/js/filter_list.js index e76e6147..747f98fa 100644 --- a/cps/static/js/filter_list.js +++ b/cps/static/js/filter_list.js @@ -19,6 +19,7 @@ var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending or var sort = 0; // Show sorted entries $("#sort_name").click(function() { + $("#sort_name").toggleClass("active"); var className = $("h1").attr("Class") + "_sort_name"; var obj = {}; obj[className] = sort; @@ -68,6 +69,9 @@ $("#desc").click(function() { if (direction === 0) { return; } + $("#asc").removeClass("active"); + $("#desc").addClass("active"); + var page = $(this).data("id"); $.ajax({ method:"post", @@ -112,10 +116,12 @@ $("#desc").click(function() { $("#asc").click(function() { - if (direction === 1) { return; } + $("#desc").removeClass("active"); + $("#asc").addClass("active"); + var page = $(this).data("id"); $.ajax({ method:"post", @@ -159,6 +165,8 @@ $("#asc").click(function() { }); $("#all").click(function() { + $("#all").addClass("active"); + $(".char").removeClass("active"); var cnt = $("#second").contents(); $("#list").append(cnt); // Find count of middle element @@ -176,6 +184,9 @@ $("#all").click(function() { }); $(".char").click(function() { + $(".char").removeClass("active"); + $(this).addClass("active"); + $("#all").removeClass("active"); var character = this.innerText; var count = 0; var index = 0; diff --git a/cps/static/js/get_meta.js b/cps/static/js/get_meta.js index f64be699..51ab740d 100644 --- a/cps/static/js/get_meta.js +++ b/cps/static/js/get_meta.js @@ -28,14 +28,17 @@ $(function () { function populateForm (book) { tinymce.get("description").setContent(book.description); - var uniqueTags = []; + var uniqueTags = $.map($("#tags").val().split(","), $.trim); + if ( uniqueTags.length == 1 && uniqueTags[0] == "") { + uniqueTags = []; + } $.each(book.tags, function(i, el) { if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el); }); var ampSeparatedAuthors = (book.authors || []).join(" & "); $("#bookAuthor").val(ampSeparatedAuthors); $("#book_title").val(book.title); - $("#tags").val(uniqueTags.join(",")); + $("#tags").val(uniqueTags.join(", ")); $("#rating").data("rating").setValue(Math.round(book.rating)); if(book.cover !== null){ $(".cover img").attr("src", book.cover); diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py index 08fb1644..ada53005 100644 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -151,6 +151,7 @@ class TaskConvert(CalibreTask): local_db.session.rollback() log.error("Database error: %s", e) local_db.session.close() + self._handleError(error_message) return self.results['path'] = cur_book.path self.title = cur_book.title diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 9a941594..f99c0938 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -16,7 +16,9 @@ {{_('Downloads')}} {{_('Admin')}} {{_('Password')}} + {% if config.config_upload %} {{_('Upload')}} + {% endif %} {{_('Download')}} {{_('View Books')}} {{_('Edit')}} @@ -32,7 +34,9 @@ {{user.downloads.count()}} {{ display_bool_setting(user.role_admin()) }} {{ display_bool_setting(user.role_passwd()) }} + {% if config.config_upload %} {{ display_bool_setting(user.role_upload()) }} + {% endif %} {{ display_bool_setting(user.role_download()) }} {{ display_bool_setting(user.role_viewer()) }} {{ display_bool_setting(user.role_edit()) }} diff --git a/cps/templates/author.html b/cps/templates/author.html index 4ae2df42..f2b71eab 100644 --- a/cps/templates/author.html +++ b/cps/templates/author.html @@ -23,12 +23,12 @@

{{_("In Library")}}

{% endif %}
{% if entries[0] %} diff --git a/cps/templates/config_view_edit.html b/cps/templates/config_view_edit.html index ebfb5362..32509e88 100644 --- a/cps/templates/config_view_edit.html +++ b/cps/templates/config_view_edit.html @@ -95,17 +95,21 @@
+ {% if config.config_upload %}
+ {% endif %}
- +
-
- - +
+
+ + +
diff --git a/cps/templates/grid.html b/cps/templates/grid.html index b1cc64ec..b9d40961 100644 --- a/cps/templates/grid.html +++ b/cps/templates/grid.html @@ -4,20 +4,20 @@ {% if entries[0] %} @@ -27,7 +27,7 @@
- {{ entry[0].name }} + {{ entry[0].series[0].name }} {{entry.count}} diff --git a/cps/templates/index.html b/cps/templates/index.html index 4c67e362..162adc7d 100644 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -65,18 +65,23 @@

{{title}}

diff --git a/cps/templates/list.html b/cps/templates/list.html index 48b90c8a..71dbea11 100644 --- a/cps/templates/list.html +++ b/cps/templates/list.html @@ -5,16 +5,16 @@ + {% if config.config_upload %}
+ {% endif %}
- +
-
- - +
+
+ + +
- {% if not content.role_anonymous() %} -
- - -
-
- - -
- {% endif %} + {% if not content.role_anonymous() %} +
+ + +
+
+ + +
{% endif %} + {% endif %} {% if kobo_support and not content.role_anonymous() %}
diff --git a/cps/web.py b/cps/web.py index 8d3e4551..940897d3 100644 --- a/cps/web.py +++ b/cps/web.py @@ -339,7 +339,7 @@ def get_matching_tags(): def get_sort_function(sort, data): - order = [db.Books.timestamp.desc()] + order = [db.Books.sort] if sort == 'stored': sort = current_user.get_view_property(data, 'stored') else: @@ -364,7 +364,13 @@ def get_sort_function(sort, data): order = [db.Books.series_index.asc()] if sort == 'seriesdesc': order = [db.Books.series_index.desc()] - return order + if sort == 'hotdesc': + order = [func.count(ub.Downloads.book_id).desc()] + if sort == 'hotasc': + order = [func.count(ub.Downloads.book_id).asc()] + if sort is None: + sort = "abc" + return order, sort def render_books_list(data, sort, book_id, page): @@ -378,7 +384,7 @@ def render_books_list(data, sort, book_id, page): elif data == "read": return render_read_books(page, True, order=order) elif data == "hot": - return render_hot_books(page) + return render_hot_books(page, order) elif data == "download": return render_downloaded_books(page, order, book_id) elif data == "author": @@ -407,12 +413,12 @@ def render_books_list(data, sort, book_id, page): return render_adv_search_results(term, offset, order, config.config_books_per_page) else: website = data or "newest" - entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order, + entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order[0], db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Books"), page=website) + title=_(u"Books"), page=website, order=order[1]) def render_rated_books(page, book_id, order): @@ -420,13 +426,13 @@ def render_rated_books(page, book_id, order): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.ratings.any(db.Ratings.rating > 9), - order, + order[0], db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - id=book_id, title=_(u"Top Rated Books"), page="rated") + id=book_id, title=_(u"Top Rated Books"), page="rated", order=order[1]) else: abort(404) @@ -440,16 +446,21 @@ def render_discover_books(page, book_id): else: abort(404) -def render_hot_books(page): +def render_hot_books(page, order): if current_user.check_visibility(constants.SIDEBAR_HOT): + if order[1] not in ['hotasc', 'hotdesc']: + # Unary expression comparsion only working (for this expression) in sqlalchemy 1.4+ + #if not (order[0][0].compare(func.count(ub.Downloads.book_id).desc()) or + # order[0][0].compare(func.count(ub.Downloads.book_id).asc())): + order = [func.count(ub.Downloads.book_id).desc()], 'hotdesc' if current_user.show_detail_random(): random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \ .order_by(func.random()).limit(config.config_random_books) else: random = false() off = int(int(config.config_books_per_page) * (page - 1)) - all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)).order_by( - func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id) + all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id))\ + .order_by(*order[0]).group_by(ub.Downloads.book_id) hot_books = all_books.offset(off).limit(config.config_books_per_page) entries = list() for book in hot_books: @@ -462,7 +473,7 @@ def render_hot_books(page): numBooks = entries.__len__() pagination = Pagination(page, config.config_books_per_page, numBooks) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Hot Books (Most Downloaded)"), page="hot") + title=_(u"Hot Books (Most Downloaded)"), page="hot", order=order[1]) else: abort(404) @@ -483,7 +494,10 @@ def render_downloaded_books(page, order, user_id): 0, db.Books, ub.Downloads.user_id == user_id, - order, + order[0], + db.books_series_link, + db.Books.id == db.books_series_link.c.book, + db.Series, ub.Downloads, db.Books.id == ub.Downloads.book_id) for book in entries: if not calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \ @@ -496,7 +510,8 @@ def render_downloaded_books(page, order, user_id): pagination=pagination, id=user_id, title=_(u"Downloaded books by %(user)s",user=user.name), - page="download") + page="download", + order=order[1]) else: abort(404) @@ -505,7 +520,7 @@ def render_author_books(page, author_id, order): entries, __, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.authors.any(db.Authors.id == author_id), - [order[0], db.Series.name, db.Books.series_index], + [order[0][0], db.Series.name, db.Books.series_index], db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series) @@ -527,7 +542,7 @@ def render_author_books(page, author_id, order): return render_title_template('author.html', entries=entries, pagination=pagination, id=author_id, title=_(u"Author: %(name)s", name=author_name), author=author_info, - other_books=other_books, page="author") + other_books=other_books, page="author", order=order[1]) def render_publisher_books(page, book_id, order): @@ -536,12 +551,14 @@ def render_publisher_books(page, book_id, order): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.publishers.any(db.Publishers.id == book_id), - [db.Series.name, order[0], db.Books.series_index], + [db.Series.name, order[0][0], db.Books.series_index], db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id, - title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher") + title=_(u"Publisher: %(name)s", name=publisher.name), + page="publisher", + order=order[1]) else: abort(404) @@ -552,9 +569,9 @@ def render_series_books(page, book_id, order): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.series.any(db.Series.id == book_id), - [order[0]]) + [order[0][0]]) return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id, - title=_(u"Series: %(serie)s", serie=name.name), page="series") + title=_(u"Series: %(serie)s", serie=name.name), page="series", order=order[1]) else: abort(404) @@ -564,10 +581,12 @@ def render_ratings_books(page, book_id, order): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.ratings.any(db.Ratings.id == book_id), - [order[0]]) + [order[0][0]]) if name and name.rating <= 10: return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id, - title=_(u"Rating: %(rating)s stars", rating=int(name.rating / 2)), page="ratings") + title=_(u"Rating: %(rating)s stars", rating=int(name.rating / 2)), + page="ratings", + order=order[1]) else: abort(404) @@ -578,9 +597,11 @@ def render_formats_books(page, book_id, order): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.data.any(db.Data.format == book_id.upper()), - [order[0]]) + [order[0][0]]) return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id, - title=_(u"File format: %(format)s", format=name.format), page="formats") + title=_(u"File format: %(format)s", format=name.format), + page="formats", + order=order[1]) else: abort(404) @@ -591,12 +612,12 @@ def render_category_books(page, book_id, order): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.tags.any(db.Tags.id == book_id), - [order[0], db.Series.name, db.Books.series_index], + [order[0][0], db.Series.name, db.Books.series_index], db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id, - title=_(u"Category: %(name)s", name=name.name), page="category") + title=_(u"Category: %(name)s", name=name.name), page="category", order=order[1]) else: abort(404) @@ -610,13 +631,13 @@ def render_language_books(page, name, order): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.languages.any(db.Languages.lang_code == name), - [order[0]]) + [order[0][0]]) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name, - title=_(u"Language: %(name)s", name=lang_name), page="language") + title=_(u"Language: %(name)s", name=lang_name), page="language", order=order[1]) def render_read_books(page, are_read, as_xml=False, order=None): - order = order or [] + sort = order[0] or [] if not config.config_read_column: if are_read: db_filter = and_(ub.ReadBook.user_id == int(current_user.id), @@ -626,7 +647,7 @@ def render_read_books(page, are_read, as_xml=False, order=None): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db_filter, - order, + sort, db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series, @@ -640,7 +661,7 @@ def render_read_books(page, are_read, as_xml=False, order=None): entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db_filter, - order, + sort, db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series, @@ -663,11 +684,11 @@ def render_read_books(page, are_read, as_xml=False, order=None): name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')' pagename = "unread" return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=name, page=pagename) + title=name, page=pagename, order=order[1]) -def render_archived_books(page, order): - order = order or [] +def render_archived_books(page, sort): + order = sort[0] or [] archived_books = ( ub.session.query(ub.ArchivedBook) .filter(ub.ArchivedBook.user_id == int(current_user.id)) @@ -687,7 +708,7 @@ def render_archived_books(page, order): name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')' pagename = "archived" return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=name, page=pagename) + title=name, page=pagename, order=sort[1]) def render_prepare_search_form(cc): @@ -732,7 +753,8 @@ def render_search_results(term, offset=None, order=None, limit=None): entries=entries, result_count=result_count, title=_(u"Search"), - page="search") + page="search", + order=order[1]) # ################################### View Books list ################################################################## @@ -931,8 +953,9 @@ def series_list(): return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist, title=_(u"Series"), page="serieslist", data="series", order=order_no) else: - entries = calibre_db.session.query(db.Books, func.count('books_series_link').label('count')) \ - .join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters()) \ + entries = calibre_db.session.query(db.Books, func.count('books_series_link').label('count'), + func.max(db.Books.series_index), db.Books.id) \ + .join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters())\ .group_by(text('books_series_link.series')).order_by(order).all() charlist = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('char')) \ .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \ @@ -1249,7 +1272,7 @@ def extend_search_term(searchterm, def render_adv_search_results(term, offset=None, order=None, limit=None): - order = order or [db.Books.sort] + sort = order[0] or [db.Books.sort] pagination = None cc = get_cc_columns(filter_config_custom_read=True) @@ -1347,7 +1370,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None): log.debug_or_exception(ex) flash(_("Error on search for custom columns, please restart Calibre-Web"), category="error") - q = q.order_by(*order).all() + q = q.order_by(*sort).all() flask_session['query'] = json.dumps(term) ub.store_ids(q) result_count = len(q) @@ -1363,7 +1386,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None): pagination=pagination, entries=q[offset:limit_all], result_count=result_count, - title=_(u"Advanced Search"), page="advsearch") + title=_(u"Advanced Search"), page="advsearch", + order=order[1]) diff --git a/optional-requirements.txt b/optional-requirements.txt index cfa2bfc3..c9a05fc0 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -34,8 +34,8 @@ rarfile>=2.7 scholarly>=1.2.0, <1.3 # other -natsort>=2.2.0,<7.2.0 -comicapi>= 2.2.0,<2.3.0 +natsort>=2.2.0,<8.1.0 +comicapi>=2.2.0,<2.3.0 #Kobo integration jsonschema>=3.2.0,<3.3.0