From 423022671620bcd5805a313473d4e277ea51d87b Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 16 Feb 2019 07:23:08 +0100 Subject: [PATCH] Link fixes Fixes reader button visible in detail view Fix formats to convert (added htmlz) Fix logger in updater Added request "v3" of github api on update Fix quotes parameter on external calls E-Mail logger working more stable (also on python3) Routing fixes Change import in ub --- cps/admin.py | 73 +- cps/editbooks.py | 12 +- cps/gdrive.py | 6 +- cps/helper.py | 47 +- cps/oauth.py | 243 +++---- cps/oauth_bb.py | 417 +++++------ cps/opds.py | 3 +- cps/shelf.py | 24 +- cps/templates/book_edit.html | 4 +- cps/templates/config_edit.html | 6 +- cps/templates/detail.html | 17 +- cps/templates/index.xml | 57 +- cps/templates/json.txt | 52 +- cps/templates/layout.html | 2 +- cps/templates/osd.xml | 4 +- cps/ub.py | 16 +- cps/updater.py | 54 +- cps/web.py | 73 +- cps/worker.py | 57 +- optional-requirements.txt | 7 + requirements.txt | 2 - test/Calibre-Web TestSummary.html | 1087 +++++++++++++---------------- 22 files changed, 1106 insertions(+), 1157 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 1b8824ca..d5be6839 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -21,6 +21,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import logging import os from flask import Blueprint, flash, redirect, url_for from flask import abort, request, make_response @@ -39,20 +40,26 @@ from gdriveutils import is_gdrive_ready, gdrive_support, downloadFile, deleteDat import helper from werkzeug.security import generate_password_hash from oauth_bb import oauth_check +try: + from urllib.parse import quote + from imp import reload +except ImportError: + from urllib import quote +feature_support = dict() try: from goodreads.client import GoodreadsClient - goodreads_support = True + feature_support['goodreads'] = True except ImportError: - goodreads_support = False + feature_support['goodreads'] = False try: import rarfile - rar_support = True + feature_support['rar'] = True except ImportError: - rar_support = False - + feature_support['rar'] = False +feature_support['gdrive'] = gdrive_support admi = Blueprint('admin', __name__) @@ -287,7 +294,7 @@ def configuration_helper(origin): db_change = False success = False filedata = None - if gdrive_support is False: + if not feature_support['gdrive']: gdriveError = _('Import of optional Google Drive requirements missing') else: if not os.path.isfile(os.path.join(config.get_main_dir, 'client_secrets.json')): @@ -327,7 +334,7 @@ def configuration_helper(origin): else: flash(_(u'client_secrets.json is not configured for web application'), category="error") return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdrive_support, gdriveError=gdriveError, + gdriveError=gdriveError, goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config") # always show google drive settings, but in case of error deny support @@ -353,7 +360,7 @@ def configuration_helper(origin): ub.session.commit() flash(_(u'Keyfile location is not valid, please enter correct path'), category="error") return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdrive_support, gdriveError=gdriveError, + gdriveError=gdriveError, goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config") if "config_certfile" in to_save: @@ -365,9 +372,8 @@ def configuration_helper(origin): ub.session.commit() flash(_(u'Certfile location is not valid, please enter correct path'), category="error") return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, title=_(u"Basic Configuration"), - page="config") + gdriveError=gdriveError, feature_support=feature_support, + title=_(u"Basic Configuration"), page="config") content.config_uploading = 0 content.config_anonbrowse = 0 content.config_public_reg = 0 @@ -391,9 +397,8 @@ def configuration_helper(origin): ub.session.commit() flash(_(u'Please enter a LDAP provider and a DN'), category="error") return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, title=_(u"Basic Configuration"), - page="config") + gdriveError=gdriveError, feature_support=feature_support, + title=_(u"Basic Configuration"), page="config") else: content.config_use_ldap = 1 content.config_ldap_provider_url = to_save["config_ldap_provider_url"] @@ -450,9 +455,8 @@ def configuration_helper(origin): ub.session.commit() flash(_(u'Logfile location is not valid, please enter correct path'), category="error") return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, title=_(u"Basic Configuration"), - page="config") + gdriveError=gdriveError, feature_support=feature_support, + title=_(u"Basic Configuration"), page="config") else: content.config_logfile = to_save["config_logfile"] reboot_required = True @@ -465,8 +469,7 @@ def configuration_helper(origin): else: flash(check[1], category="error") return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdrive_support, goodreads=goodreads_support, - rarfile_support=rar_support, title=_(u"Basic Configuration")) + feature_support=feature_support, title=_(u"Basic Configuration")) try: if content.config_use_google_drive and is_gdrive_ready() and not \ os.path.exists(os.path.join(content.config_calibre_dir, "metadata.db")): @@ -479,20 +482,18 @@ def configuration_helper(origin): flash(_(u"Calibre-Web configuration updated"), category="success") config.loadSettings() app.logger.setLevel(config.config_log_level) - logging.getLogger("book_formats").setLevel(config.config_log_level) + logging.getLogger("uploader").setLevel(config.config_log_level) except Exception as e: flash(e, category="error") return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, rarfile_support=rar_support, + gdriveError=gdriveError, feature_support=feature_support, title=_(u"Basic Configuration"), page="config") if db_change: reload(db) if not db.setup_db(): flash(_(u'DB location is not valid, please enter correct path'), category="error") return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, rarfile_support=rar_support, + gdriveError=gdriveError, feature_support=feature_support, title=_(u"Basic Configuration"), page="config") if reboot_required: # stop Server @@ -501,15 +502,14 @@ def configuration_helper(origin): app.logger.info('Reboot required, restarting') if origin: success = True - if is_gdrive_ready() and gdrive_support is True: # and config.config_use_google_drive == True: + if is_gdrive_ready() and feature_support['gdrive'] is True: # and config.config_use_google_drive == True: gdrivefolders = listRootFolders() else: gdrivefolders = list() return render_title_template("config_edit.html", origin=origin, success=success, content=config, show_authenticate_google_drive=not is_gdrive_ready(), - gdrive=gdrive_support, gdriveError=gdriveError, - gdrivefolders=gdrivefolders, rarfile_support=rar_support, - goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config") + gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support, + title=_(u"Basic Configuration"), page="config") @admi.route("/admin/user/new", methods=["GET", "POST"]) @@ -569,20 +569,20 @@ def new_user(): if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: flash(_(u"Please fill out all fields!"), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - title=_(u"Add new user")) + registered_oauth=oauth_check, title=_(u"Add new user")) content.password = generate_password_hash(to_save["password"]) content.nickname = to_save["nickname"] if config.config_public_reg and not check_valid_domain(to_save["email"]): flash(_(u"E-mail is not from valid domain"), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - title=_(u"Add new user")) + registered_oauth=oauth_check, title=_(u"Add new user")) else: content.email = to_save["email"] try: ub.session.add(content) ub.session.commit() flash(_(u"User '%(user)s' created", user=content.nickname), category="success") - return redirect(url_for('admin')) + return redirect(url_for('admin.admin')) except IntegrityError: ub.session.rollback() flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") @@ -591,7 +591,8 @@ def new_user(): content.sidebar_view = config.config_default_show content.mature_content = bool(config.config_default_show & ub.MATURE_CONTENT) return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - languages=languages, title=_(u"Add new user"), page="newuser", registered_oauth=oauth_check) + languages=languages, title=_(u"Add new user"), page="newuser", + registered_oauth=oauth_check) @admi.route("/admin/mailsettings", methods=["GET", "POST"]) @@ -649,7 +650,7 @@ def edit_user(user_id): ub.session.query(ub.User).filter(ub.User.id == content.id).delete() ub.session.commit() flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") - return redirect(url_for('admin')) + return redirect(url_for('admin.admin')) else: if "password" in to_save and to_save["password"]: content.password = generate_password_hash(to_save["password"]) @@ -766,8 +767,8 @@ def edit_user(user_id): ub.session.rollback() flash(_(u"An unknown error occured."), category="error") return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, - content=content, downloads=downloads, title=_(u"Edit User %(nick)s", - nick=content.nickname), page="edituser", registered_oauth=oauth_check) + content=content, downloads=downloads, registered_oauth=oauth_check, + title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") @admi.route("/admin/resetpassword/") @@ -787,7 +788,7 @@ def reset_password(user_id): except Exception: ub.session.rollback() flash(_(u"An unknown error occurred. Please try again later."), category="error") - return redirect(url_for('admin')) + return redirect(url_for('admin.admin')) @admi.route("/get_update_status", methods=['GET']) diff --git a/cps/editbooks.py b/cps/editbooks.py index 2c20b8a9..c758bb18 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -42,7 +42,7 @@ from iso639 import languages as isoLanguages editbook = Blueprint('editbook', __name__) -EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'html', 'rtf', 'odt'} +EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'} EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', 'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'} @@ -380,7 +380,7 @@ def upload_single_file(request, book, book_id): # Queue uploader info uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) global_WorkerThread.add_upload(current_user.nickname, - "" + uploadText + "") + "" + uploadText + "") def upload_cover(request, book): if 'btn-upload-cover' in request.files: @@ -589,10 +589,10 @@ def upload(): flash( _("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) else: flash(_('File to be uploaded must have an extension'), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) # extract metadata from file meta = uploader.upload(requested_file) @@ -612,12 +612,12 @@ def upload(): os.makedirs(filepath) except OSError: flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) try: copyfile(meta.file_path, saved_filename) except OSError: flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) try: os.unlink(meta.file_path) except OSError: diff --git a/cps/gdrive.py b/cps/gdrive.py index 3cd9c1dc..025c2d65 100644 --- a/cps/gdrive.py +++ b/cps/gdrive.py @@ -70,7 +70,7 @@ def google_drive_callback(): f.write(credentials.to_json()) except ValueError as error: app.logger.error(error) - return redirect(url_for('configuration')) + return redirect(url_for('admin.configuration')) @gdrive.route("/gdrive/watch/subscribe") @@ -102,7 +102,7 @@ def watch_gdrive(): else: flash(reason['message'], category="error") - return redirect(url_for('configuration')) + return redirect(url_for('admin.configuration')) @gdrive.route("/gdrive/watch/revoke") @@ -121,7 +121,7 @@ def revoke_watch_gdrive(): ub.session.merge(settings) ub.session.commit() config.loadSettings() - return redirect(url_for('configuration')) + return redirect(url_for('admin.configuration')) @gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST']) diff --git a/cps/helper.py b/cps/helper.py index ba323449..16530410 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -19,13 +19,13 @@ # along with this program. If not, see . -import db -from cps import config +from cps import config, global_WorkerThread, get_locale from flask import current_app as app from tempfile import gettempdir import sys import os import re +import db import unicodedata import worker import time @@ -40,7 +40,6 @@ try: import gdriveutils as gd except ImportError: pass -# import web import random from subproc_wrapper import process_open import ub @@ -244,7 +243,7 @@ def get_sorted_author(value): else: value2 = value except Exception: - web.app.logger.error("Sorting author " + str(value) + "failed") + app.logger.error("Sorting author " + str(value) + "failed") value2 = value return value2 @@ -261,13 +260,13 @@ def delete_book_file(book, calibrepath, book_format=None): else: if os.path.isdir(path): if len(next(os.walk(path))[1]): - web.app.logger.error( + app.logger.error( "Deleting book " + str(book.id) + " failed, path has subfolders: " + book.path) return False shutil.rmtree(path, ignore_errors=True) return True else: - web.app.logger.error("Deleting book " + str(book.id) + " failed, book path not valid: " + book.path) + app.logger.error("Deleting book " + str(book.id) + " failed, book path not valid: " + book.path) return False @@ -290,7 +289,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author): if not os.path.exists(new_title_path): os.renames(path, new_title_path) else: - web.app.logger.info("Copying title: " + path + " into existing: " + new_title_path) + app.logger.info("Copying title: " + path + " into existing: " + new_title_path) for dir_name, subdir_list, file_list in os.walk(path): for file in file_list: os.renames(os.path.join(dir_name, file), @@ -298,8 +297,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author): path = new_title_path localbook.path = localbook.path.split('/')[0] + '/' + new_titledir except OSError as ex: - web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex)) - web.app.logger.debug(ex, exc_info=True) + app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex)) + app.logger.debug(ex, exc_info=True) return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_title_path, error=str(ex)) if authordir != new_authordir: @@ -308,8 +307,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author): os.renames(path, new_author_path) localbook.path = new_authordir + '/' + localbook.path.split('/')[1] except OSError as ex: - web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex)) - web.app.logger.debug(ex, exc_info=True) + app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex)) + app.logger.debug(ex, exc_info=True) return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_author_path, error=str(ex)) # Rename all files from old names to new names @@ -322,8 +321,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author): os.path.join(path_name,new_name + '.' + file_format.format.lower())) file_format.name = new_name except OSError as ex: - web.app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex)) - web.app.logger.debug(ex, exc_info=True) + app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex)) + app.logger.debug(ex, exc_info=True) return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_name, error=str(ex)) return False @@ -418,17 +417,17 @@ def delete_book(book, calibrepath, book_format): def get_book_cover(cover_path): if config.config_use_google_drive: try: - if not web.is_gdrive_ready(): + if not gd.is_gdrive_ready(): return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg") path=gd.get_cover_via_gdrive(cover_path) if path: return redirect(path) else: - web.app.logger.error(cover_path + '/cover.jpg not found on Google Drive') + app.logger.error(cover_path + '/cover.jpg not found on Google Drive') return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg") except Exception as e: - web.app.logger.error("Error Message: " + e.message) - web.app.logger.exception(e) + app.logger.error("Error Message: " + e.message) + app.logger.exception(e) # traceback.print_exc() return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg") else: @@ -439,7 +438,7 @@ def get_book_cover(cover_path): def save_cover(url, book_path): img = requests.get(url) if img.headers.get('content-type') != 'image/jpeg': - web.app.logger.error("Cover is no jpg file, can't save") + app.logger.error("Cover is no jpg file, can't save") return False if config.config_use_google_drive: @@ -448,13 +447,13 @@ def save_cover(url, book_path): f.write(img.content) f.close() gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name)) - web.app.logger.info("Cover is saved on Google Drive") + app.logger.info("Cover is saved on Google Drive") return True f = open(os.path.join(config.config_calibre_dir, book_path, "cover.jpg"), "wb") f.write(img.content) f.close() - web.app.logger.info("Cover is saved") + app.logger.info("Cover is saved") return True @@ -462,7 +461,7 @@ def do_download_file(book, book_format, data, headers): if config.config_use_google_drive: startTime = time.time() df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format) - web.app.logger.debug(time.time() - startTime) + app.logger.debug(time.time() - startTime) if df: return gd.do_gdrive_download(df, headers) else: @@ -471,7 +470,7 @@ def do_download_file(book, book_format, data, headers): filename = os.path.join(config.config_calibre_dir, book.path) if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)): # ToDo: improve error handling - web.app.logger.error('File not found: %s' % os.path.join(filename, data.name + "." + book_format)) + app.logger.error('File not found: %s' % os.path.join(filename, data.name + "." + book_format)) response = make_response(send_from_directory(filename, data.name + "." + book_format)) response.headers = headers return response @@ -497,7 +496,7 @@ def check_unrar(unrarLocation): version = value.group(1) except OSError as e: error = True - web.app.logger.exception(e) + app.logger.exception(e) version =_(u'Error excecuting UnRar') else: version = _(u'Unrar binary file not found') @@ -522,7 +521,7 @@ def render_task_status(tasklist): if task['user'] == current_user.nickname or current_user.role_admin(): # task2 = copy.deepcopy(task) # = task if task['formStarttime']: - task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale()) + task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=get_locale()) # task2['formStarttime'] = "" else: if 'starttime' not in task: diff --git a/cps/oauth.py b/cps/oauth.py index 679e7f31..960a3810 100644 --- a/cps/oauth.py +++ b/cps/oauth.py @@ -2,133 +2,136 @@ # -*- coding: utf-8 -*- from flask import session -from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user -from sqlalchemy.orm.exc import NoResultFound +try: + from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user + from sqlalchemy.orm.exc import NoResultFound + class OAuthBackend(SQLAlchemyBackend): + """ + Stores and retrieves OAuth tokens using a relational database through + the `SQLAlchemy`_ ORM. -class OAuthBackend(SQLAlchemyBackend): - """ - Stores and retrieves OAuth tokens using a relational database through - the `SQLAlchemy`_ ORM. + .. _SQLAlchemy: http://www.sqlalchemy.org/ + """ + def __init__(self, model, session, + user=None, user_id=None, user_required=None, anon_user=None, + cache=None): + super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache) - .. _SQLAlchemy: http://www.sqlalchemy.org/ - """ - def __init__(self, model, session, - user=None, user_id=None, user_required=None, anon_user=None, - cache=None): - super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache) + def get(self, blueprint, user=None, user_id=None): + if blueprint.name + '_oauth_token' in session and session[blueprint.name + '_oauth_token'] != '': + return session[blueprint.name + '_oauth_token'] + # check cache + cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) + token = self.cache.get(cache_key) + if token: + return token - def get(self, blueprint, user=None, user_id=None): - if blueprint.name + '_oauth_token' in session and session[blueprint.name + '_oauth_token'] != '': - return session[blueprint.name + '_oauth_token'] - # check cache - cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) - token = self.cache.get(cache_key) - if token: - return token - - # if not cached, make database queries - query = ( - self.session.query(self.model) - .filter_by(provider=blueprint.name) - ) - uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) - u = first(_get_real_user(ref, self.anon_user) - for ref in (user, self.user, blueprint.config.get("user"))) - - use_provider_user_id = False - if blueprint.name + '_oauth_user_id' in session and session[blueprint.name + '_oauth_user_id'] != '': - query = query.filter_by(provider_user_id=session[blueprint.name + '_oauth_user_id']) - use_provider_user_id = True - - if self.user_required and not u and not uid and not use_provider_user_id: - #raise ValueError("Cannot get OAuth token without an associated user") - return None - # check for user ID - if hasattr(self.model, "user_id") and uid: - query = query.filter_by(user_id=uid) - # check for user (relationship property) - elif hasattr(self.model, "user") and u: - query = query.filter_by(user=u) - # if we have the property, but not value, filter by None - elif hasattr(self.model, "user_id"): - query = query.filter_by(user_id=None) - # run query - try: - token = query.one().token - except NoResultFound: - token = None - - # cache the result - self.cache.set(cache_key, token) - - return token - - def set(self, blueprint, token, user=None, user_id=None): - uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) - u = first(_get_real_user(ref, self.anon_user) + # if not cached, make database queries + query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) for ref in (user, self.user, blueprint.config.get("user"))) - if self.user_required and not u and not uid: - raise ValueError("Cannot set OAuth token without an associated user") + use_provider_user_id = False + if blueprint.name + '_oauth_user_id' in session and session[blueprint.name + '_oauth_user_id'] != '': + query = query.filter_by(provider_user_id=session[blueprint.name + '_oauth_user_id']) + use_provider_user_id = True - # if there was an existing model, delete it - existing_query = ( - self.session.query(self.model) - .filter_by(provider=blueprint.name) - ) - # check for user ID - has_user_id = hasattr(self.model, "user_id") - if has_user_id and uid: - existing_query = existing_query.filter_by(user_id=uid) - # check for user (relationship property) - has_user = hasattr(self.model, "user") - if has_user and u: - existing_query = existing_query.filter_by(user=u) - # queue up delete query -- won't be run until commit() - existing_query.delete() - # create a new model for this token - kwargs = { - "provider": blueprint.name, - "token": token, - } - if has_user_id and uid: - kwargs["user_id"] = uid - if has_user and u: - kwargs["user"] = u - self.session.add(self.model(**kwargs)) - # commit to delete and add simultaneously - self.session.commit() - # invalidate cache - self.cache.delete(self.make_cache_key( - blueprint=blueprint, user=user, user_id=user_id - )) + if self.user_required and not u and not uid and not use_provider_user_id: + #raise ValueError("Cannot get OAuth token without an associated user") + return None + # check for user ID + if hasattr(self.model, "user_id") and uid: + query = query.filter_by(user_id=uid) + # check for user (relationship property) + elif hasattr(self.model, "user") and u: + query = query.filter_by(user=u) + # if we have the property, but not value, filter by None + elif hasattr(self.model, "user_id"): + query = query.filter_by(user_id=None) + # run query + try: + token = query.one().token + except NoResultFound: + token = None - def delete(self, blueprint, user=None, user_id=None): - query = ( - self.session.query(self.model) - .filter_by(provider=blueprint.name) - ) - uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) - u = first(_get_real_user(ref, self.anon_user) - for ref in (user, self.user, blueprint.config.get("user"))) + # cache the result + self.cache.set(cache_key, token) - if self.user_required and not u and not uid: - raise ValueError("Cannot delete OAuth token without an associated user") + return token - # check for user ID - if hasattr(self.model, "user_id") and uid: - query = query.filter_by(user_id=uid) - # check for user (relationship property) - elif hasattr(self.model, "user") and u: - query = query.filter_by(user=u) - # if we have the property, but not value, filter by None - elif hasattr(self.model, "user_id"): - query = query.filter_by(user_id=None) - # run query - query.delete() - self.session.commit() - # invalidate cache - self.cache.delete(self.make_cache_key( - blueprint=blueprint, user=user, user_id=user_id, - )) + def set(self, blueprint, token, user=None, user_id=None): + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + if self.user_required and not u and not uid: + raise ValueError("Cannot set OAuth token without an associated user") + + # if there was an existing model, delete it + existing_query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + # check for user ID + has_user_id = hasattr(self.model, "user_id") + if has_user_id and uid: + existing_query = existing_query.filter_by(user_id=uid) + # check for user (relationship property) + has_user = hasattr(self.model, "user") + if has_user and u: + existing_query = existing_query.filter_by(user=u) + # queue up delete query -- won't be run until commit() + existing_query.delete() + # create a new model for this token + kwargs = { + "provider": blueprint.name, + "token": token, + } + if has_user_id and uid: + kwargs["user_id"] = uid + if has_user and u: + kwargs["user"] = u + self.session.add(self.model(**kwargs)) + # commit to delete and add simultaneously + self.session.commit() + # invalidate cache + self.cache.delete(self.make_cache_key( + blueprint=blueprint, user=user, user_id=user_id + )) + + def delete(self, blueprint, user=None, user_id=None): + query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + if self.user_required and not u and not uid: + raise ValueError("Cannot delete OAuth token without an associated user") + + # check for user ID + if hasattr(self.model, "user_id") and uid: + query = query.filter_by(user_id=uid) + # check for user (relationship property) + elif hasattr(self.model, "user") and u: + query = query.filter_by(user=u) + # if we have the property, but not value, filter by None + elif hasattr(self.model, "user_id"): + query = query.filter_by(user_id=None) + # run query + query.delete() + self.session.commit() + # invalidate cache + self.cache.delete(self.make_cache_key( + blueprint=blueprint, user=user, user_id=user_id, + )) + +except ImportError: + pass diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py index 7cfe1d92..830292e0 100644 --- a/cps/oauth_bb.py +++ b/cps/oauth_bb.py @@ -20,11 +20,14 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see -from flask_dance.contrib.github import make_github_blueprint, github -from flask_dance.contrib.google import make_google_blueprint, google -from flask_dance.consumer import oauth_authorized, oauth_error +try: + from flask_dance.contrib.github import make_github_blueprint, github + from flask_dance.contrib.google import make_google_blueprint, google + from flask_dance.consumer import oauth_authorized, oauth_error + from oauth import OAuthBackend +except ImportError: + pass from sqlalchemy.orm.exc import NoResultFound -from oauth import OAuthBackend from flask import flash, session, redirect, url_for, request, make_response, abort import json from cps import config, app @@ -91,226 +94,226 @@ def logout_oauth_user(): if oauth + '_oauth_user_id' in session: session.pop(oauth + '_oauth_user_id') +if ub.oauth_support: + github_blueprint = make_github_blueprint( + client_id=config.config_github_oauth_client_id, + client_secret=config.config_github_oauth_client_secret, + redirect_to="github_login",) -github_blueprint = make_github_blueprint( - client_id=config.config_github_oauth_client_id, - client_secret=config.config_github_oauth_client_secret, - redirect_to="github_login",) - -google_blueprint = make_google_blueprint( - client_id=config.config_google_oauth_client_id, - client_secret=config.config_google_oauth_client_secret, - redirect_to="google_login", - scope=[ - "https://www.googleapis.com/auth/plus.me", - "https://www.googleapis.com/auth/userinfo.email", - ] -) - -app.register_blueprint(google_blueprint, url_prefix="/login") -app.register_blueprint(github_blueprint, url_prefix='/login') - -github_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) -google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) - - -if config.config_use_github_oauth: - register_oauth_blueprint(github_blueprint, 'GitHub') -if config.config_use_google_oauth: - register_oauth_blueprint(google_blueprint, 'Google') - - -@oauth_authorized.connect_via(github_blueprint) -def github_logged_in(blueprint, token): - if not token: - flash(_("Failed to log in with GitHub."), category="error") - return False - - resp = blueprint.session.get("/user") - if not resp.ok: - flash(_("Failed to fetch user info from GitHub."), category="error") - return False - - github_info = resp.json() - github_user_id = str(github_info["id"]) - return oauth_update_token(blueprint, token, github_user_id) - - -@oauth_authorized.connect_via(google_blueprint) -def google_logged_in(blueprint, token): - if not token: - flash(_("Failed to log in with Google."), category="error") - return False - - resp = blueprint.session.get("/oauth2/v2/userinfo") - if not resp.ok: - flash(_("Failed to fetch user info from Google."), category="error") - return False - - google_info = resp.json() - google_user_id = str(google_info["id"]) - - return oauth_update_token(blueprint, token, google_user_id) - - -def oauth_update_token(blueprint, token, provider_user_id): - session[blueprint.name + "_oauth_user_id"] = provider_user_id - session[blueprint.name + "_oauth_token"] = token - - # Find this OAuth token in the database, or create it - query = ub.session.query(ub.OAuth).filter_by( - provider=blueprint.name, - provider_user_id=provider_user_id, + google_blueprint = make_google_blueprint( + client_id=config.config_google_oauth_client_id, + client_secret=config.config_google_oauth_client_secret, + redirect_to="google_login", + scope=[ + "https://www.googleapis.com/auth/plus.me", + "https://www.googleapis.com/auth/userinfo.email", + ] ) - try: - oauth = query.one() - # update token - oauth.token = token - except NoResultFound: - oauth = ub.OAuth( + + app.register_blueprint(google_blueprint, url_prefix="/login") + app.register_blueprint(github_blueprint, url_prefix='/login') + + github_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) + google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) + + + if config.config_use_github_oauth: + register_oauth_blueprint(github_blueprint, 'GitHub') + if config.config_use_google_oauth: + register_oauth_blueprint(google_blueprint, 'Google') + + + @oauth_authorized.connect_via(github_blueprint) + def github_logged_in(blueprint, token): + if not token: + flash(_("Failed to log in with GitHub."), category="error") + return False + + resp = blueprint.session.get("/user") + if not resp.ok: + flash(_("Failed to fetch user info from GitHub."), category="error") + return False + + github_info = resp.json() + github_user_id = str(github_info["id"]) + return oauth_update_token(blueprint, token, github_user_id) + + + @oauth_authorized.connect_via(google_blueprint) + def google_logged_in(blueprint, token): + if not token: + flash(_("Failed to log in with Google."), category="error") + return False + + resp = blueprint.session.get("/oauth2/v2/userinfo") + if not resp.ok: + flash(_("Failed to fetch user info from Google."), category="error") + return False + + google_info = resp.json() + google_user_id = str(google_info["id"]) + + return oauth_update_token(blueprint, token, google_user_id) + + + def oauth_update_token(blueprint, token, provider_user_id): + session[blueprint.name + "_oauth_user_id"] = provider_user_id + session[blueprint.name + "_oauth_token"] = token + + # Find this OAuth token in the database, or create it + query = ub.session.query(ub.OAuth).filter_by( provider=blueprint.name, provider_user_id=provider_user_id, - token=token, ) - try: - ub.session.add(oauth) - ub.session.commit() - except Exception as e: - app.logger.exception(e) - ub.session.rollback() + try: + oauth = query.one() + # update token + oauth.token = token + except NoResultFound: + oauth = ub.OAuth( + provider=blueprint.name, + provider_user_id=provider_user_id, + token=token, + ) + try: + ub.session.add(oauth) + ub.session.commit() + except Exception as e: + app.logger.exception(e) + ub.session.rollback() - # Disable Flask-Dance's default behavior for saving the OAuth token - return False + # Disable Flask-Dance's default behavior for saving the OAuth token + return False -def bind_oauth_or_register(provider, provider_user_id, redirect_url): - query = ub.session.query(ub.OAuth).filter_by( - provider=provider, - provider_user_id=provider_user_id, - ) - try: - oauth = query.one() - # already bind with user, just login - if oauth.user: - login_user(oauth.user) - return redirect(url_for('index')) - else: - # bind to current user + def bind_oauth_or_register(provider, provider_user_id, redirect_url): + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + provider_user_id=provider_user_id, + ) + try: + oauth = query.one() + # already bind with user, just login + if oauth.user: + login_user(oauth.user) + return redirect(url_for('web.index')) + else: + # bind to current user + if current_user and current_user.is_authenticated: + oauth.user = current_user + try: + ub.session.add(oauth) + ub.session.commit() + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + return redirect(url_for('web.register')) + except NoResultFound: + return redirect(url_for(redirect_url)) + + + def get_oauth_status(): + status = [] + query = ub.session.query(ub.OAuth).filter_by( + user_id=current_user.id, + ) + try: + oauths = query.all() + for oauth in oauths: + status.append(oauth.provider) + return status + except NoResultFound: + return None + + + def unlink_oauth(provider): + if request.host_url + 'me' != request.referrer: + pass + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + user_id=current_user.id, + ) + try: + oauth = query.one() if current_user and current_user.is_authenticated: oauth.user = current_user try: - ub.session.add(oauth) + ub.session.delete(oauth) ub.session.commit() + logout_oauth_user() + flash(_("Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success") except Exception as e: app.logger.exception(e) ub.session.rollback() - return redirect(url_for('web.register')) - except NoResultFound: - return redirect(url_for(redirect_url)) + flash(_("Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") + except NoResultFound: + app.logger.warning("oauth %s for user %d not fount" % (provider, current_user.id)) + flash(_("Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") + return redirect(url_for('profile')) -def get_oauth_status(): - status = [] - query = ub.session.query(ub.OAuth).filter_by( - user_id=current_user.id, - ) - try: - oauths = query.all() - for oauth in oauths: - status.append(oauth.provider) - return status - except NoResultFound: - return None + # notify on OAuth provider error + @oauth_error.connect_via(github_blueprint) + def github_error(blueprint, error, error_description=None, error_uri=None): + msg = ( + "OAuth error from {name}! " + "error={error} description={description} uri={uri}" + ).format( + name=blueprint.name, + error=error, + description=error_description, + uri=error_uri, + ) + flash(msg, category="error") + ''' + @oauth.route('/github') + @github_oauth_required + def github_login(): + if not github.authorized: + return redirect(url_for('github.login')) + account_info = github.get('/user') + if account_info.ok: + account_info_json = account_info.json() + return bind_oauth_or_register(github_blueprint.name, account_info_json['id'], 'github.login') + flash(_(u"GitHub Oauth error, please retry later."), category="error") + return redirect(url_for('web.login')) + + + @oauth.route('/unlink/github', methods=["GET"]) + @login_required + def github_login_unlink(): + return unlink_oauth(github_blueprint.name) + + + @oauth.route('/google') + @google_oauth_required + def google_login(): + if not google.authorized: + return redirect(url_for("google.login")) + resp = google.get("/oauth2/v2/userinfo") + if resp.ok: + account_info_json = resp.json() + return bind_oauth_or_register(google_blueprint.name, account_info_json['id'], 'google.login') + flash(_(u"Google Oauth error, please retry later."), category="error") + return redirect(url_for('web.login')) + ''' -def unlink_oauth(provider): - if request.host_url + 'me' != request.referrer: - pass - query = ub.session.query(ub.OAuth).filter_by( - provider=provider, - user_id=current_user.id, - ) - try: - oauth = query.one() - if current_user and current_user.is_authenticated: - oauth.user = current_user - try: - ub.session.delete(oauth) - ub.session.commit() - logout_oauth_user() - flash(_("Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success") - except Exception as e: - app.logger.exception(e) - ub.session.rollback() - flash(_("Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") - except NoResultFound: - app.logger.warning("oauth %s for user %d not fount" % (provider, current_user.id)) - flash(_("Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") - return redirect(url_for('profile')) + @oauth_error.connect_via(google_blueprint) + def google_error(blueprint, error, error_description=None, error_uri=None): + msg = ( + "OAuth error from {name}! " + "error={error} description={description} uri={uri}" + ).format( + name=blueprint.name, + error=error, + description=error_description, + uri=error_uri, + ) + flash(msg, category="error") - -# notify on OAuth provider error -@oauth_error.connect_via(github_blueprint) -def github_error(blueprint, error, error_description=None, error_uri=None): - msg = ( - "OAuth error from {name}! " - "error={error} description={description} uri={uri}" - ).format( - name=blueprint.name, - error=error, - description=error_description, - uri=error_uri, - ) - flash(msg, category="error") - -''' -@oauth.route('/github') -@github_oauth_required -def github_login(): - if not github.authorized: - return redirect(url_for('github.login')) - account_info = github.get('/user') - if account_info.ok: - account_info_json = account_info.json() - return bind_oauth_or_register(github_blueprint.name, account_info_json['id'], 'github.login') - flash(_(u"GitHub Oauth error, please retry later."), category="error") - return redirect(url_for('login')) - - -@oauth.route('/unlink/github', methods=["GET"]) -@login_required -def github_login_unlink(): - return unlink_oauth(github_blueprint.name) - - -@oauth.route('/google') -@google_oauth_required -def google_login(): - if not google.authorized: - return redirect(url_for("google.login")) - resp = google.get("/oauth2/v2/userinfo") - if resp.ok: - account_info_json = resp.json() - return bind_oauth_or_register(google_blueprint.name, account_info_json['id'], 'google.login') - flash(_(u"Google Oauth error, please retry later."), category="error") - return redirect(url_for('login')) -''' - -@oauth_error.connect_via(google_blueprint) -def google_error(blueprint, error, error_description=None, error_uri=None): - msg = ( - "OAuth error from {name}! " - "error={error} description={description} uri={uri}" - ).format( - name=blueprint.name, - error=error, - description=error_description, - uri=error_uri, - ) - flash(msg, category="error") - -''' -@oauth.route('/unlink/google', methods=["GET"]) -@login_required -def google_login_unlink(): - return unlink_oauth(google_blueprint.name)''' + ''' + @oauth.route('/unlink/google', methods=["GET"]) + @login_required + def google_login_unlink(): + return unlink_oauth(google_blueprint.name)''' diff --git a/cps/opds.py b/cps/opds.py index dd6ed984..419cdea2 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -38,7 +38,6 @@ from werkzeug.security import check_password_hash from werkzeug.datastructures import Headers try: from urllib.parse import quote - from imp import reload except ImportError: from urllib import quote @@ -315,7 +314,7 @@ def authenticate(): def render_xml_template(*args, **kwargs): #ToDo: return time in current timezone similar to %z currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00") - xml = render_template(current_time=currtime, *args, **kwargs) + xml = render_template(current_time=currtime, instance=config.config_calibre_web_title, *args, **kwargs) response = make_response(xml) response.headers["Content-Type"] = "application/atom+xml; charset=utf-8" return response diff --git a/cps/shelf.py b/cps/shelf.py index 6300a0ce..34d8eb47 100644 --- a/cps/shelf.py +++ b/cps/shelf.py @@ -40,7 +40,7 @@ def add_to_shelf(shelf_id, book_id): app.logger.info("Invalid shelf specified") if not request.is_xhr: flash(_(u"Invalid shelf specified"), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) return "Invalid shelf specified", 400 if not shelf.is_public and not shelf.user_id == int(current_user.id): @@ -48,14 +48,14 @@ def add_to_shelf(shelf_id, book_id): if not request.is_xhr: flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403 if shelf.is_public and not current_user.role_edit_shelfs(): app.logger.info("User is not allowed to edit public shelves") if not request.is_xhr: flash(_(u"You are not allowed to edit public shelves"), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) return "User is not allowed to edit public shelves", 403 book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, @@ -64,7 +64,7 @@ def add_to_shelf(shelf_id, book_id): app.logger.info("Book is already part of the shelf: %s" % shelf.name) if not request.is_xhr: flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) return "Book is already part of the shelf: %s" % shelf.name, 400 maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() @@ -81,7 +81,7 @@ def add_to_shelf(shelf_id, book_id): if "HTTP_REFERER" in request.environ: return redirect(request.environ["HTTP_REFERER"]) else: - return redirect(url_for('index')) + return redirect(url_for('web.index')) return "", 204 @@ -92,17 +92,17 @@ def search_to_shelf(shelf_id): if shelf is None: app.logger.info("Invalid shelf specified") flash(_(u"Invalid shelf specified"), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) if not shelf.is_public and not shelf.user_id == int(current_user.id): app.logger.info("You are not allowed to add a book to the the shelf: %s" % shelf.name) flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) if shelf.is_public and not current_user.role_edit_shelfs(): app.logger.info("User is not allowed to edit public shelves") flash(_(u"User is not allowed to edit public shelves"), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) if current_user.id in searched_ids and searched_ids[current_user.id]: books_for_shelf = list() @@ -120,7 +120,7 @@ def search_to_shelf(shelf_id): if not books_for_shelf: app.logger.info("Books are already part of the shelf: %s" % shelf.name) flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() if maxOrder[0] is None: @@ -146,7 +146,7 @@ def remove_from_shelf(shelf_id, book_id): if shelf is None: app.logger.info("Invalid shelf specified") if not request.is_xhr: - return redirect(url_for('index')) + return redirect(url_for('web.index')) return "Invalid shelf specified", 400 # if shelf is public and use is allowed to edit shelfs, or if shelf is private and user is owner @@ -165,7 +165,7 @@ def remove_from_shelf(shelf_id, book_id): if book_shelf is None: app.logger.info("Book already removed from shelf") if not request.is_xhr: - return redirect(url_for('index')) + return redirect(url_for('web.index')) return "Book already removed from shelf", 410 ub.session.delete(book_shelf) @@ -180,7 +180,7 @@ def remove_from_shelf(shelf_id, book_id): if not request.is_xhr: flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403 diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 0a3f4bb9..664a5b4f 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -19,7 +19,7 @@

