diff --git a/SECURITY.md b/SECURITY.md index 26ce3c55..54be54bd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -36,6 +36,6 @@ To receive fixes for security vulnerabilities it is required to always upgrade t | V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH || -## Staement regarding Log4j (CVE-2021-44228 and related) +## Statement regarding Log4j (CVE-2021-44228 and related) Calibre-web is not affected by bugs related to Log4j. Calibre-Web is a python program, therefore not using Java, and not using the Java logging feature log4j. diff --git a/cps/admin.py b/cps/admin.py index fea20ddc..b486b689 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -161,7 +161,7 @@ def shutdown(): # needed for docker applications, as changes on metadata.db from host are not visible to application @admi.route("/reconnect", methods=['GET']) def reconnect(): - if cli.args.r: + if cli.reconnect_enable: calibre_db.reconnect_db(config, ub.app_DB_path) return json.dumps({}) else: @@ -1239,7 +1239,7 @@ def _db_configuration_update_helper(): config.store_calibre_uuid(calibre_db, db.LibraryId) # if db changed -> delete shelfs, delete download books, delete read books, kobo sync... if db_change: - log.info("Calibre Database changed, delete all Calibre-Web info related to old Database") + log.info("Calibre Database changed, all Calibre-Web info related to old Database gets deleted") ub.session.query(ub.Downloads).delete() ub.session.query(ub.ArchivedBook).delete() ub.session.query(ub.ReadBook).delete() diff --git a/cps/cli.py b/cps/cli.py index a63d7282..36c03513 100644 --- a/cps/cli.py +++ b/cps/cli.py @@ -84,10 +84,14 @@ if args.k == "": # dry run updater dry_run = args.d or None +# enable reconnect endpoint for docker database reconnect +reconnect_enable = args.r or os.environ.get("CALIBRE_RECONNECT", None) # load covers from localhost -allow_localhost = args.l or None +allow_localhost = args.l or os.environ.get("CALIBRE_LOCALHOST", None) # handle and check ip address argument ip_address = args.i or None + + if ip_address: try: # try to parse the given ip address with socket diff --git a/cps/db.py b/cps/db.py index 2e2a02fc..5049eccb 100644 --- a/cps/db.py +++ b/cps/db.py @@ -620,8 +620,8 @@ class CalibreDB: bd = (self.session.query(Books, read_column.value, ub.ArchivedBook.is_archived).select_from(Books) .join(read_column, read_column.book == book_id, isouter=True)) - except (KeyError, AttributeError): - log.error("Custom Column No.%d is not existing in calibre database", read_column) + except (KeyError, AttributeError, IndexError): + log.error("Custom Column No.{} is not existing in calibre database".format(read_column)) # Skip linking read column and return None instead of read status bd = self.session.query(Books, None, ub.ArchivedBook.is_archived) return (bd.filter(Books.id == book_id) @@ -665,11 +665,11 @@ class CalibreDB: neg_content_cc_filter = false() if neg_cc_list == [''] else \ getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list)) - except (KeyError, AttributeError): + except (KeyError, AttributeError, IndexError): pos_content_cc_filter = false() neg_content_cc_filter = true() - log.error(u"Custom Column No.%d is not existing in calibre database", - self.config.config_restricted_column) + log.error("Custom Column No.{} is not existing in calibre database".format( + self.config.config_restricted_column)) flash(_("Custom Column No.%(column)d is not existing in calibre database", column=self.config.config_restricted_column), category="error") @@ -728,8 +728,8 @@ class CalibreDB: query = (self.session.query(database, read_column.value, ub.ArchivedBook.is_archived) .select_from(Books) .outerjoin(read_column, read_column.book == Books.id)) - except (KeyError, AttributeError): - log.error("Custom Column No.%d is not existing in calibre database", read_column) + except (KeyError, AttributeError, IndexError): + log.error("Custom Column No.{} is not existing in calibre database".format(read_column)) # Skip linking read column and return None instead of read status query = self.session.query(database, None, ub.ArchivedBook.is_archived) query = query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id, @@ -840,8 +840,8 @@ class CalibreDB: read_column = cc_classes[config_read_column] query = (self.session.query(Books, ub.ArchivedBook.is_archived, read_column.value).select_from(Books) .outerjoin(read_column, read_column.book == Books.id)) - except (KeyError, AttributeError): - log.error("Custom Column No.%d is not existing in calibre database", config_read_column) + except (KeyError, AttributeError, IndexError): + log.error("Custom Column No.{} is not existing in calibre database".format(config_read_column)) # Skip linking read column query = self.session.query(Books, ub.ArchivedBook.is_archived, None) query = query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id, diff --git a/cps/editbooks.py b/cps/editbooks.py index 7f6ee932..e2e4a534 100755 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -289,13 +289,13 @@ def delete_whole_book(book_id, book): def render_delete_book_result(book_format, json_response, warning, book_id): if book_format: if json_response: - return json.dumps([warning, {"location": url_for("edit-book.edit_book", book_id=book_id), + return json.dumps([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "success", "format": book_format, "message": _('Book Format Successfully Deleted')}]) else: flash(_('Book Format Successfully Deleted'), category="success") - return redirect(url_for('edit-book.edit_book', book_id=book_id)) + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) else: if json_response: return json.dumps([warning, {"location": url_for('web.index'), @@ -316,16 +316,16 @@ def delete_book_from_table(book_id, book_format, json_response): result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) if not result: if json_response: - return json.dumps([{"location": url_for("edit-book.edit_book", book_id=book_id), + return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "danger", "format": "", "message": error}]) else: flash(error, category="error") - return redirect(url_for('edit-book.edit_book', book_id=book_id)) + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) if error: if json_response: - warning = {"location": url_for("edit-book.edit_book", book_id=book_id), + warning = {"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "warning", "format": "", "message": error} @@ -343,13 +343,13 @@ def delete_book_from_table(book_id, book_format, json_response): log.error_or_exception(ex) calibre_db.session.rollback() if json_response: - return json.dumps([{"location": url_for("edit-book.edit_book", book_id=book_id), + return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "danger", "format": "", "message": ex}]) else: flash(str(ex), category="error") - return redirect(url_for('edit-book.edit_book', book_id=book_id)) + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) else: # book not found @@ -357,13 +357,13 @@ def delete_book_from_table(book_id, book_format, json_response): return render_delete_book_result(book_format, json_response, warning, book_id) message = _("You are missing permissions to delete books") if json_response: - return json.dumps({"location": url_for("edit-book.edit_book", book_id=book_id), + return json.dumps({"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "danger", "format": "", "message": message}) else: flash(message, category="error") - return redirect(url_for('edit-book.edit_book', book_id=book_id)) + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) def render_edit_book(book_id): @@ -413,18 +413,18 @@ def render_edit_book(book_id): def edit_book_ratings(to_save, book): changed = False - if to_save["rating"].strip(): + if to_save.get("rating","").strip(): old_rating = False if len(book.ratings) > 0: old_rating = book.ratings[0].rating - ratingx2 = int(float(to_save["rating"]) * 2) - if ratingx2 != old_rating: + rating_x2 = int(float(to_save.get("rating","")) * 2) + if rating_x2 != old_rating: changed = True - is_rating = calibre_db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first() + is_rating = calibre_db.session.query(db.Ratings).filter(db.Ratings.rating == rating_x2).first() if is_rating: book.ratings.append(is_rating) else: - new_rating = db.Ratings(rating=ratingx2) + new_rating = db.Ratings(rating=rating_x2) book.ratings.append(new_rating) if old_rating: book.ratings.remove(book.ratings[0]) @@ -622,24 +622,26 @@ def edit_cc_data(book_id, book, to_save, cc): 'custom') return changed - +# returns None if no file is uploaded +# returns False if an error occours, in all other cases the ebook metadata is returned def upload_single_file(file_request, book, book_id): # Check and handle Uploaded file - if 'btn-upload-format' in file_request.files: - requested_file = file_request.files['btn-upload-format'] + requested_file = file_request.files.get('btn-upload-format', None) + if requested_file: # check for empty request if requested_file.filename != '': if not current_user.role_upload(): - abort(403) + flash(_(u"User has no rights to upload additional file formats"), category="error") + return False if '.' in requested_file.filename: file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD: flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), category="error") - return redirect(url_for('web.show_book', book_id=book.id)) + return False else: flash(_('File to be uploaded must have an extension'), category="error") - return redirect(url_for('web.show_book', book_id=book.id)) + return False file_name = book.path.rsplit('/', 1)[-1] filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) @@ -651,12 +653,12 @@ def upload_single_file(file_request, book, book_id): os.makedirs(filepath) except OSError: flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") - return redirect(url_for('web.show_book', book_id=book.id)) + return False try: requested_file.save(saved_filename) except OSError: flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error") - return redirect(url_for('web.show_book', book_id=book.id)) + return False file_size = os.path.getsize(saved_filename) is_format = calibre_db.get_book_format(book_id, file_ext.upper()) @@ -674,7 +676,7 @@ def upload_single_file(file_request, book, book_id): calibre_db.session.rollback() log.error_or_exception("Database error: {}".format(e)) flash(_(u"Database error: %(error)s.", error=e.orig), category="error") - return redirect(url_for('web.show_book', book_id=book.id)) + return False # return redirect(url_for('web.show_book', book_id=book.id)) # Queue uploader info link = '{}'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) @@ -684,15 +686,16 @@ def upload_single_file(file_request, book, book_id): return uploader.process( saved_filename, *os.path.splitext(requested_file.filename), rarExecutable=config.config_rarfile_location) - + return None def upload_cover(cover_request, book): - if 'btn-upload-cover' in cover_request.files: - requested_file = cover_request.files['btn-upload-cover'] + requested_file = cover_request.files.get('btn-upload-cover', None) + if requested_file: # check for empty request if requested_file.filename != '': if not current_user.role_upload(): - abort(403) + flash(_(u"User has no rights to upload cover"), category="error") + return False ret, message = helper.save_cover(requested_file, book.path) if ret is True: helper.clear_cover_thumbnail_cache(book.id) @@ -717,25 +720,6 @@ def handle_title_on_edit(book, book_title): def handle_author_on_edit(book, author_name, update_stored=True): # handle author(s) input_authors, renamed = prepare_authors(author_name) - '''input_authors = author_name.split('&') - input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) - # Remove duplicates in authors list - input_authors = helper.uniq(input_authors) - # we have all author names now - if input_authors == ['']: - input_authors = [_(u'Unknown')] # prevent empty Author - - renamed = list() - for in_aut in input_authors: - renamed_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == in_aut).first() - if renamed_author and in_aut != renamed_author.name: - renamed.append(renamed_author.name) - all_books = calibre_db.session.query(db.Books) \ - .filter(db.Books.authors.any(db.Authors.name == renamed_author.name)).all() - sorted_renamed_author = helper.get_sorted_author(renamed_author.name) - sorted_old_author = helper.get_sorted_author(in_aut) - for one_book in all_books: - one_book.author_sort = one_book.author_sort.replace(sorted_renamed_author, sorted_old_author)''' change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author') @@ -755,12 +739,19 @@ def handle_author_on_edit(book, author_name, update_stored=True): change = True return input_authors, change, renamed +@EditBook.route("/admin/book/", methods=['GET']) +@login_required_if_no_ano +@edit_required +def show_edit_book(book_id): + return render_edit_book(book_id) -@EditBook.route("/admin/book/", methods=['GET', 'POST']) + +@EditBook.route("/admin/book/", methods=['POST']) @login_required_if_no_ano @edit_required def edit_book(book_id): modify_date = False + edit_error = False # create the function for sorting... try: @@ -769,110 +760,120 @@ def edit_book(book_id): log.error_or_exception(e) calibre_db.session.rollback() - # Show form - if request.method != 'POST': - return render_edit_book(book_id) - book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) - # Book not found if not book: flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error") return redirect(url_for("web.index")) - meta = upload_single_file(request, book, book_id) - if upload_cover(request, book) is True: - book.has_cover = 1 - modify_date = True + to_save = request.form.to_dict() + try: - to_save = request.form.to_dict() - merge_metadata(to_save, meta) - # Update book + # Update folder of book on local disk edited_books_id = None - - # handle book title + title_author_error = None + # handle book title change title_change = handle_title_on_edit(book, to_save["book_title"]) - - input_authors, authorchange, renamed = handle_author_on_edit(book, to_save["author_name"]) - if authorchange or title_change: + # handle book author change + input_authors, author_change, renamed = handle_author_on_edit(book, to_save["author_name"]) + if author_change or title_change: edited_books_id = book.id modify_date = True + title_author_error = helper.update_dir_structure(edited_books_id, + config.config_calibre_dir, + input_authors[0], + renamed_author=renamed) + if title_author_error: + flash(title_author_error, category="error") + calibre_db.session.rollback() + book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) + + # handle upload other formats from local disk + meta = upload_single_file(request, book, book_id) + # only merge metadata if file was uploaded and no error occurred (meta equals not false or none) + if meta: + merge_metadata(to_save, meta) + # handle upload covers from local disk + cover_upload_success = upload_cover(request, book) + if cover_upload_success: + book.has_cover = 1 + modify_date = True + # upload new covers or new file formats to google drive if config.config_use_google_drive: gdriveutils.updateGdriveCalibreFromLocal() - error = "" - if edited_books_id: - error = helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], - renamed_author=renamed) - - if not error: - if "cover_url" in to_save: - if to_save["cover_url"]: - if not current_user.role_upload(): - calibre_db.session.rollback() - return "", 403 - if to_save["cover_url"].endswith('/static/generic_cover.jpg'): - book.has_cover = 0 - else: - result, error = helper.save_cover_from_url(to_save["cover_url"], book.path) - if result is True: - book.has_cover = 1 - modify_date = True - helper.clear_cover_thumbnail_cache(book.id) - else: - flash(error, category="error") - - # Add default series_index to book - modify_date |= edit_book_series_index(to_save["series_index"], book) - # Handle book comments/description - modify_date |= edit_book_comments(Markup(to_save['description']).unescape(), book) - # Handle identifiers - input_identifiers = identifier_list(to_save, book) - modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) - if warning: - flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning") - modify_date |= modification - # Handle book tags - modify_date |= edit_book_tags(to_save['tags'], book) - # Handle book series - modify_date |= edit_book_series(to_save["series"], book) - # handle book publisher - modify_date |= edit_book_publisher(to_save['publisher'], book) - # handle book languages - modify_date |= edit_book_languages(to_save['languages'], book) - # handle book ratings - modify_date |= edit_book_ratings(to_save, book) - # handle cc data - modify_date |= edit_all_cc_data(book_id, book, to_save) - - if to_save["pubdate"]: - try: - book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d") - except ValueError: - book.pubdate = db.Books.DEFAULT_PUBDATE + if to_save.get("cover_url", None): + if not current_user.role_upload(): + edit_error = True + flash(_(u"User has no rights to upload cover"), category="error") + if to_save["cover_url"].endswith('/static/generic_cover.jpg'): + book.has_cover = 0 else: + result, error = helper.save_cover_from_url(to_save["cover_url"], book.path) + if result is True: + book.has_cover = 1 + modify_date = True + else: + flash(error, category="error") + + # Add default series_index to book + modify_date |= edit_book_series_index(to_save["series_index"], book) + # Handle book comments/description + modify_date |= edit_book_comments(Markup(to_save['description']).unescape(), book) + # Handle identifiers + input_identifiers = identifier_list(to_save, book) + modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) + if warning: + flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning") + modify_date |= modification + # Handle book tags + modify_date |= edit_book_tags(to_save['tags'], book) + # Handle book series + modify_date |= edit_book_series(to_save["series"], book) + # handle book publisher + modify_date |= edit_book_publisher(to_save['publisher'], book) + # handle book languages + try: + modify_date |= edit_book_languages(to_save['languages'], book) + except ValueError as e: + flash(str(e), category="error") + edit_error = True + # handle book ratings + modify_date |= edit_book_ratings(to_save, book) + # handle cc data + modify_date |= edit_all_cc_data(book_id, book, to_save) + + if to_save.get("pubdate", None): + try: + book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d") + except ValueError as e: book.pubdate = db.Books.DEFAULT_PUBDATE + flash(str(e), category="error") + edit_error = True + else: + book.pubdate = db.Books.DEFAULT_PUBDATE - if modify_date: - book.last_modified = datetime.utcnow() - kobo_sync_status.remove_synced_book(edited_books_id, all=True) + if modify_date: + book.last_modified = datetime.utcnow() + kobo_sync_status.remove_synced_book(edited_books_id, all=True) - calibre_db.session.merge(book) - calibre_db.session.commit() - if config.config_use_google_drive: - gdriveutils.updateGdriveCalibreFromLocal() - if "detail_view" in to_save: - return redirect(url_for('web.show_book', book_id=book.id)) - else: - flash(_("Metadata successfully updated"), category="success") - return render_edit_book(book_id) + calibre_db.session.merge(book) + calibre_db.session.commit() + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + if meta is not False \ + and edit_error is not True \ + and title_author_error is not True \ + and cover_upload_success is not False: + flash(_("Metadata successfully updated"), category="success") + if "detail_view" in to_save: + return redirect(url_for('web.show_book', book_id=book.id)) else: - calibre_db.session.rollback() - flash(error, category="error") return render_edit_book(book_id) except ValueError as e: + log.error_or_exception("Error: {}".format(e)) calibre_db.session.rollback() flash(str(e), category="error") return redirect(url_for('web.show_book', book_id=book.id)) @@ -884,14 +885,14 @@ def edit_book(book_id): except Exception as ex: log.error_or_exception(ex) calibre_db.session.rollback() - flash(_("Error editing book, please check logfile for details"), category="error") + flash(_("Error editing book: {}".format(ex)), category="error") return redirect(url_for('web.show_book', book_id=book.id)) def merge_metadata(to_save, meta): - if to_save['author_name'] == _(u'Unknown'): + if to_save.get('author_name', "") == _(u'Unknown'): to_save['author_name'] = '' - if to_save['book_title'] == _(u'Unknown'): + if to_save.get('book_title', "") == _(u'Unknown'): to_save['book_title'] = '' for s_field, m_field in [ ('tags', 'tags'), ('author_name', 'author'), ('series', 'series'), @@ -1119,7 +1120,7 @@ def upload(): if len(request.files.getlist("btn-upload")) < 2: if current_user.role_edit() or current_user.role_admin(): - resp = {"location": url_for('edit-book.edit_book', book_id=book_id)} + resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)} return Response(json.dumps(resp), mimetype='application/json') else: resp = {"location": url_for('web.show_book', book_id=book_id)} @@ -1141,7 +1142,7 @@ def convert_bookformat(book_id): if (book_format_from is None) or (book_format_to is None): flash(_(u"Source or destination format for conversion missing"), category="error") - return redirect(url_for('edit-book.edit_book', book_id=book_id)) + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), @@ -1153,7 +1154,7 @@ def convert_bookformat(book_id): category="success") else: flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") - return redirect(url_for('edit-book.edit_book', book_id=book_id)) + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) @EditBook.route("/ajax/getcustomenum/") @@ -1213,10 +1214,15 @@ def edit_list_book(param): mimetype='application/json') elif param == 'title': sort_param = book.sort - handle_title_on_edit(book, vals.get('value', "")) - helper.update_dir_structure(book.id, config.config_calibre_dir) - ret = Response(json.dumps({'success': True, 'newValue': book.title}), - mimetype='application/json') + if handle_title_on_edit(book, vals.get('value', "")): + rename_error = helper.update_dir_structure(book.id, config.config_calibre_dir) + if not rename_error: + ret = Response(json.dumps({'success': True, 'newValue': book.title}), + mimetype='application/json') + else: + ret = Response(json.dumps({'success': False, + 'msg': rename_error}), + mimetype='application/json') elif param == 'sort': book.sort = vals['value'] ret = Response(json.dumps({'success': True, 'newValue': book.sort}), @@ -1227,11 +1233,17 @@ def edit_list_book(param): mimetype='application/json') elif param == 'authors': input_authors, __, renamed = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") - helper.update_dir_structure(book.id, config.config_calibre_dir, input_authors[0], renamed_author=renamed) - ret = Response(json.dumps({ - 'success': True, - 'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}), - mimetype='application/json') + rename_error = helper.update_dir_structure(book.id, config.config_calibre_dir, input_authors[0], + renamed_author=renamed) + if not rename_error: + ret = Response(json.dumps({ + 'success': True, + 'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}), + mimetype='application/json') + else: + ret = Response(json.dumps({'success': False, + 'msg': rename_error}), + mimetype='application/json') elif param == 'is_archived': is_archived = change_archived_books(book.id, vals['value'] == "True", message="Book {} archive bit set to: {}".format(book.id, vals['value'])) @@ -1358,8 +1370,8 @@ def table_xchange_author_title(): author_names.append(authr.name.replace('|', ',')) title_change = handle_title_on_edit(book, " ".join(author_names)) - input_authors, authorchange, renamed = handle_author_on_edit(book, authors) - if authorchange or title_change: + input_authors, author_change, renamed = handle_author_on_edit(book, authors) + if author_change or title_change: edited_books_id = book.id modify_date = True @@ -1367,8 +1379,9 @@ def table_xchange_author_title(): gdriveutils.updateGdriveCalibreFromLocal() if edited_books_id: - helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], - renamed_author=renamed) + # toDo: Handle error + edit_error = helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], + renamed_author=renamed) if modify_date: book.last_modified = datetime.utcnow() try: diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index 0b704db4..e75f3742 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -81,7 +81,7 @@ if gdrive_support: if not logger.is_debug_enabled(): logger.get('googleapiclient.discovery').setLevel(logger.logging.ERROR) else: - log.debug("Cannot import pydrive,httplib2, using gdrive will not work: %s", importError) + log.debug("Cannot import pydrive, httplib2, using gdrive will not work: %s", importError) class Singleton: @@ -272,8 +272,7 @@ def getEbooksFolderId(drive=None): try: session.commit() except OperationalError as ex: - log.error("gdrive.db DB is not Writeable") - log.debug('Database error: %s', ex) + log.error_or_exception('Database error: %s', ex) session.rollback() return gDriveId.gdrive_id @@ -322,8 +321,7 @@ def getFolderId(path, drive): else: currentFolderId = storedPathName.gdrive_id except OperationalError as ex: - log.error("gdrive.db DB is not Writeable") - log.debug('Database error: %s', ex) + log.error_or_exception('Database error: %s', ex) session.rollback() except ApiRequestError as ex: log.error('{} {}'.format(ex.error['message'], path)) @@ -547,8 +545,7 @@ def deleteDatabaseOnChange(): session.commit() except (OperationalError, InvalidRequestError) as ex: session.rollback() - log.debug('Database error: %s', ex) - log.error(u"GDrive DB is not Writeable") + log.error_or_exception('Database error: %s', ex) def updateGdriveCalibreFromLocal(): @@ -566,8 +563,7 @@ def updateDatabaseOnEdit(ID,newPath): try: session.commit() except OperationalError as ex: - log.error("gdrive.db DB is not Writeable") - log.debug('Database error: %s', ex) + log.error_or_exception('Database error: %s', ex) session.rollback() @@ -577,8 +573,7 @@ def deleteDatabaseEntry(ID): try: session.commit() except OperationalError as ex: - log.error("gdrive.db DB is not Writeable") - log.debug('Database error: %s', ex) + log.error_or_exception('Database error: %s', ex) session.rollback() @@ -599,8 +594,7 @@ def get_cover_via_gdrive(cover_path): try: session.commit() except OperationalError as ex: - log.error("gdrive.db DB is not Writeable") - log.debug('Database error: %s', ex) + log.error_or_exception('Database error: %s', ex) session.rollback() return df.metadata.get('webContentLink') else: diff --git a/cps/helper.py b/cps/helper.py index c8db50c2..8a261581 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -329,8 +329,9 @@ def edit_book_read_status(book_id, read_status=None): new_cc = cc_class(value=read_status or 1, book=book_id) calibre_db.session.add(new_cc) calibre_db.session.commit() - except (KeyError, AttributeError): - log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column) + except (KeyError, AttributeError, IndexError): + log.error( + "Custom Column No.{} is not existing in calibre database".format(config.config_read_column)) return "Custom Column No.{} is not existing in calibre database".format(config.config_read_column) except (OperationalError, InvalidRequestError) as ex: calibre_db.session.rollback() @@ -449,31 +450,31 @@ def rename_all_authors(first_author, renamed_author, calibre_path="", localbook= # Moves files in file storage during author/title rename, or from temp dir to file storage def update_dir_structure_file(book_id, calibre_path, first_author, original_filepath, db_filename, renamed_author): # get book database entry from id, if original path overwrite source with original_filepath - localbook = calibre_db.get_book(book_id) + local_book = calibre_db.get_book(book_id) if original_filepath: path = original_filepath else: - path = os.path.join(calibre_path, localbook.path) + path = os.path.join(calibre_path, local_book.path) - # Create (current) authordir and titledir from database - authordir = localbook.path.split('/')[0] - titledir = localbook.path.split('/')[1] + # Create (current) author_dir and title_dir from database + author_dir = local_book.path.split('/')[0] + title_dir = local_book.path.split('/')[1] - # Create new_authordir from parameter or from database - # Create new titledir from database and add id - new_authordir = rename_all_authors(first_author, renamed_author, calibre_path, localbook) + # Create new_author_dir from parameter or from database + # Create new title_dir from database and add id + new_author_dir = rename_all_authors(first_author, renamed_author, calibre_path, local_book) if first_author: if first_author.lower() in [r.lower() for r in renamed_author]: - if os.path.isdir(os.path.join(calibre_path, new_authordir)): - path = os.path.join(calibre_path, new_authordir, titledir) + if os.path.isdir(os.path.join(calibre_path, new_author_dir)): + path = os.path.join(calibre_path, new_author_dir, title_dir) - new_titledir = get_valid_filename(localbook.title, chars=96) + " (" + str(book_id) + ")" + new_title_dir = get_valid_filename(local_book.title, chars=96) + " (" + str(book_id) + ")" - if titledir != new_titledir or authordir != new_authordir or original_filepath: + if title_dir != new_title_dir or author_dir != new_author_dir or original_filepath: error = move_files_on_change(calibre_path, - new_authordir, - new_titledir, - localbook, + new_author_dir, + new_title_dir, + local_book, db_filename, original_filepath, path) @@ -481,7 +482,7 @@ def update_dir_structure_file(book_id, calibre_path, first_author, original_file return error # Rename all files from old names to new names - return rename_files_on_change(first_author, renamed_author, localbook, original_filepath, path, calibre_path) + return rename_files_on_change(first_author, renamed_author, local_book, original_filepath, path, calibre_path) def upload_new_file_gdrive(book_id, first_author, renamed_author, title, title_dir, original_filepath, filename_ext): @@ -493,7 +494,7 @@ def upload_new_file_gdrive(book_id, first_author, renamed_author, title, title_d title_dir + " (" + str(book_id) + ")") book.path = gdrive_path.replace("\\", "/") gd.uploadFileToEbooksFolder(os.path.join(gdrive_path, file_name).replace("\\", "/"), original_filepath) - return rename_files_on_change(first_author, renamed_author, localbook=book, gdrive=True) + return rename_files_on_change(first_author, renamed_author, local_book=book, gdrive=True) @@ -553,8 +554,7 @@ def move_files_on_change(calibre_path, new_authordir, new_titledir, localbook, d # change location in database to new author/title path localbook.path = os.path.join(new_authordir, new_titledir).replace('\\', '/') except OSError as ex: - log.error("Rename title from: %s to %s: %s", path, new_path, ex) - log.debug(ex, exc_info=True) + log.error_or_exception("Rename title from {} to {} failed with error: {}".format(path, new_path, ex)) return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_path, error=str(ex)) return False @@ -562,8 +562,8 @@ def move_files_on_change(calibre_path, new_authordir, new_titledir, localbook, d def rename_files_on_change(first_author, renamed_author, - localbook, - orignal_filepath="", + local_book, + original_filepath="", path="", calibre_path="", gdrive=False): @@ -571,13 +571,12 @@ def rename_files_on_change(first_author, try: clean_author_database(renamed_author, calibre_path, gdrive=gdrive) if first_author and first_author not in renamed_author: - clean_author_database([first_author], calibre_path, localbook, gdrive) - if not gdrive and not renamed_author and not orignal_filepath and len(os.listdir(os.path.dirname(path))) == 0: + clean_author_database([first_author], calibre_path, local_book, gdrive) + if not gdrive and not renamed_author and not original_filepath and len(os.listdir(os.path.dirname(path))) == 0: shutil.rmtree(os.path.dirname(path)) except (OSError, FileNotFoundError) as ex: - log.error("Error in rename file in path %s", ex) - log.debug(ex, exc_info=True) - return _("Error in rename file in path: %(error)s", error=str(ex)) + log.error_or_exception("Error in rename file in path {}".format(ex)) + return _("Error in rename file in path: {}".format(str(ex))) return False @@ -804,16 +803,18 @@ def save_cover_from_url(url, book_path): return save_cover(img, book_path) except (socket.gaierror, requests.exceptions.HTTPError, + requests.exceptions.InvalidURL, requests.exceptions.ConnectionError, requests.exceptions.Timeout) as ex: - log.info(u'Cover Download Error %s', ex) + # "Invalid host" can be the result of a redirect response + log.error(u'Cover Download Error %s', ex) return False, _("Error Downloading Cover") except MissingDelegateError as ex: log.info(u'File Format Error %s', ex) return False, _("Cover Format Error") - except UnacceptableAddressException: - log.error("Localhost was accessed for cover upload") - return False, _("You are not allowed to access localhost for cover uploads") + except UnacceptableAddressException as e: + log.error("Localhost or local network was accessed for cover upload") + return False, _("You are not allowed to access localhost or the local network for cover uploads") def save_cover_from_filestorage(filepath, saved_filename, img): diff --git a/cps/opds.py b/cps/opds.py index c8ccdea9..180fcacb 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -34,7 +34,6 @@ from .pagination import Pagination from .web import render_read_books from .usermanagement import load_user_from_request from flask_babel import gettext as _ -from sqlalchemy.orm import InstrumentedAttribute opds = Blueprint('opds', __name__) log = logger.create() diff --git a/cps/render_template.py b/cps/render_template.py index 7cd341ea..09a32497 100644 --- a/cps/render_template.py +++ b/cps/render_template.py @@ -110,8 +110,8 @@ def get_readbooks_ids(): readBooks = calibre_db.session.query(db.cc_classes[config.config_read_column])\ .filter(db.cc_classes[config.config_read_column].value == True).all() return frozenset([x.book for x in readBooks]) - except (KeyError, AttributeError): - log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column) + except (KeyError, AttributeError, IndexError): + log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column)) return [] # Returns the template for rendering and includes the instance name diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 69ba4a6a..5d7c846e 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -296,7 +296,7 @@ {% if g.user.role_edit() %} {% endif %} diff --git a/cps/ub.py b/cps/ub.py index 02ee90b4..8d273c53 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -68,6 +68,7 @@ logged_in = dict() def signal_store_user_session(object, user): store_user_session() + def store_user_session(): if flask_session.get('user_id', ""): flask_session['_user_id'] = flask_session.get('user_id', "") @@ -86,15 +87,16 @@ def store_user_session(): else: log.error("No user id in session") + def delete_user_session(user_id, session_key): try: log.debug("Deleted session_key: " + session_key) - session.query(User_Sessions).filter(User_Sessions.user_id==user_id, - User_Sessions.session_key==session_key).delete() + session.query(User_Sessions).filter(User_Sessions.user_id == user_id, + User_Sessions.session_key == session_key).delete() session.commit() - except (exc.OperationalError, exc.InvalidRequestError) as e: + except (exc.OperationalError, exc.InvalidRequestError) as ex: session.rollback() - log.exception(e) + log.exception(ex) def check_user_session(user_id, session_key): @@ -210,9 +212,9 @@ class UserBase: pass try: session.commit() - except (exc.OperationalError, exc.InvalidRequestError): + except (exc.OperationalError, exc.InvalidRequestError) as e: session.rollback() - # ToDo: Error message + log.error_or_exception(e) def __repr__(self): return '' % self.name diff --git a/cps/web.py b/cps/web.py index 53335753..babc3bcc 100644 --- a/cps/web.py +++ b/cps/web.py @@ -87,7 +87,7 @@ def add_security_headers(resp): csp += ''.join([' ' + host for host in config.config_trustedhosts.strip().split(',')]) csp += " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:" resp.headers['Content-Security-Policy'] = csp - if request.endpoint == "edit-book.edit_book" or config.config_use_google_drive: + if request.endpoint == "edit-book.show_edit_book" or config.config_use_google_drive: resp.headers['Content-Security-Policy'] += " *" elif request.endpoint == "web.read_book": resp.headers['Content-Security-Policy'] += " blob:;style-src-elem 'self' blob: 'unsafe-inline';" @@ -646,8 +646,8 @@ def render_read_books(page, are_read, as_xml=False, order=None): db.Books.id == db.books_series_link.c.book, db.Series, db.cc_classes[config.config_read_column]) - except (KeyError, AttributeError): - log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column) + except (KeyError, AttributeError, IndexError): + log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column)) if not as_xml: flash(_("Custom Column No.%(column)d is not existing in calibre database", column=config.config_read_column), @@ -826,8 +826,9 @@ def list_books(): books = (calibre_db.session.query(db.Books, read_column.value, ub.ArchivedBook.is_archived) .select_from(db.Books) .outerjoin(read_column, read_column.book == db.Books.id)) - except (KeyError, AttributeError): - log.error("Custom Column No.%d is not existing in calibre database", read_column) + except (KeyError, AttributeError, IndexError): + log.error( + "Custom Column No.{} is not existing in calibre database".format(config.config_read_column)) # Skip linking read column and return None instead of read status books = calibre_db.session.query(db.Books, None, ub.ArchivedBook.is_archived) books = (books.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id, @@ -1139,8 +1140,9 @@ def adv_search_read_status(q, read_status): else: q = q.join(db.cc_classes[config.config_read_column], isouter=True) \ .filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True) - except (KeyError, AttributeError): - log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column) + except (KeyError, AttributeError, IndexError): + log.error( + "Custom Column No.{} is not existing in calibre database".format(config.config_read_column)) flash(_("Custom Column No.%(column)d is not existing in calibre database", column=config.config_read_column), category="error") @@ -1262,8 +1264,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None): query = (calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, read_column.value) .select_from(db.Books) .outerjoin(read_column, read_column.book == db.Books.id)) - except (KeyError, AttributeError): - log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column) + except (KeyError, AttributeError, IndexError): + log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column)) # Skip linking read column query = calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, None) query = query.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id, diff --git a/optional-requirements.txt b/optional-requirements.txt index aea1efb7..e54b0829 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,5 +1,5 @@ # GDrive Integration -google-api-python-client>=1.7.11,<2.41.0 +google-api-python-client>=1.7.11,<2.42.0 gevent>20.6.0,<22.0.0 greenlet>=0.4.17,<1.2.0 httplib2>=0.9.2,<0.21.0 @@ -13,7 +13,7 @@ rsa>=3.4.2,<4.9.0 # Gmail google-auth-oauthlib>=0.4.3,<0.6.0 -google-api-python-client>=1.7.11,<2.41.0 +google-api-python-client>=1.7.11,<2.42.0 # goodreads goodreads>=0.3.2,<0.4.0 diff --git a/requirements.txt b/requirements.txt index 02a94d8c..985bbaf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -APScheduler>=3.6.3,<3.8.0 +APScheduler>=3.6.3,<3.10.0 Babel>=1.3,<3.0 Flask-Babel>=0.11.1,<2.1.0 Flask-Login>=0.3.2,<0.5.1