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 @@