{{_('Delete formats:')}}

{% for file in book.data %} {% endfor %}
@@ -28,7 +28,7 @@ {% if source_formats|length > 0 and conversion_formats|length > 0 %}

{{_('Convert book format:')}}

-
+
diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index a7d0bcb9..12f04e2d 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -159,7 +159,7 @@
- {% if goodreads %} + {% if feature_support['goodreads'] %}
@@ -176,6 +176,7 @@
{% endif %} + {% if feature_support['ldap'] %}
@@ -190,6 +191,8 @@
+ {% endif %} + {% if feature_support['oauth'] %}
@@ -220,6 +223,7 @@
+ {% endif %} diff --git a/cps/templates/detail.html b/cps/templates/detail.html index a459b89e..c6019294 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -57,9 +57,22 @@ {% endif %} {% endif %} + {% if reader_list %} +
+ + +
+ {% endif %} {% if reader_list %} {% if audioentries|length %} -
+ {% endif %} {% endif %} diff --git a/cps/templates/index.xml b/cps/templates/index.xml index 7ab305aa..f7e8d6f0 100644 --- a/cps/templates/index.xml +++ b/cps/templates/index.xml @@ -2,13 +2,12 @@ urn:uuid:2853dacf-ed79-42f5-8e8a-a7bb3d1ae6a2 {{ current_time }} - - + - {{instance}} {{instance}} @@ -16,88 +15,88 @@ {{_('Hot Books')}} - - {{url_for('feed_hot')}} + + {{url_for('opds.feed_hot')}} {{ current_time }} {{_('Popular publications from this catalog based on Downloads.')}} {{_('Best rated Books')}} - - {{url_for('feed_best_rated')}} + + {{url_for('opds.feed_best_rated')}} {{ current_time }} {{_('Popular publications from this catalog based on Rating.')}} {{_('New Books')}} - - {{url_for('feed_new')}} + + {{url_for('opds.feed_new')}} {{ current_time }} {{_('The latest Books')}} {{_('Random Books')}} - - {{url_for('feed_discover')}} + + {{url_for('opds.feed_discover')}} {{ current_time }} {{_('Show Random Books')}} {% if not current_user.is_anonymous %} {{_('Read Books')}} - - {{url_for('feed_read_books')}} + + {{url_for('opds.feed_read_books')}} {{ current_time }} {{_('Read Books')}} - {% endif %} {{_('Unread Books')}} - - {{url_for('feed_unread_books')}} + + {{url_for('opds.feed_unread_books')}} {{ current_time }} {{_('Unread Books')}} + {% endif %} {{_('Authors')}} - - {{url_for('feed_authorindex')}} + + {{url_for('opds.feed_authorindex')}} {{ current_time }} {{_('Books ordered by Author')}} {{_('Publishers')}} - - {{url_for('feed_publisherindex')}} + + {{url_for('opds.feed_publisherindex')}} {{ current_time }} {{_('Books ordered by publisher')}} {{_('Category list')}} - - {{url_for('feed_categoryindex')}} + + {{url_for('opds.feed_categoryindex')}} {{ current_time }} {{_('Books ordered by category')}} {{_('Series list')}} - - {{url_for('feed_seriesindex')}} + + {{url_for('opds.feed_seriesindex')}} {{ current_time }} {{_('Books ordered by series')}} {{_('Public Shelves')}} - - {{url_for('feed_shelfindex', public="public")}} + + {{url_for('opds.feed_shelfindex', public="public")}} {{ current_time }} {{_('Books organized in public shelfs, visible to everyone')}} {% if not current_user.is_anonymous %} {{_('Your Shelves')}} - - {{url_for('feed_shelfindex')}} + + {{url_for('opds.feed_shelfindex')}} {{ current_time }} {{_("User's own shelfs, only visible to the current user himself")}} diff --git a/cps/templates/json.txt b/cps/templates/json.txt index fa5239b1..c068b1b4 100644 --- a/cps/templates/json.txt +++ b/cps/templates/json.txt @@ -1,53 +1,53 @@ { - "pubdate": "{{entry.pubdate}}", - "title": "{{entry.title}}", + "pubdate": "{{entry.pubdate}}", + "title": "{{entry.title}}", "format_metadata": { - {% for format in entry.data %} + {% for format in entry.data %} "{{format.format}}": { - "mtime": "{{entry.last_modified}}", - "size": {{format.uncompressed_size}}, + "mtime": "{{entry.last_modified}}", + "size": {{format.uncompressed_size}}, "path": "" }{% if not loop.last %},{% endif %} {% endfor %} - }, + }, "formats": [ - {% for format in entry.data %} + {% for format in entry.data %} "{{format.format}}"{% if not loop.last %},{% endif %} - {% endfor %} - ], - "series": null, - "cover": "{{url_for('feed_get_cover', book_id=entry.id)}}", + {% endfor %} + ], + "series": null, + "cover": "{{url_for('opds.feed_get_cover', book_id=entry.id)}}", "languages": [ - {% for lang in entry.languages %} + {% for lang in entry.languages %} "{{lang.lang_code}}"{% if not loop.last %},{% endif %} {% endfor %} - ], + ], "comments": "{% if entry.comments|length > 0 %}{{entry.comments[0].text.replace('"', '\\"')|safe}}{% endif %}", "tags": [ {% for tag in entry.tags %} "{{tag.name}}"{% if not loop.last %},{% endif %} - {% endfor %} - ], - "application_id": {{entry.id}}, - "series_index": {% if entry.series|length > 0 %}"{{entry.series_index}}"{% else %}null{% endif %}, - "last_modified": "{{entry.last_modified}}", - "author_sort": "{{entry.author_sort}}", - "uuid": "{{entry.uuid}}", - "timestamp": "{{entry.timestamp}}", - "thumbnail": "{{url_for('feed_get_cover', book_id=entry.id)}}", + {% endfor %} + ], + "application_id": {{entry.id}}, + "series_index": {% if entry.series|length > 0 %}"{{entry.series_index}}"{% else %}null{% endif %}, + "last_modified": "{{entry.last_modified}}", + "author_sort": "{{entry.author_sort}}", + "uuid": "{{entry.uuid}}", + "timestamp": "{{entry.timestamp}}", + "thumbnail": "{{url_for('opds.feed_get_cover', book_id=entry.id)}}", "main_format": { - "{{entry.data[0].format|lower}}": "{{ url_for('get_opds_download_link', book_id=entry.id, book_format=entry.data[0].format|lower)}}" + "{{entry.data[0].format|lower}}": "{{ url_for('opds.get_opds_download_link', book_id=entry.id, book_format=entry.data[0].format|lower)}}" }, "rating":{% if entry.ratings.__len__() > 0 %} "{{entry.ratings[0].rating}}.0"{% else %}0.0{% endif %}, "authors": [ {% for author in entry.authors %} "{{author.name.replace('|',',')}}"{% if not loop.last %},{% endif %} - {% endfor %} - ], + {% endfor %} + ], "other_formats": { {% if entry.data.__len__() > 1 %} {% for format in entry.data[1:] %} - "{{format.format|lower}}": "{{ url_for('get_opds_download_link', book_id=entry.id, book_format=format.format|lower)}}"{% if not loop.last %},{% endif %} + "{{format.format|lower}}": "{{ url_for('opds.get_opds_download_link', book_id=entry.id, book_format=format.format|lower)}}"{% if not loop.last %},{% endif %} {% endfor %} {% endif %} }, "title_sort": "{{entry.sort}}" diff --git a/cps/templates/layout.html b/cps/templates/layout.html index ef83e842..897c6cb6 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -161,7 +161,7 @@ {% if g.user.is_authenticated or g.user.is_anonymous %} {% for shelf in g.public_shelfes %} -
  • {{shelf.name|shortentitle(40)}}
  • +
  • {{shelf.name|shortentitle(40)}}
  • {% endfor %} {% for shelf in g.user.shelf %} diff --git a/cps/templates/osd.xml b/cps/templates/osd.xml index b88e6823..bb741bb5 100644 --- a/cps/templates/osd.xml +++ b/cps/templates/osd.xml @@ -6,9 +6,9 @@ Janeczku https://github.com/janeczku/calibre-web + template="{{url_for('opds.search')}}?query={searchTerms}"/> + template="{{url_for('opds.feed_normal_search')}}?query={searchTerms}"/> open {{lang}} UTF-8 diff --git a/cps/ub.py b/cps/ub.py index 59f0b613..dbc42ac4 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -23,7 +23,6 @@ from sqlalchemy import exc from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import * from flask_login import AnonymousUserMixin -from flask_dance.consumer.backend.sqla import OAuthConsumerMixin import sys import os import logging @@ -34,8 +33,11 @@ from binascii import hexlify import cli try: + from flask_dance.consumer.backend.sqla import OAuthConsumerMixin import ldap + oauth_support = True except ImportError: + oauth_support = False pass engine = create_engine('sqlite:///{0}'.format(cli.settingspath), echo=False) @@ -207,11 +209,11 @@ class User(UserBase, Base): default_language = Column(String(3), default="all") mature_content = Column(Boolean, default=True) - -class OAuth(OAuthConsumerMixin, Base): - provider_user_id = Column(String(256)) - user_id = Column(Integer, ForeignKey(User.id)) - user = relationship(User) +if oauth_support: + class OAuth(OAuthConsumerMixin, Base): + provider_user_id = Column(String(256)) + user_id = Column(Integer, ForeignKey(User.id)) + user = relationship(User) # Class for anonymous user is derived from User base and completly overrides methods and properties for the @@ -834,7 +836,7 @@ def delete_download(book_id): session.commit() # Generate user Guest (translated text), as anoymous user, no rights -def create_anonymous_user(session): +def create_anonymous_user(): user = User() user.nickname = "Guest" user.email = 'no@email' diff --git a/cps/updater.py b/cps/updater.py index b01646a0..e5dd6bae 100644 --- a/cps/updater.py +++ b/cps/updater.py @@ -18,11 +18,10 @@ # along with this program. If not, see . -from cps import config, get_locale +from cps import config, get_locale, Server, app import threading import zipfile import requests -import logging import time from io import BytesIO import os @@ -35,7 +34,6 @@ import json from flask_babel import gettext as _ from babel.dates import format_datetime -import server def is_sha1(sha1): if len(sha1) != 40: @@ -69,39 +67,45 @@ class Updater(threading.Thread): def run(self): try: self.status = 1 - r = requests.get(self._get_request_path(), stream=True) + app.logger.debug(u'Download update file') + headers = {'Accept': 'application/vnd.github.v3+json'} + r = requests.get(self._get_request_path(), stream=True, headers=headers) r.raise_for_status() self.status = 2 + app.logger.debug(u'Opening zipfile') z = zipfile.ZipFile(BytesIO(r.content)) self.status = 3 + app.logger.debug(u'Extracting zipfile') tmp_dir = gettempdir() z.extractall(tmp_dir) foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1] if not os.path.isdir(foldername): self.status = 11 - logging.getLogger('cps.web').info(u'Extracted contents of zipfile not found in temp folder') + app.logger.info(u'Extracted contents of zipfile not found in temp folder') return self.status = 4 + app.logger.debug(u'Replacing files') self.update_source(foldername, config.get_main_dir) self.status = 6 + app.logger.debug(u'Preparing restart of server') time.sleep(2) - server.Server.setRestartTyp(True) - server.Server.stopServer() + Server.setRestartTyp(True) + Server.stopServer() self.status = 7 time.sleep(2) except requests.exceptions.HTTPError as ex: - logging.getLogger('cps.web').info( u'HTTP Error' + ' ' + str(ex)) + app.logger.info( u'HTTP Error' + ' ' + str(ex)) self.status = 8 except requests.exceptions.ConnectionError: - logging.getLogger('cps.web').info(u'Connection error') + app.logger.info(u'Connection error') self.status = 9 except requests.exceptions.Timeout: - logging.getLogger('cps.web').info(u'Timeout while establishing connection') + app.logger.info(u'Timeout while establishing connection') self.status = 10 except requests.exceptions.RequestException: self.status = 11 - logging.getLogger('cps.web').info(u'General error') + app.logger.info(u'General error') def get_update_status(self): return self.status @@ -149,14 +153,14 @@ class Updater(threading.Thread): if sys.platform == "win32" or sys.platform == "darwin": change_permissions = False else: - logging.getLogger('cps.web').debug('Update on OS-System : ' + sys.platform) + app.logger.debug('Update on OS-System : ' + sys.platform) new_permissions = os.stat(root_dst_dir) # print new_permissions for src_dir, __, files in os.walk(root_src_dir): dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1) if not os.path.exists(dst_dir): os.makedirs(dst_dir) - logging.getLogger('cps.web').debug('Create-Dir: '+dst_dir) + app.logger.debug('Create-Dir: '+dst_dir) if change_permissions: # print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid)) os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid) @@ -166,20 +170,20 @@ class Updater(threading.Thread): if os.path.exists(dst_file): if change_permissions: permission = os.stat(dst_file) - logging.getLogger('cps.web').debug('Remove file before copy: '+dst_file) + app.logger.debug('Remove file before copy: '+dst_file) os.remove(dst_file) else: if change_permissions: permission = new_permissions shutil.move(src_file, dst_dir) - logging.getLogger('cps.web').debug('Move File '+src_file+' to '+dst_dir) + app.logger.debug('Move File '+src_file+' to '+dst_dir) if change_permissions: try: os.chown(dst_file, permission.st_uid, permission.st_gid) except (Exception) as e: # ex = sys.exc_info() old_permissions = os.stat(dst_file) - logging.getLogger('cps.web').debug('Fail change permissions of ' + str(dst_file) + '. Before: ' + app.logger.debug('Fail change permissions of ' + str(dst_file) + '. Before: ' + str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: ' + str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e)) return @@ -215,15 +219,15 @@ class Updater(threading.Thread): for item in remove_items: item_path = os.path.join(destination, item[1:]) if os.path.isdir(item_path): - logging.getLogger('cps.web').debug("Delete dir " + item_path) + app.logger.debug("Delete dir " + item_path) shutil.rmtree(item_path, ignore_errors=True) else: try: - logging.getLogger('cps.web').debug("Delete file " + item_path) + app.logger.debug("Delete file " + item_path) # log_from_thread("Delete file " + item_path) os.remove(item_path) except Exception: - logging.getLogger('cps.web').debug("Could not remove:" + item_path) + app.logger.debug("Could not remove:" + item_path) shutil.rmtree(source, ignore_errors=True) def _nightly_version_info(self): @@ -263,7 +267,8 @@ class Updater(threading.Thread): status['update'] = True try: - r = requests.get(repository_url + '/git/commits/' + commit['object']['sha']) + headers = {'Accept': 'application/vnd.github.v3+json'} + r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'], headers=headers) r.raise_for_status() update_data = r.json() except requests.exceptions.HTTPError as e: @@ -310,7 +315,8 @@ class Updater(threading.Thread): # check if we are more than one update behind if so, go up the tree if parent_commit['sha'] != status['current_commit_hash']: try: - r = requests.get(parent_commit['url']) + headers = {'Accept': 'application/vnd.github.v3+json'} + r = requests.get(parent_commit['url'], headers=headers) r.raise_for_status() parent_data = r.json() @@ -368,7 +374,8 @@ class Updater(threading.Thread): # check if we are more than one update behind if so, go up the tree if commit['sha'] != status['current_commit_hash']: try: - r = requests.get(parent_commit['url']) + headers = {'Accept': 'application/vnd.github.v3+json'} + r = requests.get(parent_commit['url'], headers=headers) r.raise_for_status() parent_data = r.json() @@ -492,7 +499,8 @@ class Updater(threading.Thread): else: status['current_commit_hash'] = version['version'] try: - r = requests.get(repository_url) + headers = {'Accept': 'application/vnd.github.v3+json'} + r = requests.get(repository_url, headers=headers) commit = r.json() r.raise_for_status() except requests.exceptions.HTTPError as e: diff --git a/cps/web.py b/cps/web.py index 91cc8ff0..ef87712f 100644 --- a/cps/web.py +++ b/cps/web.py @@ -47,18 +47,19 @@ from cps import lm, babel, ub, config, get_locale, language_table, app from pagination import Pagination from sqlalchemy.sql.expression import text -from oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status - -'''try: - oauth_support = True +feature_support = dict() +try: + from oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status + feature_support['oauth'] = True except ImportError: - oauth_support = False''' + feature_support['oauth'] = False + oauth_check = {} try: import ldap - ldap_support = True + feature_support['ldap'] = True except ImportError: - ldap_support = False + feature_support['ldap'] = False try: from googleapiclient.errors import HttpError @@ -67,15 +68,15 @@ except ImportError: try: from goodreads.client import GoodreadsClient - goodreads_support = True + feature_support['goodreads'] = True except ImportError: - goodreads_support = False + feature_support['goodreads'] = False try: import Levenshtein - levenshtein_support = True + feature_support['levenshtein'] = True except ImportError: - levenshtein_support = False + feature_support['levenshtein'] = False try: from functools import reduce, wraps @@ -84,9 +85,9 @@ except ImportError: try: import rarfile - rar_support=True + feature_support['rar'] = True except ImportError: - rar_support=False + feature_support['rar'] = False try: from natsort import natsorted as sort @@ -95,18 +96,17 @@ except ImportError: try: from urllib.parse import quote - from imp import reload except ImportError: from urllib import quote - from flask import Blueprint # Global variables EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'} -# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else [])) +'''EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + + (['rar','cbr'] if feature_support['rar'] else []))''' # custom error page @@ -346,8 +346,8 @@ def before_request(): g.allow_upload = config.config_uploading g.current_theme = config.config_theme g.public_shelfes = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1).order_by(ub.Shelf.name).all() - if not config.db_configured and request.endpoint not in ('web.basic_configuration', 'login') and '/static/' not in request.path: - return redirect(url_for('web.basic_configuration')) + if not config.db_configured and request.endpoint not in ('admin.basic_configuration', 'login') and '/static/' not in request.path: + return redirect(url_for('admin.basic_configuration')) @web.route("/ajax/emailstat") @@ -373,7 +373,7 @@ def get_comic_book(book_id, book_format, page): if bookformat.format.lower() == book_format.lower(): cbr_file = os.path.join(config.config_calibre_dir, book.path, bookformat.name) + "." + book_format if book_format in ("cbr", "rar"): - if rar_support == True: + if feature_support['rar'] == True: rarfile.UNRAR_TOOL = config.config_rarfile_location try: rf = rarfile.RarFile(cbr_file) @@ -636,7 +636,7 @@ def author(book_id, page): author_info = None other_books = [] - if goodreads_support and config.config_use_goodreads: + if feature_support['goodreads'] and config.config_use_goodreads: try: gc = GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret) author_info = gc.find_author(author_name=name) @@ -688,7 +688,7 @@ def get_unique_other_books(library_books, author_books): author_books) # Fuzzy match book titles - if levenshtein_support: + if feature_support['levenshtein']: library_titles = reduce(lambda acc, book: acc + [book.title], library_books, []) other_books = filter(lambda author_book: not filter( lambda library_book: @@ -1215,7 +1215,7 @@ def read_book(book_id, book_format): # copyfile(cbr_file, tmp_file) return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), extension=fileext) - '''if rar_support == True: + '''if feature_support['rar']: extensionList = ["cbr","cbt","cbz"] else: extensionList = ["cbt","cbz"] @@ -1292,7 +1292,7 @@ def register(): try: ub.session.add(content) ub.session.commit() - if oauth_support: + if feature_support['oauth']: register_user_with_oauth(content) helper.send_registration_mail(to_save["email"], to_save["nickname"], password) except Exception: @@ -1310,7 +1310,7 @@ def register(): flash(_(u"This username or e-mail address is already in use."), category="error") return render_title_template('register.html', title=_(u"register"), page="register") - if oauth_support: + if feature_support['oauth']: register_user_with_oauth() return render_title_template('register.html', config=config, title=_(u"register"), page="register") @@ -1318,7 +1318,7 @@ def register(): @web.route('/login', methods=['GET', 'POST']) def login(): if not config.db_configured: - return redirect(url_for('web.basic_configuration')) + return redirect(url_for('admin.basic_configuration')) if current_user is not None and current_user.is_authenticated: return redirect(url_for('web.index')) if request.method == "POST": @@ -1358,7 +1358,7 @@ def login(): def logout(): if current_user is not None and current_user.is_authenticated: logout_user() - if oauth_support: + if feature_support['oauth']: logout_oauth_user() return redirect(url_for('web.login')) @@ -1370,7 +1370,7 @@ def remote_login(): ub.session.add(auth_token) ub.session.commit() - verify_url = url_for('verify_token', token=auth_token.auth_token, _external=true) + verify_url = url_for('web.verify_token', token=auth_token.auth_token, _external=true) return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token, verify_url=verify_url, page="remotelogin") @@ -1385,7 +1385,7 @@ def verify_token(token): # Token not found if auth_token is None: flash(_(u"Token not found"), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) # Token expired if datetime.datetime.now() > auth_token.expiration: @@ -1393,7 +1393,7 @@ def verify_token(token): ub.session.commit() flash(_(u"Token has expired"), category="error") - return redirect(url_for('index')) + return redirect(url_for('web.index')) # Update token with user information auth_token.user_id = current_user.id @@ -1401,7 +1401,7 @@ def verify_token(token): ub.session.commit() flash(_(u"Success! Please return to your device"), category="success") - return redirect(url_for('index')) + return redirect(url_for('web.index')) @web.route('/ajax/verify_token', methods=['POST']) @@ -1472,7 +1472,10 @@ def profile(): downloads = list() languages = speaking_language() translations = babel.list_translations() + [LC('en')] - oauth_status = get_oauth_status() + if feature_support['oauth']: + oauth_status = get_oauth_status() + else: + oauth_status = None for book in content.downloads: downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() if downloadBook: @@ -1535,9 +1538,11 @@ def profile(): ub.session.rollback() flash(_(u"Found an existing account for this e-mail address."), category="error") return render_title_template("user_edit.html", content=content, downloads=downloads, - title=_(u"%(name)s's profile", name=current_user.nickname, registered_oauth=oauth_check, oauth_status=oauth_status)) + title=_(u"%(name)s's profile", name=current_user.nickname, + registered_oauth=oauth_check, oauth_status=oauth_status)) flash(_(u"Profile updated"), category="success") return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages, - content=content, downloads=downloads, title=_(u"%(name)s's profile", - name=current_user.nickname), page="me", registered_oauth=oauth_check, oauth_status=oauth_status) + content=content, downloads=downloads, title=_(u"%(name)s's profile", + name=current_user.nickname), page="me", registered_oauth=oauth_check, + oauth_status=oauth_status) diff --git a/cps/worker.py b/cps/worker.py index c0c68833..77df162c 100644 --- a/cps/worker.py +++ b/cps/worker.py @@ -27,14 +27,12 @@ import socket import sys import os from email.generator import Generator -from cps import config, db # , app -# import web +from cps import config, db, app from flask_babel import gettext as _ import re -# import gdriveutils as gd +import gdriveutils as gd from subproc_wrapper import process_open - try: from StringIO import StringIO from email.MIMEBase import MIMEBase @@ -90,8 +88,8 @@ def get_attachment(bookpath, filename): data = file_.read() file_.close() except IOError as e: - # web.app.logger.exception(e) # traceback.print_exc() - # web.app.logger.error(u'The requested file could not be read. Maybe wrong permissions?') + app.logger.exception(e) # traceback.print_exc() + app.logger.error(u'The requested file could not be read. Maybe wrong permissions?') return None attachment = MIMEBase('application', 'octet-stream') @@ -116,8 +114,7 @@ class emailbase(): def send(self, strg): """Send `strg' to the server.""" - if self.debuglevel > 0: - print('send:', repr(strg[:300]), file=sys.stderr) + app.logger.debug('send:' + repr(strg[:300])) if hasattr(self, 'sock') and self.sock: try: if self.transferSize: @@ -141,6 +138,9 @@ class emailbase(): else: raise smtplib.SMTPServerDisconnected('please run connect() first') + def _print_debug(self, *args): + app.logger.debug(args) + def getTransferStatus(self): if self.transferSize: lock2 = threading.Lock() @@ -254,14 +254,14 @@ class WorkerThread(threading.Thread): # if it does - mark the conversion task as complete and return a success # this will allow send to kindle workflow to continue to work if os.path.isfile(file_path + format_new_ext): - # web.app.logger.info("Book id %d already converted to %s", bookid, format_new_ext) + app.logger.info("Book id %d already converted to %s", bookid, format_new_ext) cur_book = db.session.query(db.Books).filter(db.Books.id == bookid).first() self.queue[self.current]['path'] = file_path self.queue[self.current]['title'] = cur_book.title self._handleSuccess() return file_path + format_new_ext else: - web.app.logger.info("Book id %d - target format of %s does not exist. Moving forward with convert.", bookid, format_new_ext) + app.logger.info("Book id %d - target format of %s does not exist. Moving forward with convert.", bookid, format_new_ext) # check if converter-executable is existing if not os.path.exists(config.config_converterpath): @@ -274,22 +274,22 @@ class WorkerThread(threading.Thread): if format_old_ext == '.epub' and format_new_ext == '.mobi': if config.config_ebookconverter == 1: '''if os.name == 'nt': - command = web.ub.config.config_converterpath + u' "' + file_path + u'.epub"' + command = config.config_converterpath + u' "' + file_path + u'.epub"' if sys.version_info < (3, 0): command = command.encode(sys.getfilesystemencoding()) else:''' command = [config.config_converterpath, file_path + u'.epub'] - quotes = (1) + quotes = [1] if config.config_ebookconverter == 2: # Linux py2.7 encode as list without quotes no empty element for parameters # linux py3.x no encode and as list without quotes no empty element for parameters # windows py2.7 encode as string with quotes empty element for parameters is okay # windows py 3.x no encode and as string with quotes empty element for parameters is okay # separate handling for windows and linux - quotes = (1,2) + quotes = [1,2] '''if os.name == 'nt': - command = web.ub.config.config_converterpath + u' "' + file_path + format_old_ext + u'" "' + \ - file_path + format_new_ext + u'" ' + web.ub.config.config_calibre + command = config.config_converterpath + u' "' + file_path + format_old_ext + u'" "' + \ + file_path + format_new_ext + u'" ' + config.config_calibre if sys.version_info < (3, 0): command = command.encode(sys.getfilesystemencoding()) else:''' @@ -317,13 +317,13 @@ class WorkerThread(threading.Thread): if conv_error: error_message = _(u"Kindlegen failed with Error %(error)s. Message: %(message)s", error=conv_error.group(1), message=conv_error.group(2).strip()) - web.app.logger.debug("convert_kindlegen: " + nextline) + app.logger.debug("convert_kindlegen: " + nextline) else: while p.poll() is None: nextline = p.stdout.readline() if os.name == 'nt' and sys.version_info < (3, 0): nextline = nextline.decode('windows-1252') - web.app.logger.debug(nextline.strip('\r\n')) + app.logger.debug(nextline.strip('\r\n')) # parse progress string from calibre-converter progress = re.search("(\d+)%\s.*", nextline) if progress: @@ -353,7 +353,7 @@ class WorkerThread(threading.Thread): return file_path + format_new_ext else: error_message = format_new_ext.upper() + ' format not found on disk' - # web.app.logger.info("ebook converter failed with error while converting book") + app.logger.info("ebook converter failed with error while converting book") if not error_message: error_message = 'Ebook converter failed with unknown error' self._handleError(error_message) @@ -414,7 +414,6 @@ class WorkerThread(threading.Thread): def _send_raw_email(self): self.queue[self.current]['starttime'] = datetime.now() self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime'] - # self.queue[self.current]['status'] = STAT_STARTED self.UIqueue[self.current]['stat'] = STAT_STARTED obj=self.queue[self.current] # create MIME message @@ -446,8 +445,11 @@ class WorkerThread(threading.Thread): # send email timeout = 600 # set timeout to 5mins - org_stderr = sys.stderr - sys.stderr = StderrLogger() + # redirect output to logfile on python2 pn python3 debugoutput is caught with overwritten + # _print_debug function + if sys.version_info < (3, 0): + org_smtpstderr = smtplib.stderr + smtplib.stderr = StderrLogger() if use_ssl == 2: self.asyncSMTP = email_SSL(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout) @@ -455,7 +457,7 @@ class WorkerThread(threading.Thread): self.asyncSMTP = email(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout) # link to logginglevel - if web.ub.config.config_log_level != logging.DEBUG: + if config.config_log_level != logging.DEBUG: self.asyncSMTP.set_debuglevel(0) else: self.asyncSMTP.set_debuglevel(1) @@ -466,7 +468,9 @@ class WorkerThread(threading.Thread): self.asyncSMTP.sendmail(obj['settings']["mail_from"], obj['recipent'], msg) self.asyncSMTP.quit() self._handleSuccess() - sys.stderr = org_stderr + + if sys.version_info < (3, 0): + smtplib.stderr = org_smtpstderr except (MemoryError) as e: self._handleError(u'Error sending email: ' + e.message) @@ -497,7 +501,7 @@ class WorkerThread(threading.Thread): return retVal def _handleError(self, error_message): - web.app.logger.error(error_message) + app.logger.error(error_message) # self.queue[self.current]['status'] = STAT_FAIL self.UIqueue[self.current]['stat'] = STAT_FAIL self.UIqueue[self.current]['progress'] = "100 %" @@ -519,13 +523,12 @@ class StderrLogger(object): buffer = '' def __init__(self): - self.logger = web.app.logger + self.logger = app.logger def write(self, message): try: if message == '\n': - self.logger.debug(self.buffer) - print(self.buffer) + self.logger.debug(self.buffer.replace("\n","\\n")) self.buffer = '' else: self.buffer += message diff --git a/optional-requirements.txt b/optional-requirements.txt index 399acec4..9b740f6c 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -11,12 +11,19 @@ PyDrive==1.3.1 PyYAML==3.12 rsa==3.4.2 six==1.10.0 + # goodreads goodreads>=0.3.2 python-Levenshtein>=0.12.0 + # ldap login python_ldap>=3.0.0 + # other lxml>=3.8.0 rarfile>=2.7 natsort>=2.2.0 + +# Oauth Login +flask-dance>=0.13.0 +sqlalchemy_utils>=0.33.5 diff --git a/requirements.txt b/requirements.txt index 2b13eb54..3fb23ea3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,3 @@ SQLAlchemy>=1.1.0 tornado>=4.1 Wand>=0.4.4 unidecode>=0.04.19 -flask-dance>=0.13.0 -sqlalchemy_utils>=0.33.5 diff --git a/test/Calibre-Web TestSummary.html b/test/Calibre-Web TestSummary.html index 97d89880..32ddf212 100644 --- a/test/Calibre-Web TestSummary.html +++ b/test/Calibre-Web TestSummary.html @@ -30,15 +30,15 @@
    -

    Start Time: 2019-01-27 07:20:26.105524

    +

    Start Time: 2019-02-10 12:10:31.096065

    -

    Stop Time: 2019-01-27 08:08:50.419347

    +

    Stop Time: 2019-02-10 12:53:18.410539

    -

    Duration: 0:48:24.313823

    +

    Duration: 0:42:47.314474

    @@ -577,97 +577,92 @@ - + test_email_ssl.test_SSL_Python27 4 - 4 - 0 + 1 0 + 3 0 Detail - +
    test_SSL_None_setup_error
    - PASS + ERROR
    -