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
pull/823/head
Ozzieisaacs 6 years ago
parent 1dc6f44828
commit 4230226716

@ -21,6 +21,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import os import os
from flask import Blueprint, flash, redirect, url_for from flask import Blueprint, flash, redirect, url_for
from flask import abort, request, make_response from flask import abort, request, make_response
@ -39,20 +40,26 @@ from gdriveutils import is_gdrive_ready, gdrive_support, downloadFile, deleteDat
import helper import helper
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from oauth_bb import oauth_check 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: try:
from goodreads.client import GoodreadsClient from goodreads.client import GoodreadsClient
goodreads_support = True feature_support['goodreads'] = True
except ImportError: except ImportError:
goodreads_support = False feature_support['goodreads'] = False
try: try:
import rarfile import rarfile
rar_support = True feature_support['rar'] = True
except ImportError: except ImportError:
rar_support = False feature_support['rar'] = False
feature_support['gdrive'] = gdrive_support
admi = Blueprint('admin', __name__) admi = Blueprint('admin', __name__)
@ -287,7 +294,7 @@ def configuration_helper(origin):
db_change = False db_change = False
success = False success = False
filedata = None filedata = None
if gdrive_support is False: if not feature_support['gdrive']:
gdriveError = _('Import of optional Google Drive requirements missing') gdriveError = _('Import of optional Google Drive requirements missing')
else: else:
if not os.path.isfile(os.path.join(config.get_main_dir, 'client_secrets.json')): if not os.path.isfile(os.path.join(config.get_main_dir, 'client_secrets.json')):
@ -327,7 +334,7 @@ def configuration_helper(origin):
else: else:
flash(_(u'client_secrets.json is not configured for web application'), category="error") flash(_(u'client_secrets.json is not configured for web application'), category="error")
return render_title_template("config_edit.html", content=config, origin=origin, 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"), goodreads=goodreads_support, title=_(u"Basic Configuration"),
page="config") page="config")
# always show google drive settings, but in case of error deny support # always show google drive settings, but in case of error deny support
@ -353,7 +360,7 @@ def configuration_helper(origin):
ub.session.commit() ub.session.commit()
flash(_(u'Keyfile location is not valid, please enter correct path'), category="error") flash(_(u'Keyfile location is not valid, please enter correct path'), category="error")
return render_title_template("config_edit.html", content=config, origin=origin, 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"), goodreads=goodreads_support, title=_(u"Basic Configuration"),
page="config") page="config")
if "config_certfile" in to_save: if "config_certfile" in to_save:
@ -365,9 +372,8 @@ def configuration_helper(origin):
ub.session.commit() ub.session.commit()
flash(_(u'Certfile location is not valid, please enter correct path'), category="error") flash(_(u'Certfile location is not valid, please enter correct path'), category="error")
return render_title_template("config_edit.html", content=config, origin=origin, return render_title_template("config_edit.html", content=config, origin=origin,
gdrive=gdrive_support, gdriveError=gdriveError, gdriveError=gdriveError, feature_support=feature_support,
goodreads=goodreads_support, title=_(u"Basic Configuration"), title=_(u"Basic Configuration"), page="config")
page="config")
content.config_uploading = 0 content.config_uploading = 0
content.config_anonbrowse = 0 content.config_anonbrowse = 0
content.config_public_reg = 0 content.config_public_reg = 0
@ -391,9 +397,8 @@ def configuration_helper(origin):
ub.session.commit() ub.session.commit()
flash(_(u'Please enter a LDAP provider and a DN'), category="error") flash(_(u'Please enter a LDAP provider and a DN'), category="error")
return render_title_template("config_edit.html", content=config, origin=origin, return render_title_template("config_edit.html", content=config, origin=origin,
gdrive=gdrive_support, gdriveError=gdriveError, gdriveError=gdriveError, feature_support=feature_support,
goodreads=goodreads_support, title=_(u"Basic Configuration"), title=_(u"Basic Configuration"), page="config")
page="config")
else: else:
content.config_use_ldap = 1 content.config_use_ldap = 1
content.config_ldap_provider_url = to_save["config_ldap_provider_url"] content.config_ldap_provider_url = to_save["config_ldap_provider_url"]
@ -450,9 +455,8 @@ def configuration_helper(origin):
ub.session.commit() ub.session.commit()
flash(_(u'Logfile location is not valid, please enter correct path'), category="error") flash(_(u'Logfile location is not valid, please enter correct path'), category="error")
return render_title_template("config_edit.html", content=config, origin=origin, return render_title_template("config_edit.html", content=config, origin=origin,
gdrive=gdrive_support, gdriveError=gdriveError, gdriveError=gdriveError, feature_support=feature_support,
goodreads=goodreads_support, title=_(u"Basic Configuration"), title=_(u"Basic Configuration"), page="config")
page="config")
else: else:
content.config_logfile = to_save["config_logfile"] content.config_logfile = to_save["config_logfile"]
reboot_required = True reboot_required = True
@ -465,8 +469,7 @@ def configuration_helper(origin):
else: else:
flash(check[1], category="error") flash(check[1], category="error")
return render_title_template("config_edit.html", content=config, origin=origin, return render_title_template("config_edit.html", content=config, origin=origin,
gdrive=gdrive_support, goodreads=goodreads_support, feature_support=feature_support, title=_(u"Basic Configuration"))
rarfile_support=rar_support, title=_(u"Basic Configuration"))
try: try:
if content.config_use_google_drive and is_gdrive_ready() and not \ if content.config_use_google_drive and is_gdrive_ready() and not \
os.path.exists(os.path.join(content.config_calibre_dir, "metadata.db")): 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") flash(_(u"Calibre-Web configuration updated"), category="success")
config.loadSettings() config.loadSettings()
app.logger.setLevel(config.config_log_level) 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: except Exception as e:
flash(e, category="error") flash(e, category="error")
return render_title_template("config_edit.html", content=config, origin=origin, return render_title_template("config_edit.html", content=config, origin=origin,
gdrive=gdrive_support, gdriveError=gdriveError, gdriveError=gdriveError, feature_support=feature_support,
goodreads=goodreads_support, rarfile_support=rar_support,
title=_(u"Basic Configuration"), page="config") title=_(u"Basic Configuration"), page="config")
if db_change: if db_change:
reload(db) reload(db)
if not db.setup_db(): if not db.setup_db():
flash(_(u'DB location is not valid, please enter correct path'), category="error") flash(_(u'DB location is not valid, please enter correct path'), category="error")
return render_title_template("config_edit.html", content=config, origin=origin, return render_title_template("config_edit.html", content=config, origin=origin,
gdrive=gdrive_support, gdriveError=gdriveError, gdriveError=gdriveError, feature_support=feature_support,
goodreads=goodreads_support, rarfile_support=rar_support,
title=_(u"Basic Configuration"), page="config") title=_(u"Basic Configuration"), page="config")
if reboot_required: if reboot_required:
# stop Server # stop Server
@ -501,15 +502,14 @@ def configuration_helper(origin):
app.logger.info('Reboot required, restarting') app.logger.info('Reboot required, restarting')
if origin: if origin:
success = True 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() gdrivefolders = listRootFolders()
else: else:
gdrivefolders = list() gdrivefolders = list()
return render_title_template("config_edit.html", origin=origin, success=success, content=config, return render_title_template("config_edit.html", origin=origin, success=success, content=config,
show_authenticate_google_drive=not is_gdrive_ready(), show_authenticate_google_drive=not is_gdrive_ready(),
gdrive=gdrive_support, gdriveError=gdriveError, gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support,
gdrivefolders=gdrivefolders, rarfile_support=rar_support, title=_(u"Basic Configuration"), page="config")
goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config")
@admi.route("/admin/user/new", methods=["GET", "POST"]) @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"]: if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
flash(_(u"Please fill out all fields!"), category="error") flash(_(u"Please fill out all fields!"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, 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.password = generate_password_hash(to_save["password"])
content.nickname = to_save["nickname"] content.nickname = to_save["nickname"]
if config.config_public_reg and not check_valid_domain(to_save["email"]): if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error") 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, 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: else:
content.email = to_save["email"] content.email = to_save["email"]
try: try:
ub.session.add(content) ub.session.add(content)
ub.session.commit() ub.session.commit()
flash(_(u"User '%(user)s' created", user=content.nickname), category="success") flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
return redirect(url_for('admin')) return redirect(url_for('admin.admin'))
except IntegrityError: except IntegrityError:
ub.session.rollback() ub.session.rollback()
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") 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.sidebar_view = config.config_default_show
content.mature_content = bool(config.config_default_show & ub.MATURE_CONTENT) 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, 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"]) @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.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session.commit() ub.session.commit()
flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
return redirect(url_for('admin')) return redirect(url_for('admin.admin'))
else: else:
if "password" in to_save and to_save["password"]: if "password" in to_save and to_save["password"]:
content.password = generate_password_hash(to_save["password"]) content.password = generate_password_hash(to_save["password"])
@ -766,8 +767,8 @@ def edit_user(user_id):
ub.session.rollback() ub.session.rollback()
flash(_(u"An unknown error occured."), category="error") flash(_(u"An unknown error occured."), category="error")
return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0,
content=content, downloads=downloads, title=_(u"Edit User %(nick)s", content=content, downloads=downloads, registered_oauth=oauth_check,
nick=content.nickname), page="edituser", registered_oauth=oauth_check) title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
@admi.route("/admin/resetpassword/<int:user_id>") @admi.route("/admin/resetpassword/<int:user_id>")
@ -787,7 +788,7 @@ def reset_password(user_id):
except Exception: except Exception:
ub.session.rollback() ub.session.rollback()
flash(_(u"An unknown error occurred. Please try again later."), category="error") 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']) @admi.route("/get_update_status", methods=['GET'])

@ -42,7 +42,7 @@ from iso639 import languages as isoLanguages
editbook = Blueprint('editbook', __name__) 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', EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'} 'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'}
@ -380,7 +380,7 @@ def upload_single_file(request, book, book_id):
# Queue uploader info # Queue uploader info
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
global_WorkerThread.add_upload(current_user.nickname, global_WorkerThread.add_upload(current_user.nickname,
"<a href=\"" + url_for('show_book', book_id=book.id) + "\">" + uploadText + "</a>") "<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")
def upload_cover(request, book): def upload_cover(request, book):
if 'btn-upload-cover' in request.files: if 'btn-upload-cover' in request.files:
@ -589,10 +589,10 @@ def upload():
flash( flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server", _("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error") ext=file_ext), category="error")
return redirect(url_for('index')) return redirect(url_for('web.index'))
else: else:
flash(_('File to be uploaded must have an extension'), category="error") 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 # extract metadata from file
meta = uploader.upload(requested_file) meta = uploader.upload(requested_file)
@ -612,12 +612,12 @@ def upload():
os.makedirs(filepath) os.makedirs(filepath)
except OSError: except OSError:
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") 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: try:
copyfile(meta.file_path, saved_filename) copyfile(meta.file_path, saved_filename)
except OSError: except OSError:
flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error") 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: try:
os.unlink(meta.file_path) os.unlink(meta.file_path)
except OSError: except OSError:

@ -70,7 +70,7 @@ def google_drive_callback():
f.write(credentials.to_json()) f.write(credentials.to_json())
except ValueError as error: except ValueError as error:
app.logger.error(error) app.logger.error(error)
return redirect(url_for('configuration')) return redirect(url_for('admin.configuration'))
@gdrive.route("/gdrive/watch/subscribe") @gdrive.route("/gdrive/watch/subscribe")
@ -102,7 +102,7 @@ def watch_gdrive():
else: else:
flash(reason['message'], category="error") flash(reason['message'], category="error")
return redirect(url_for('configuration')) return redirect(url_for('admin.configuration'))
@gdrive.route("/gdrive/watch/revoke") @gdrive.route("/gdrive/watch/revoke")
@ -121,7 +121,7 @@ def revoke_watch_gdrive():
ub.session.merge(settings) ub.session.merge(settings)
ub.session.commit() ub.session.commit()
config.loadSettings() config.loadSettings()
return redirect(url_for('configuration')) return redirect(url_for('admin.configuration'))
@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST']) @gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST'])

@ -19,13 +19,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import db from cps import config, global_WorkerThread, get_locale
from cps import config
from flask import current_app as app from flask import current_app as app
from tempfile import gettempdir from tempfile import gettempdir
import sys import sys
import os import os
import re import re
import db
import unicodedata import unicodedata
import worker import worker
import time import time
@ -40,7 +40,6 @@ try:
import gdriveutils as gd import gdriveutils as gd
except ImportError: except ImportError:
pass pass
# import web
import random import random
from subproc_wrapper import process_open from subproc_wrapper import process_open
import ub import ub
@ -244,7 +243,7 @@ def get_sorted_author(value):
else: else:
value2 = value value2 = value
except Exception: except Exception:
web.app.logger.error("Sorting author " + str(value) + "failed") app.logger.error("Sorting author " + str(value) + "failed")
value2 = value value2 = value
return value2 return value2
@ -261,13 +260,13 @@ def delete_book_file(book, calibrepath, book_format=None):
else: else:
if os.path.isdir(path): if os.path.isdir(path):
if len(next(os.walk(path))[1]): 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) "Deleting book " + str(book.id) + " failed, path has subfolders: " + book.path)
return False return False
shutil.rmtree(path, ignore_errors=True) shutil.rmtree(path, ignore_errors=True)
return True return True
else: 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 return False
@ -290,7 +289,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
if not os.path.exists(new_title_path): if not os.path.exists(new_title_path):
os.renames(path, new_title_path) os.renames(path, new_title_path)
else: 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 dir_name, subdir_list, file_list in os.walk(path):
for file in file_list: for file in file_list:
os.renames(os.path.join(dir_name, file), 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 path = new_title_path
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
except OSError as ex: except OSError as ex:
web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex)) app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex))
web.app.logger.debug(ex, exc_info=True) app.logger.debug(ex, exc_info=True)
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_title_path, error=str(ex)) src=path, dest=new_title_path, error=str(ex))
if authordir != new_authordir: if authordir != new_authordir:
@ -308,8 +307,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
os.renames(path, new_author_path) os.renames(path, new_author_path)
localbook.path = new_authordir + '/' + localbook.path.split('/')[1] localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
except OSError as ex: except OSError as ex:
web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex)) app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex))
web.app.logger.debug(ex, exc_info=True) app.logger.debug(ex, exc_info=True)
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_author_path, error=str(ex)) src=path, dest=new_author_path, error=str(ex))
# Rename all files from old names to new names # 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())) os.path.join(path_name,new_name + '.' + file_format.format.lower()))
file_format.name = new_name file_format.name = new_name
except OSError as ex: except OSError as ex:
web.app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex)) app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex))
web.app.logger.debug(ex, exc_info=True) app.logger.debug(ex, exc_info=True)
return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s", return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_name, error=str(ex)) src=path, dest=new_name, error=str(ex))
return False return False
@ -418,17 +417,17 @@ def delete_book(book, calibrepath, book_format):
def get_book_cover(cover_path): def get_book_cover(cover_path):
if config.config_use_google_drive: if config.config_use_google_drive:
try: 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") return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
path=gd.get_cover_via_gdrive(cover_path) path=gd.get_cover_via_gdrive(cover_path)
if path: if path:
return redirect(path) return redirect(path)
else: 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") return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
except Exception as e: except Exception as e:
web.app.logger.error("Error Message: " + e.message) app.logger.error("Error Message: " + e.message)
web.app.logger.exception(e) app.logger.exception(e)
# traceback.print_exc() # traceback.print_exc()
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg") return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg")
else: else:
@ -439,7 +438,7 @@ def get_book_cover(cover_path):
def save_cover(url, book_path): def save_cover(url, book_path):
img = requests.get(url) img = requests.get(url)
if img.headers.get('content-type') != 'image/jpeg': 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 return False
if config.config_use_google_drive: if config.config_use_google_drive:
@ -448,13 +447,13 @@ def save_cover(url, book_path):
f.write(img.content) f.write(img.content)
f.close() f.close()
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name)) 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 return True
f = open(os.path.join(config.config_calibre_dir, book_path, "cover.jpg"), "wb") f = open(os.path.join(config.config_calibre_dir, book_path, "cover.jpg"), "wb")
f.write(img.content) f.write(img.content)
f.close() f.close()
web.app.logger.info("Cover is saved") app.logger.info("Cover is saved")
return True return True
@ -462,7 +461,7 @@ def do_download_file(book, book_format, data, headers):
if config.config_use_google_drive: if config.config_use_google_drive:
startTime = time.time() startTime = time.time()
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format) df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
web.app.logger.debug(time.time() - startTime) app.logger.debug(time.time() - startTime)
if df: if df:
return gd.do_gdrive_download(df, headers) return gd.do_gdrive_download(df, headers)
else: else:
@ -471,7 +470,7 @@ def do_download_file(book, book_format, data, headers):
filename = os.path.join(config.config_calibre_dir, book.path) filename = os.path.join(config.config_calibre_dir, book.path)
if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)): if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)):
# ToDo: improve error handling # 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 = make_response(send_from_directory(filename, data.name + "." + book_format))
response.headers = headers response.headers = headers
return response return response
@ -497,7 +496,7 @@ def check_unrar(unrarLocation):
version = value.group(1) version = value.group(1)
except OSError as e: except OSError as e:
error = True error = True
web.app.logger.exception(e) app.logger.exception(e)
version =_(u'Error excecuting UnRar') version =_(u'Error excecuting UnRar')
else: else:
version = _(u'Unrar binary file not found') 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(): if task['user'] == current_user.nickname or current_user.role_admin():
# task2 = copy.deepcopy(task) # = task # task2 = copy.deepcopy(task) # = task
if task['formStarttime']: 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'] = "" # task2['formStarttime'] = ""
else: else:
if 'starttime' not in task: if 'starttime' not in task:

@ -2,133 +2,136 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask import session from flask import session
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user try:
from sqlalchemy.orm.exc import NoResultFound from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound
class OAuthBackend(SQLAlchemyBackend): class OAuthBackend(SQLAlchemyBackend):
""" """
Stores and retrieves OAuth tokens using a relational database through Stores and retrieves OAuth tokens using a relational database through
the `SQLAlchemy`_ ORM. the `SQLAlchemy`_ ORM.
.. _SQLAlchemy: http://www.sqlalchemy.org/ .. _SQLAlchemy: http://www.sqlalchemy.org/
""" """
def __init__(self, model, session, def __init__(self, model, session,
user=None, user_id=None, user_required=None, anon_user=None, user=None, user_id=None, user_required=None, anon_user=None,
cache=None): cache=None):
super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache) super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache)
def get(self, blueprint, user=None, user_id=None): def get(self, blueprint, user=None, user_id=None):
if blueprint.name + '_oauth_token' in session and session[blueprint.name + '_oauth_token'] != '': if blueprint.name + '_oauth_token' in session and session[blueprint.name + '_oauth_token'] != '':
return session[blueprint.name + '_oauth_token'] return session[blueprint.name + '_oauth_token']
# check cache # check cache
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
token = self.cache.get(cache_key) token = self.cache.get(cache_key)
if token: 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 return token
# if not cached, make database queries def set(self, blueprint, token, user=None, user_id=None):
query = ( uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
self.session.query(self.model) u = first(_get_real_user(ref, self.anon_user)
.filter_by(provider=blueprint.name) for ref in (user, self.user, blueprint.config.get("user")))
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) if self.user_required and not u and not uid:
u = first(_get_real_user(ref, self.anon_user) raise ValueError("Cannot set OAuth token without an associated user")
for ref in (user, self.user, blueprint.config.get("user")))
# if there was an existing model, delete it
use_provider_user_id = False existing_query = (
if blueprint.name + '_oauth_user_id' in session and session[blueprint.name + '_oauth_user_id'] != '': self.session.query(self.model)
query = query.filter_by(provider_user_id=session[blueprint.name + '_oauth_user_id']) .filter_by(provider=blueprint.name)
use_provider_user_id = True )
# check for user ID
if self.user_required and not u and not uid and not use_provider_user_id: has_user_id = hasattr(self.model, "user_id")
#raise ValueError("Cannot get OAuth token without an associated user") if has_user_id and uid:
return None existing_query = existing_query.filter_by(user_id=uid)
# check for user ID # check for user (relationship property)
if hasattr(self.model, "user_id") and uid: has_user = hasattr(self.model, "user")
query = query.filter_by(user_id=uid) if has_user and u:
# check for user (relationship property) existing_query = existing_query.filter_by(user=u)
elif hasattr(self.model, "user") and u: # queue up delete query -- won't be run until commit()
query = query.filter_by(user=u) existing_query.delete()
# if we have the property, but not value, filter by None # create a new model for this token
elif hasattr(self.model, "user_id"): kwargs = {
query = query.filter_by(user_id=None) "provider": blueprint.name,
# run query "token": token,
try: }
token = query.one().token if has_user_id and uid:
except NoResultFound: kwargs["user_id"] = uid
token = None if has_user and u:
kwargs["user"] = u
# cache the result self.session.add(self.model(**kwargs))
self.cache.set(cache_key, token) # commit to delete and add simultaneously
self.session.commit()
return token # invalidate cache
self.cache.delete(self.make_cache_key(
def set(self, blueprint, token, user=None, user_id=None): blueprint=blueprint, user=user, user_id=user_id
uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) ))
u = first(_get_real_user(ref, self.anon_user)
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"))) for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid: if self.user_required and not u and not uid:
raise ValueError("Cannot set OAuth token without an associated user") raise ValueError("Cannot delete OAuth token without an associated user")
# if there was an existing model, delete it # check for user ID
existing_query = ( if hasattr(self.model, "user_id") and uid:
self.session.query(self.model) query = query.filter_by(user_id=uid)
.filter_by(provider=blueprint.name) # check for user (relationship property)
) elif hasattr(self.model, "user") and u:
# check for user ID query = query.filter_by(user=u)
has_user_id = hasattr(self.model, "user_id") # if we have the property, but not value, filter by None
if has_user_id and uid: elif hasattr(self.model, "user_id"):
existing_query = existing_query.filter_by(user_id=uid) query = query.filter_by(user_id=None)
# check for user (relationship property) # run query
has_user = hasattr(self.model, "user") query.delete()
if has_user and u: self.session.commit()
existing_query = existing_query.filter_by(user=u) # invalidate cache
# queue up delete query -- won't be run until commit() self.cache.delete(self.make_cache_key(
existing_query.delete() blueprint=blueprint, user=user, user_id=user_id,
# create a new model for this token ))
kwargs = {
"provider": blueprint.name, except ImportError:
"token": token, pass
}
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,
))

@ -20,11 +20,14 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
from flask_dance.contrib.github import make_github_blueprint, github try:
from flask_dance.contrib.google import make_google_blueprint, google from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.consumer import oauth_authorized, oauth_error 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 sqlalchemy.orm.exc import NoResultFound
from oauth import OAuthBackend
from flask import flash, session, redirect, url_for, request, make_response, abort from flask import flash, session, redirect, url_for, request, make_response, abort
import json import json
from cps import config, app from cps import config, app
@ -91,226 +94,226 @@ def logout_oauth_user():
if oauth + '_oauth_user_id' in session: if oauth + '_oauth_user_id' in session:
session.pop(oauth + '_oauth_user_id') 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",)
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",
]
)
github_blueprint = make_github_blueprint( app.register_blueprint(google_blueprint, url_prefix="/login")
client_id=config.config_github_oauth_client_id, app.register_blueprint(github_blueprint, url_prefix='/login')
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) 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) google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True)
if config.config_use_github_oauth: if config.config_use_github_oauth:
register_oauth_blueprint(github_blueprint, 'GitHub') register_oauth_blueprint(github_blueprint, 'GitHub')
if config.config_use_google_oauth: if config.config_use_google_oauth:
register_oauth_blueprint(google_blueprint, 'Google') register_oauth_blueprint(google_blueprint, 'Google')
@oauth_authorized.connect_via(github_blueprint) @oauth_authorized.connect_via(github_blueprint)
def github_logged_in(blueprint, token): def github_logged_in(blueprint, token):
if not token: if not token:
flash(_("Failed to log in with GitHub."), category="error") flash(_("Failed to log in with GitHub."), category="error")
return False return False
resp = blueprint.session.get("/user") resp = blueprint.session.get("/user")
if not resp.ok: if not resp.ok:
flash(_("Failed to fetch user info from GitHub."), category="error") flash(_("Failed to fetch user info from GitHub."), category="error")
return False return False
github_info = resp.json() github_info = resp.json()
github_user_id = str(github_info["id"]) github_user_id = str(github_info["id"])
return oauth_update_token(blueprint, token, github_user_id) return oauth_update_token(blueprint, token, github_user_id)
@oauth_authorized.connect_via(google_blueprint) @oauth_authorized.connect_via(google_blueprint)
def google_logged_in(blueprint, token): def google_logged_in(blueprint, token):
if not token: if not token:
flash(_("Failed to log in with Google."), category="error") flash(_("Failed to log in with Google."), category="error")
return False return False
resp = blueprint.session.get("/oauth2/v2/userinfo") resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok: if not resp.ok:
flash(_("Failed to fetch user info from Google."), category="error") flash(_("Failed to fetch user info from Google."), category="error")
return False return False
google_info = resp.json() google_info = resp.json()
google_user_id = str(google_info["id"]) google_user_id = str(google_info["id"])
return oauth_update_token(blueprint, token, google_user_id) return oauth_update_token(blueprint, token, google_user_id)
def oauth_update_token(blueprint, token, provider_user_id): def oauth_update_token(blueprint, token, provider_user_id):
session[blueprint.name + "_oauth_user_id"] = provider_user_id session[blueprint.name + "_oauth_user_id"] = provider_user_id
session[blueprint.name + "_oauth_token"] = token session[blueprint.name + "_oauth_token"] = token
# Find this OAuth token in the database, or create it # Find this OAuth token in the database, or create it
query = ub.session.query(ub.OAuth).filter_by( query = ub.session.query(ub.OAuth).filter_by(
provider=blueprint.name,
provider_user_id=provider_user_id,
)
try:
oauth = query.one()
# update token
oauth.token = token
except NoResultFound:
oauth = ub.OAuth(
provider=blueprint.name, provider=blueprint.name,
provider_user_id=provider_user_id, provider_user_id=provider_user_id,
token=token,
) )
try: try:
ub.session.add(oauth) oauth = query.one()
ub.session.commit() # update token
except Exception as e: oauth.token = token
app.logger.exception(e) except NoResultFound:
ub.session.rollback() oauth = ub.OAuth(
provider=blueprint.name,
# Disable Flask-Dance's default behavior for saving the OAuth token provider_user_id=provider_user_id,
return False 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
def bind_oauth_or_register(provider, provider_user_id, redirect_url): def bind_oauth_or_register(provider, provider_user_id, redirect_url):
query = ub.session.query(ub.OAuth).filter_by( query = ub.session.query(ub.OAuth).filter_by(
provider=provider, provider=provider,
provider_user_id=provider_user_id, provider_user_id=provider_user_id,
) )
try: try:
oauth = query.one() oauth = query.one()
# already bind with user, just login # already bind with user, just login
if oauth.user: if oauth.user:
login_user(oauth.user) login_user(oauth.user)
return redirect(url_for('index')) return redirect(url_for('web.index'))
else: else:
# bind to current user # 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: if current_user and current_user.is_authenticated:
oauth.user = current_user oauth.user = current_user
try: try:
ub.session.add(oauth) ub.session.delete(oauth)
ub.session.commit() ub.session.commit()
logout_oauth_user()
flash(_("Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success")
except Exception as e: except Exception as e:
app.logger.exception(e) app.logger.exception(e)
ub.session.rollback() ub.session.rollback()
return redirect(url_for('web.register')) flash(_("Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error")
except NoResultFound: except NoResultFound:
return redirect(url_for(redirect_url)) 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( # notify on OAuth provider error
user_id=current_user.id, @oauth_error.connect_via(github_blueprint)
) def github_error(blueprint, error, error_description=None, error_uri=None):
try: msg = (
oauths = query.all() "OAuth error from {name}! "
for oauth in oauths: "error={error} description={description} uri={uri}"
status.append(oauth.provider) ).format(
return status name=blueprint.name,
except NoResultFound: error=error,
return None description=error_description,
uri=error_uri,
)
def unlink_oauth(provider): flash(msg, category="error")
if request.host_url + 'me' != request.referrer:
pass '''
query = ub.session.query(ub.OAuth).filter_by( @oauth.route('/github')
provider=provider, @github_oauth_required
user_id=current_user.id, def github_login():
) if not github.authorized:
try: return redirect(url_for('github.login'))
oauth = query.one() account_info = github.get('/user')
if current_user and current_user.is_authenticated: if account_info.ok:
oauth.user = current_user account_info_json = account_info.json()
try: return bind_oauth_or_register(github_blueprint.name, account_info_json['id'], 'github.login')
ub.session.delete(oauth) flash(_(u"GitHub Oauth error, please retry later."), category="error")
ub.session.commit() return redirect(url_for('web.login'))
logout_oauth_user()
flash(_("Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success")
except Exception as e: @oauth.route('/unlink/github', methods=["GET"])
app.logger.exception(e) @login_required
ub.session.rollback() def github_login_unlink():
flash(_("Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") return unlink_oauth(github_blueprint.name)
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") @oauth.route('/google')
return redirect(url_for('profile')) @google_oauth_required
def google_login():
if not google.authorized:
# notify on OAuth provider error return redirect(url_for("google.login"))
@oauth_error.connect_via(github_blueprint) resp = google.get("/oauth2/v2/userinfo")
def github_error(blueprint, error, error_description=None, error_uri=None): if resp.ok:
msg = ( account_info_json = resp.json()
"OAuth error from {name}! " return bind_oauth_or_register(google_blueprint.name, account_info_json['id'], 'google.login')
"error={error} description={description} uri={uri}" flash(_(u"Google Oauth error, please retry later."), category="error")
).format( return redirect(url_for('web.login'))
name=blueprint.name, '''
error=error,
description=error_description, @oauth_error.connect_via(google_blueprint)
uri=error_uri, def google_error(blueprint, error, error_description=None, error_uri=None):
) msg = (
flash(msg, category="error") "OAuth error from {name}! "
"error={error} description={description} uri={uri}"
''' ).format(
@oauth.route('/github') name=blueprint.name,
@github_oauth_required error=error,
def github_login(): description=error_description,
if not github.authorized: uri=error_uri,
return redirect(url_for('github.login')) )
account_info = github.get('/user') flash(msg, category="error")
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"]) @oauth.route('/unlink/google', methods=["GET"])
@login_required @login_required
def google_login_unlink(): def google_login_unlink():
return unlink_oauth(google_blueprint.name)''' return unlink_oauth(google_blueprint.name)'''

@ -38,7 +38,6 @@ from werkzeug.security import check_password_hash
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
try: try:
from urllib.parse import quote from urllib.parse import quote
from imp import reload
except ImportError: except ImportError:
from urllib import quote from urllib import quote
@ -315,7 +314,7 @@ def authenticate():
def render_xml_template(*args, **kwargs): def render_xml_template(*args, **kwargs):
#ToDo: return time in current timezone similar to %z #ToDo: return time in current timezone similar to %z
currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00") 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 = make_response(xml)
response.headers["Content-Type"] = "application/atom+xml; charset=utf-8" response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
return response return response

@ -40,7 +40,7 @@ def add_to_shelf(shelf_id, book_id):
app.logger.info("Invalid shelf specified") app.logger.info("Invalid shelf specified")
if not request.is_xhr: if not request.is_xhr:
flash(_(u"Invalid shelf specified"), category="error") flash(_(u"Invalid shelf specified"), category="error")
return redirect(url_for('index')) return redirect(url_for('web.index'))
return "Invalid shelf specified", 400 return "Invalid shelf specified", 400
if not shelf.is_public and not shelf.user_id == int(current_user.id): 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: 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), flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name),
category="error") 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 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(): if shelf.is_public and not current_user.role_edit_shelfs():
app.logger.info("User is not allowed to edit public shelves") app.logger.info("User is not allowed to edit public shelves")
if not request.is_xhr: if not request.is_xhr:
flash(_(u"You are not allowed to edit public shelves"), category="error") 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 return "User is not allowed to edit public shelves", 403
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, 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) app.logger.info("Book is already part of the shelf: %s" % shelf.name)
if not request.is_xhr: if not request.is_xhr:
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") 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 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() 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: if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"]) return redirect(request.environ["HTTP_REFERER"])
else: else:
return redirect(url_for('index')) return redirect(url_for('web.index'))
return "", 204 return "", 204
@ -92,17 +92,17 @@ def search_to_shelf(shelf_id):
if shelf is None: if shelf is None:
app.logger.info("Invalid shelf specified") app.logger.info("Invalid shelf specified")
flash(_(u"Invalid shelf specified"), category="error") 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): 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) 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") 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(): if shelf.is_public and not current_user.role_edit_shelfs():
app.logger.info("User is not allowed to edit public shelves") app.logger.info("User is not allowed to edit public shelves")
flash(_(u"User is not allowed to edit public shelves"), category="error") 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]: if current_user.id in searched_ids and searched_ids[current_user.id]:
books_for_shelf = list() books_for_shelf = list()
@ -120,7 +120,7 @@ def search_to_shelf(shelf_id):
if not books_for_shelf: if not books_for_shelf:
app.logger.info("Books are already part of the shelf: %s" % shelf.name) 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") 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() maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()
if maxOrder[0] is None: if maxOrder[0] is None:
@ -146,7 +146,7 @@ def remove_from_shelf(shelf_id, book_id):
if shelf is None: if shelf is None:
app.logger.info("Invalid shelf specified") app.logger.info("Invalid shelf specified")
if not request.is_xhr: if not request.is_xhr:
return redirect(url_for('index')) return redirect(url_for('web.index'))
return "Invalid shelf specified", 400 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 # 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: if book_shelf is None:
app.logger.info("Book already removed from shelf") app.logger.info("Book already removed from shelf")
if not request.is_xhr: if not request.is_xhr:
return redirect(url_for('index')) return redirect(url_for('web.index'))
return "Book already removed from shelf", 410 return "Book already removed from shelf", 410
ub.session.delete(book_shelf) ub.session.delete(book_shelf)
@ -180,7 +180,7 @@ def remove_from_shelf(shelf_id, book_id):
if not request.is_xhr: if not request.is_xhr:
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
category="error") 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 return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403

@ -19,7 +19,7 @@
<div class="text-center more-stuff"><h4>{{_('Delete formats:')}}</h4> <div class="text-center more-stuff"><h4>{{_('Delete formats:')}}</h4>
{% for file in book.data %} {% for file in book.data %}
<div class="form-group"> <div class="form-group">
<a href="{{ url_for('web.delete_book', book_id=book.id, book_format=file.format) }}" class="btn btn-danger" type="button">{{_('Delete')}} - {{file.format}}</a> <a href="{{ url_for('editbook.delete_book', book_id=book.id, book_format=file.format) }}" class="btn btn-danger" type="button">{{_('Delete')}} - {{file.format}}</a>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -28,7 +28,7 @@
{% if source_formats|length > 0 and conversion_formats|length > 0 %} {% if source_formats|length > 0 and conversion_formats|length > 0 %}
<div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4> <div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4>
<form class="padded-bottom" action="{{ url_for('web.convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm"> <form class="padded-bottom" action="{{ url_for('editbook.convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
<div class="form-group"> <div class="form-group">
<div class="text-left"> <div class="text-left">
<label class="control-label" for="book_format_from">{{_('Convert from:')}}</label> <label class="control-label" for="book_format_from">{{_('Convert from:')}}</label>

@ -159,7 +159,7 @@
<input type="checkbox" id="config_remote_login" name="config_remote_login" {% if content.config_remote_login %}checked{% endif %}> <input type="checkbox" id="config_remote_login" name="config_remote_login" {% if content.config_remote_login %}checked{% endif %}>
<label for="config_remote_login">{{_('Enable remote login ("magic link")')}}</label> <label for="config_remote_login">{{_('Enable remote login ("magic link")')}}</label>
</div> </div>
{% if goodreads %} {% if feature_support['goodreads'] %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" id="config_use_goodreads" name="config_use_goodreads" data-control="goodreads-settings" {% if content.config_use_goodreads %}checked{% endif %}> <input type="checkbox" id="config_use_goodreads" name="config_use_goodreads" data-control="goodreads-settings" {% if content.config_use_goodreads %}checked{% endif %}>
<label for="config_use_goodreads">{{_('Use')}} Goodreads</label> <label for="config_use_goodreads">{{_('Use')}} Goodreads</label>
@ -176,6 +176,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if feature_support['ldap'] %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" id="config_use_ldap" name="config_use_ldap" data-control="ldap-settings" {% if content.config_use_ldap %}checked{% endif %}> <input type="checkbox" id="config_use_ldap" name="config_use_ldap" data-control="ldap-settings" {% if content.config_use_ldap %}checked{% endif %}>
<label for="config_use_ldap">{{_('Use')}} LDAP Authentication</label> <label for="config_use_ldap">{{_('Use')}} LDAP Authentication</label>
@ -190,6 +191,8 @@
<input type="text" class="form-control" id="config_ldap_dn" name="config_ldap_dn" value="{% if content.config_use_ldap != None %}{{ content.config_ldap_dn }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_ldap_dn" name="config_ldap_dn" value="{% if content.config_use_ldap != None %}{{ content.config_ldap_dn }}{% endif %}" autocomplete="off">
</div> </div>
</div> </div>
{% endif %}
{% if feature_support['oauth'] %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" id="config_use_github_oauth" name="config_use_github_oauth" data-control="github-oauth-settings" {% if content.config_use_github_oauth %}checked{% endif %}> <input type="checkbox" id="config_use_github_oauth" name="config_use_github_oauth" data-control="github-oauth-settings" {% if content.config_use_github_oauth %}checked{% endif %}>
<label for="config_use_github_oauth">{{_('Use')}} GitHub OAuth</label> <label for="config_use_github_oauth">{{_('Use')}} GitHub OAuth</label>
@ -220,6 +223,7 @@
<input type="text" class="form-control" id="config_google_oauth_client_secret" name="config_google_oauth_client_secret" value="{% if content.config_google_oauth_client_secret != None %}{{ content.config_google_oauth_client_secret }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_google_oauth_client_secret" name="config_google_oauth_client_secret" value="{% if content.config_google_oauth_client_secret != None %}{{ content.config_google_oauth_client_secret }}{% endif %}" autocomplete="off">
</div> </div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

@ -57,9 +57,22 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if reader_list %}
<div class="btn-group" role="group">
<button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-eye-open"></span> {{_('Read in browser')}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="read-in-browser">
{% for format in reader_list %}
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
{%endfor%}
</ul>
</div>
{% endif %}
{% if reader_list %} {% if reader_list %}
{% if audioentries|length %} {% if audioentries|length %}
<div class="btn-group" role="group"> <!--div class="btn-group" role="group">
<button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-music"></span> {{_('Listen in browser')}} <span class="glyphicon glyphicon-music"></span> {{_('Listen in browser')}}
<span class="caret"></span> <span class="caret"></span>
@ -77,7 +90,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div-->
{% endif %} {% endif %}
{% endif %} {% endif %}

@ -2,13 +2,12 @@
<feed xmlns="http://www.w3.org/2005/Atom"> <feed xmlns="http://www.w3.org/2005/Atom">
<id>urn:uuid:2853dacf-ed79-42f5-8e8a-a7bb3d1ae6a2</id> <id>urn:uuid:2853dacf-ed79-42f5-8e8a-a7bb3d1ae6a2</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/> <link rel="self" href="{{url_for('opds.feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}" <link rel="start" title="{{_('Start')}}" href="{{url_for('opds.feed_index')}}"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/> type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="search" <link rel="search"
href="{{url_for('feed_osd')}}" href="{{url_for('opds.feed_osd')}}"
type="application/opensearchdescription+xml"/> type="application/opensearchdescription+xml"/>
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
<title>{{instance}}</title> <title>{{instance}}</title>
<author> <author>
<name>{{instance}}</name> <name>{{instance}}</name>
@ -16,88 +15,88 @@
</author> </author>
<entry> <entry>
<title>{{_('Hot Books')}}</title> <title>{{_('Hot Books')}}</title>
<link rel="http://opds-spec.org/sort/popular" href="{{url_for('feed_hot')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="http://opds-spec.org/sort/popular" href="{{url_for('opds.feed_hot')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_hot')}}</id> <id>{{url_for('opds.feed_hot')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Popular publications from this catalog based on Downloads.')}}</content> <content type="text">{{_('Popular publications from this catalog based on Downloads.')}}</content>
</entry> </entry>
<entry> <entry>
<title>{{_('Best rated Books')}}</title> <title>{{_('Best rated Books')}}</title>
<link rel="http://opds-spec.org/recommended" href="{{url_for('feed_best_rated')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="http://opds-spec.org/recommended" href="{{url_for('opds.feed_best_rated')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_best_rated')}}</id> <id>{{url_for('opds.feed_best_rated')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Popular publications from this catalog based on Rating.')}}</content> <content type="text">{{_('Popular publications from this catalog based on Rating.')}}</content>
</entry> </entry>
<entry> <entry>
<title>{{_('New Books')}}</title> <title>{{_('New Books')}}</title>
<link rel="http://opds-spec.org/sort/new" href="{{url_for('feed_new')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="http://opds-spec.org/sort/new" href="{{url_for('opds.feed_new')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_new')}}</id> <id>{{url_for('opds.feed_new')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('The latest Books')}}</content> <content type="text">{{_('The latest Books')}}</content>
</entry> </entry>
<entry> <entry>
<title>{{_('Random Books')}}</title> <title>{{_('Random Books')}}</title>
<link rel="http://opds-spec.org/featured" href="{{url_for('feed_discover')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="http://opds-spec.org/featured" href="{{url_for('opds.feed_discover')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_discover')}}</id> <id>{{url_for('opds.feed_discover')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Show Random Books')}}</content> <content type="text">{{_('Show Random Books')}}</content>
</entry> </entry>
{% if not current_user.is_anonymous %} {% if not current_user.is_anonymous %}
<entry> <entry>
<title>{{_('Read Books')}}</title> <title>{{_('Read Books')}}</title>
<link rel="subsection" href="{{url_for('feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('opds.feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_read_books')}}</id> <id>{{url_for('opds.feed_read_books')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Read Books')}}</content> <content type="text">{{_('Read Books')}}</content>
</entry> </entry>
{% endif %}
<entry> <entry>
<title>{{_('Unread Books')}}</title> <title>{{_('Unread Books')}}</title>
<link rel="subsection" href="{{url_for('feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('opds.feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_unread_books')}}</id> <id>{{url_for('opds.feed_unread_books')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Unread Books')}}</content> <content type="text">{{_('Unread Books')}}</content>
</entry> </entry>
{% endif %}
<entry> <entry>
<title>{{_('Authors')}}</title> <title>{{_('Authors')}}</title>
<link rel="subsection" href="{{url_for('feed_authorindex')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('opds.feed_authorindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_authorindex')}}</id> <id>{{url_for('opds.feed_authorindex')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by Author')}}</content> <content type="text">{{_('Books ordered by Author')}}</content>
</entry> </entry>
<entry> <entry>
<title>{{_('Publishers')}}</title> <title>{{_('Publishers')}}</title>
<link rel="subsection" href="{{url_for('feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('opds.feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_publisherindex')}}</id> <id>{{url_for('opds.feed_publisherindex')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by publisher')}}</content> <content type="text">{{_('Books ordered by publisher')}}</content>
</entry> </entry>
<entry> <entry>
<title>{{_('Category list')}}</title> <title>{{_('Category list')}}</title>
<link rel="subsection" href="{{url_for('feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('opds.feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_categoryindex')}}</id> <id>{{url_for('opds.feed_categoryindex')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by category')}}</content> <content type="text">{{_('Books ordered by category')}}</content>
</entry> </entry>
<entry> <entry>
<title>{{_('Series list')}}</title> <title>{{_('Series list')}}</title>
<link rel="subsection" href="{{url_for('feed_seriesindex')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('opds.feed_seriesindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_seriesindex')}}</id> <id>{{url_for('opds.feed_seriesindex')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by series')}}</content> <content type="text">{{_('Books ordered by series')}}</content>
</entry> </entry>
<entry> <entry>
<title>{{_('Public Shelves')}}</title> <title>{{_('Public Shelves')}}</title>
<link rel="subsection" href="{{url_for('feed_shelfindex', public="public")}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('opds.feed_shelfindex', public="public")}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_shelfindex', public="public")}}</id> <id>{{url_for('opds.feed_shelfindex', public="public")}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Books organized in public shelfs, visible to everyone')}}</content> <content type="text">{{_('Books organized in public shelfs, visible to everyone')}}</content>
</entry> </entry>
{% if not current_user.is_anonymous %} {% if not current_user.is_anonymous %}
<entry> <entry>
<title>{{_('Your Shelves')}}</title> <title>{{_('Your Shelves')}}</title>
<link rel="subsection" href="{{url_for('feed_shelfindex')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('opds.feed_shelfindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_shelfindex')}}</id> <id>{{url_for('opds.feed_shelfindex')}}</id>
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_("User's own shelfs, only visible to the current user himself")}}</content> <content type="text">{{_("User's own shelfs, only visible to the current user himself")}}</content>
</entry> </entry>

@ -1,53 +1,53 @@
{ {
"pubdate": "{{entry.pubdate}}", "pubdate": "{{entry.pubdate}}",
"title": "{{entry.title}}", "title": "{{entry.title}}",
"format_metadata": { "format_metadata": {
{% for format in entry.data %} {% for format in entry.data %}
"{{format.format}}": { "{{format.format}}": {
"mtime": "{{entry.last_modified}}", "mtime": "{{entry.last_modified}}",
"size": {{format.uncompressed_size}}, "size": {{format.uncompressed_size}},
"path": "" "path": ""
}{% if not loop.last %},{% endif %} }{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
}, },
"formats": [ "formats": [
{% for format in entry.data %} {% for format in entry.data %}
"{{format.format}}"{% if not loop.last %},{% endif %} "{{format.format}}"{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
], ],
"series": null, "series": null,
"cover": "{{url_for('feed_get_cover', book_id=entry.id)}}", "cover": "{{url_for('opds.feed_get_cover', book_id=entry.id)}}",
"languages": [ "languages": [
{% for lang in entry.languages %} {% for lang in entry.languages %}
"{{lang.lang_code}}"{% if not loop.last %},{% endif %} "{{lang.lang_code}}"{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
], ],
"comments": "{% if entry.comments|length > 0 %}{{entry.comments[0].text.replace('"', '\\"')|safe}}{% endif %}", "comments": "{% if entry.comments|length > 0 %}{{entry.comments[0].text.replace('"', '\\"')|safe}}{% endif %}",
"tags": [ "tags": [
{% for tag in entry.tags %} {% for tag in entry.tags %}
"{{tag.name}}"{% if not loop.last %},{% endif %} "{{tag.name}}"{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
], ],
"application_id": {{entry.id}}, "application_id": {{entry.id}},
"series_index": {% if entry.series|length > 0 %}"{{entry.series_index}}"{% else %}null{% endif %}, "series_index": {% if entry.series|length > 0 %}"{{entry.series_index}}"{% else %}null{% endif %},
"last_modified": "{{entry.last_modified}}", "last_modified": "{{entry.last_modified}}",
"author_sort": "{{entry.author_sort}}", "author_sort": "{{entry.author_sort}}",
"uuid": "{{entry.uuid}}", "uuid": "{{entry.uuid}}",
"timestamp": "{{entry.timestamp}}", "timestamp": "{{entry.timestamp}}",
"thumbnail": "{{url_for('feed_get_cover', book_id=entry.id)}}", "thumbnail": "{{url_for('opds.feed_get_cover', book_id=entry.id)}}",
"main_format": { "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 %}, "rating":{% if entry.ratings.__len__() > 0 %} "{{entry.ratings[0].rating}}.0"{% else %}0.0{% endif %},
"authors": [ "authors": [
{% for author in entry.authors %} {% for author in entry.authors %}
"{{author.name.replace('|',',')}}"{% if not loop.last %},{% endif %} "{{author.name.replace('|',',')}}"{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
], ],
"other_formats": { "other_formats": {
{% if entry.data.__len__() > 1 %} {% if entry.data.__len__() > 1 %}
{% for format in entry.data[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 %} {% endfor %}
{% endif %} }, {% endif %} },
"title_sort": "{{entry.sort}}" "title_sort": "{{entry.sort}}"

@ -161,7 +161,7 @@
{% if g.user.is_authenticated or g.user.is_anonymous %} {% if g.user.is_authenticated or g.user.is_anonymous %}
<li class="nav-head hidden-xs public-shelves">{{_('Public Shelves')}}</li> <li class="nav-head hidden-xs public-shelves">{{_('Public Shelves')}}</li>
{% for shelf in g.public_shelfes %} {% for shelf in g.public_shelfes %}
<li><a href="{{url_for('web.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list public_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li> <li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list public_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
{% endfor %} {% endfor %}
<li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li> <li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %} {% for shelf in g.user.shelf %}

@ -6,9 +6,9 @@
<Developer>Janeczku</Developer> <Developer>Janeczku</Developer>
<Contact>https://github.com/janeczku/calibre-web</Contact> <Contact>https://github.com/janeczku/calibre-web</Contact>
<Url type="text/html" <Url type="text/html"
template="{{url_for('search')}}?query={searchTerms}"/> template="{{url_for('opds.search')}}?query={searchTerms}"/>
<Url type="application/atom+xml" <Url type="application/atom+xml"
template="{{url_for('feed_normal_search')}}?query={searchTerms}"/> template="{{url_for('opds.feed_normal_search')}}?query={searchTerms}"/>
<SyndicationRight>open</SyndicationRight> <SyndicationRight>open</SyndicationRight>
<Language>{{lang}}</Language> <Language>{{lang}}</Language>
<OutputEncoding>UTF-8</OutputEncoding> <OutputEncoding>UTF-8</OutputEncoding>

@ -23,7 +23,6 @@ from sqlalchemy import exc
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import * from sqlalchemy.orm import *
from flask_login import AnonymousUserMixin from flask_login import AnonymousUserMixin
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
import sys import sys
import os import os
import logging import logging
@ -34,8 +33,11 @@ from binascii import hexlify
import cli import cli
try: try:
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
import ldap import ldap
oauth_support = True
except ImportError: except ImportError:
oauth_support = False
pass pass
engine = create_engine('sqlite:///{0}'.format(cli.settingspath), echo=False) 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") default_language = Column(String(3), default="all")
mature_content = Column(Boolean, default=True) mature_content = Column(Boolean, default=True)
if oauth_support:
class OAuth(OAuthConsumerMixin, Base): class OAuth(OAuthConsumerMixin, Base):
provider_user_id = Column(String(256)) provider_user_id = Column(String(256))
user_id = Column(Integer, ForeignKey(User.id)) user_id = Column(Integer, ForeignKey(User.id))
user = relationship(User) user = relationship(User)
# Class for anonymous user is derived from User base and completly overrides methods and properties for the # 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() session.commit()
# Generate user Guest (translated text), as anoymous user, no rights # Generate user Guest (translated text), as anoymous user, no rights
def create_anonymous_user(session): def create_anonymous_user():
user = User() user = User()
user.nickname = "Guest" user.nickname = "Guest"
user.email = 'no@email' user.email = 'no@email'

@ -18,11 +18,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from cps import config, get_locale from cps import config, get_locale, Server, app
import threading import threading
import zipfile import zipfile
import requests import requests
import logging
import time import time
from io import BytesIO from io import BytesIO
import os import os
@ -35,7 +34,6 @@ import json
from flask_babel import gettext as _ from flask_babel import gettext as _
from babel.dates import format_datetime from babel.dates import format_datetime
import server
def is_sha1(sha1): def is_sha1(sha1):
if len(sha1) != 40: if len(sha1) != 40:
@ -69,39 +67,45 @@ class Updater(threading.Thread):
def run(self): def run(self):
try: try:
self.status = 1 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() r.raise_for_status()
self.status = 2 self.status = 2
app.logger.debug(u'Opening zipfile')
z = zipfile.ZipFile(BytesIO(r.content)) z = zipfile.ZipFile(BytesIO(r.content))
self.status = 3 self.status = 3
app.logger.debug(u'Extracting zipfile')
tmp_dir = gettempdir() tmp_dir = gettempdir()
z.extractall(tmp_dir) z.extractall(tmp_dir)
foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1] foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1]
if not os.path.isdir(foldername): if not os.path.isdir(foldername):
self.status = 11 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 return
self.status = 4 self.status = 4
app.logger.debug(u'Replacing files')
self.update_source(foldername, config.get_main_dir) self.update_source(foldername, config.get_main_dir)
self.status = 6 self.status = 6
app.logger.debug(u'Preparing restart of server')
time.sleep(2) time.sleep(2)
server.Server.setRestartTyp(True) Server.setRestartTyp(True)
server.Server.stopServer() Server.stopServer()
self.status = 7 self.status = 7
time.sleep(2) time.sleep(2)
except requests.exceptions.HTTPError as ex: 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 self.status = 8
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logging.getLogger('cps.web').info(u'Connection error') app.logger.info(u'Connection error')
self.status = 9 self.status = 9
except requests.exceptions.Timeout: 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 self.status = 10
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
self.status = 11 self.status = 11
logging.getLogger('cps.web').info(u'General error') app.logger.info(u'General error')
def get_update_status(self): def get_update_status(self):
return self.status return self.status
@ -149,14 +153,14 @@ class Updater(threading.Thread):
if sys.platform == "win32" or sys.platform == "darwin": if sys.platform == "win32" or sys.platform == "darwin":
change_permissions = False change_permissions = False
else: 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) new_permissions = os.stat(root_dst_dir)
# print new_permissions # print new_permissions
for src_dir, __, files in os.walk(root_src_dir): for src_dir, __, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1) dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir): if not os.path.exists(dst_dir):
os.makedirs(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: if change_permissions:
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid)) # 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) 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 os.path.exists(dst_file):
if change_permissions: if change_permissions:
permission = os.stat(dst_file) 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) os.remove(dst_file)
else: else:
if change_permissions: if change_permissions:
permission = new_permissions permission = new_permissions
shutil.move(src_file, dst_dir) 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: if change_permissions:
try: try:
os.chown(dst_file, permission.st_uid, permission.st_gid) os.chown(dst_file, permission.st_uid, permission.st_gid)
except (Exception) as e: except (Exception) as e:
# ex = sys.exc_info() # ex = sys.exc_info()
old_permissions = os.stat(dst_file) 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(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: '
+ str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e)) + str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e))
return return
@ -215,15 +219,15 @@ class Updater(threading.Thread):
for item in remove_items: for item in remove_items:
item_path = os.path.join(destination, item[1:]) item_path = os.path.join(destination, item[1:])
if os.path.isdir(item_path): 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) shutil.rmtree(item_path, ignore_errors=True)
else: else:
try: try:
logging.getLogger('cps.web').debug("Delete file " + item_path) app.logger.debug("Delete file " + item_path)
# log_from_thread("Delete file " + item_path) # log_from_thread("Delete file " + item_path)
os.remove(item_path) os.remove(item_path)
except Exception: 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) shutil.rmtree(source, ignore_errors=True)
def _nightly_version_info(self): def _nightly_version_info(self):
@ -263,7 +267,8 @@ class Updater(threading.Thread):
status['update'] = True status['update'] = True
try: 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() r.raise_for_status()
update_data = r.json() update_data = r.json()
except requests.exceptions.HTTPError as e: 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 # check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']: if parent_commit['sha'] != status['current_commit_hash']:
try: 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() r.raise_for_status()
parent_data = r.json() 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 # check if we are more than one update behind if so, go up the tree
if commit['sha'] != status['current_commit_hash']: if commit['sha'] != status['current_commit_hash']:
try: 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() r.raise_for_status()
parent_data = r.json() parent_data = r.json()
@ -492,7 +499,8 @@ class Updater(threading.Thread):
else: else:
status['current_commit_hash'] = version['version'] status['current_commit_hash'] = version['version']
try: try:
r = requests.get(repository_url) headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(repository_url, headers=headers)
commit = r.json() commit = r.json()
r.raise_for_status() r.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:

@ -47,18 +47,19 @@ from cps import lm, babel, ub, config, get_locale, language_table, app
from pagination import Pagination from pagination import Pagination
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text
from oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status feature_support = dict()
try:
'''try: from oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
oauth_support = True feature_support['oauth'] = True
except ImportError: except ImportError:
oauth_support = False''' feature_support['oauth'] = False
oauth_check = {}
try: try:
import ldap import ldap
ldap_support = True feature_support['ldap'] = True
except ImportError: except ImportError:
ldap_support = False feature_support['ldap'] = False
try: try:
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
@ -67,15 +68,15 @@ except ImportError:
try: try:
from goodreads.client import GoodreadsClient from goodreads.client import GoodreadsClient
goodreads_support = True feature_support['goodreads'] = True
except ImportError: except ImportError:
goodreads_support = False feature_support['goodreads'] = False
try: try:
import Levenshtein import Levenshtein
levenshtein_support = True feature_support['levenshtein'] = True
except ImportError: except ImportError:
levenshtein_support = False feature_support['levenshtein'] = False
try: try:
from functools import reduce, wraps from functools import reduce, wraps
@ -84,9 +85,9 @@ except ImportError:
try: try:
import rarfile import rarfile
rar_support=True feature_support['rar'] = True
except ImportError: except ImportError:
rar_support=False feature_support['rar'] = False
try: try:
from natsort import natsorted as sort from natsort import natsorted as sort
@ -95,18 +96,17 @@ except ImportError:
try: try:
from urllib.parse import quote from urllib.parse import quote
from imp import reload
except ImportError: except ImportError:
from urllib import quote from urllib import quote
from flask import Blueprint from flask import Blueprint
# Global variables # Global variables
EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'} 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 # custom error page
@ -346,8 +346,8 @@ def before_request():
g.allow_upload = config.config_uploading g.allow_upload = config.config_uploading
g.current_theme = config.config_theme 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() 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: if not config.db_configured and request.endpoint not in ('admin.basic_configuration', 'login') and '/static/' not in request.path:
return redirect(url_for('web.basic_configuration')) return redirect(url_for('admin.basic_configuration'))
@web.route("/ajax/emailstat") @web.route("/ajax/emailstat")
@ -373,7 +373,7 @@ def get_comic_book(book_id, book_format, page):
if bookformat.format.lower() == book_format.lower(): if bookformat.format.lower() == book_format.lower():
cbr_file = os.path.join(config.config_calibre_dir, book.path, bookformat.name) + "." + book_format cbr_file = os.path.join(config.config_calibre_dir, book.path, bookformat.name) + "." + book_format
if book_format in ("cbr", "rar"): if book_format in ("cbr", "rar"):
if rar_support == True: if feature_support['rar'] == True:
rarfile.UNRAR_TOOL = config.config_rarfile_location rarfile.UNRAR_TOOL = config.config_rarfile_location
try: try:
rf = rarfile.RarFile(cbr_file) rf = rarfile.RarFile(cbr_file)
@ -636,7 +636,7 @@ def author(book_id, page):
author_info = None author_info = None
other_books = [] other_books = []
if goodreads_support and config.config_use_goodreads: if feature_support['goodreads'] and config.config_use_goodreads:
try: try:
gc = GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret) gc = GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret)
author_info = gc.find_author(author_name=name) author_info = gc.find_author(author_name=name)
@ -688,7 +688,7 @@ def get_unique_other_books(library_books, author_books):
author_books) author_books)
# Fuzzy match book titles # Fuzzy match book titles
if levenshtein_support: if feature_support['levenshtein']:
library_titles = reduce(lambda acc, book: acc + [book.title], library_books, []) library_titles = reduce(lambda acc, book: acc + [book.title], library_books, [])
other_books = filter(lambda author_book: not filter( other_books = filter(lambda author_book: not filter(
lambda library_book: lambda library_book:
@ -1215,7 +1215,7 @@ def read_book(book_id, book_format):
# copyfile(cbr_file, tmp_file) # copyfile(cbr_file, tmp_file)
return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"),
extension=fileext) extension=fileext)
'''if rar_support == True: '''if feature_support['rar']:
extensionList = ["cbr","cbt","cbz"] extensionList = ["cbr","cbt","cbz"]
else: else:
extensionList = ["cbt","cbz"] extensionList = ["cbt","cbz"]
@ -1292,7 +1292,7 @@ def register():
try: try:
ub.session.add(content) ub.session.add(content)
ub.session.commit() ub.session.commit()
if oauth_support: if feature_support['oauth']:
register_user_with_oauth(content) register_user_with_oauth(content)
helper.send_registration_mail(to_save["email"], to_save["nickname"], password) helper.send_registration_mail(to_save["email"], to_save["nickname"], password)
except Exception: except Exception:
@ -1310,7 +1310,7 @@ def register():
flash(_(u"This username or e-mail address is already in use."), category="error") 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") return render_title_template('register.html', title=_(u"register"), page="register")
if oauth_support: if feature_support['oauth']:
register_user_with_oauth() register_user_with_oauth()
return render_title_template('register.html', config=config, title=_(u"register"), page="register") 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']) @web.route('/login', methods=['GET', 'POST'])
def login(): def login():
if not config.db_configured: 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: if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if request.method == "POST": if request.method == "POST":
@ -1358,7 +1358,7 @@ def login():
def logout(): def logout():
if current_user is not None and current_user.is_authenticated: if current_user is not None and current_user.is_authenticated:
logout_user() logout_user()
if oauth_support: if feature_support['oauth']:
logout_oauth_user() logout_oauth_user()
return redirect(url_for('web.login')) return redirect(url_for('web.login'))
@ -1370,7 +1370,7 @@ def remote_login():
ub.session.add(auth_token) ub.session.add(auth_token)
ub.session.commit() 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, return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
verify_url=verify_url, page="remotelogin") verify_url=verify_url, page="remotelogin")
@ -1385,7 +1385,7 @@ def verify_token(token):
# Token not found # Token not found
if auth_token is None: if auth_token is None:
flash(_(u"Token not found"), category="error") flash(_(u"Token not found"), category="error")
return redirect(url_for('index')) return redirect(url_for('web.index'))
# Token expired # Token expired
if datetime.datetime.now() > auth_token.expiration: if datetime.datetime.now() > auth_token.expiration:
@ -1393,7 +1393,7 @@ def verify_token(token):
ub.session.commit() ub.session.commit()
flash(_(u"Token has expired"), category="error") flash(_(u"Token has expired"), category="error")
return redirect(url_for('index')) return redirect(url_for('web.index'))
# Update token with user information # Update token with user information
auth_token.user_id = current_user.id auth_token.user_id = current_user.id
@ -1401,7 +1401,7 @@ def verify_token(token):
ub.session.commit() ub.session.commit()
flash(_(u"Success! Please return to your device"), category="success") 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']) @web.route('/ajax/verify_token', methods=['POST'])
@ -1472,7 +1472,10 @@ def profile():
downloads = list() downloads = list()
languages = speaking_language() languages = speaking_language()
translations = babel.list_translations() + [LC('en')] 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: for book in content.downloads:
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
if downloadBook: if downloadBook:
@ -1535,9 +1538,11 @@ def profile():
ub.session.rollback() ub.session.rollback()
flash(_(u"Found an existing account for this e-mail address."), category="error") flash(_(u"Found an existing account for this e-mail address."), category="error")
return render_title_template("user_edit.html", content=content, downloads=downloads, 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") flash(_(u"Profile updated"), category="success")
return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages, return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages,
content=content, downloads=downloads, title=_(u"%(name)s's profile", content=content, downloads=downloads, title=_(u"%(name)s's profile",
name=current_user.nickname), page="me", registered_oauth=oauth_check, oauth_status=oauth_status) name=current_user.nickname), page="me", registered_oauth=oauth_check,
oauth_status=oauth_status)

@ -27,14 +27,12 @@ import socket
import sys import sys
import os import os
from email.generator import Generator from email.generator import Generator
from cps import config, db # , app from cps import config, db, app
# import web
from flask_babel import gettext as _ from flask_babel import gettext as _
import re import re
# import gdriveutils as gd import gdriveutils as gd
from subproc_wrapper import process_open from subproc_wrapper import process_open
try: try:
from StringIO import StringIO from StringIO import StringIO
from email.MIMEBase import MIMEBase from email.MIMEBase import MIMEBase
@ -90,8 +88,8 @@ def get_attachment(bookpath, filename):
data = file_.read() data = file_.read()
file_.close() file_.close()
except IOError as e: except IOError as e:
# web.app.logger.exception(e) # traceback.print_exc() app.logger.exception(e) # traceback.print_exc()
# web.app.logger.error(u'The requested file could not be read. Maybe wrong permissions?') app.logger.error(u'The requested file could not be read. Maybe wrong permissions?')
return None return None
attachment = MIMEBase('application', 'octet-stream') attachment = MIMEBase('application', 'octet-stream')
@ -116,8 +114,7 @@ class emailbase():
def send(self, strg): def send(self, strg):
"""Send `strg' to the server.""" """Send `strg' to the server."""
if self.debuglevel > 0: app.logger.debug('send:' + repr(strg[:300]))
print('send:', repr(strg[:300]), file=sys.stderr)
if hasattr(self, 'sock') and self.sock: if hasattr(self, 'sock') and self.sock:
try: try:
if self.transferSize: if self.transferSize:
@ -141,6 +138,9 @@ class emailbase():
else: else:
raise smtplib.SMTPServerDisconnected('please run connect() first') raise smtplib.SMTPServerDisconnected('please run connect() first')
def _print_debug(self, *args):
app.logger.debug(args)
def getTransferStatus(self): def getTransferStatus(self):
if self.transferSize: if self.transferSize:
lock2 = threading.Lock() lock2 = threading.Lock()
@ -254,14 +254,14 @@ class WorkerThread(threading.Thread):
# if it does - mark the conversion task as complete and return a success # if it does - mark the conversion task as complete and return a success
# this will allow send to kindle workflow to continue to work # this will allow send to kindle workflow to continue to work
if os.path.isfile(file_path + format_new_ext): 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() cur_book = db.session.query(db.Books).filter(db.Books.id == bookid).first()
self.queue[self.current]['path'] = file_path self.queue[self.current]['path'] = file_path
self.queue[self.current]['title'] = cur_book.title self.queue[self.current]['title'] = cur_book.title
self._handleSuccess() self._handleSuccess()
return file_path + format_new_ext return file_path + format_new_ext
else: 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 # check if converter-executable is existing
if not os.path.exists(config.config_converterpath): 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 format_old_ext == '.epub' and format_new_ext == '.mobi':
if config.config_ebookconverter == 1: if config.config_ebookconverter == 1:
'''if os.name == 'nt': '''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): if sys.version_info < (3, 0):
command = command.encode(sys.getfilesystemencoding()) command = command.encode(sys.getfilesystemencoding())
else:''' else:'''
command = [config.config_converterpath, file_path + u'.epub'] command = [config.config_converterpath, file_path + u'.epub']
quotes = (1) quotes = [1]
if config.config_ebookconverter == 2: if config.config_ebookconverter == 2:
# Linux py2.7 encode as list without quotes no empty element for parameters # 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 # 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 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 # windows py 3.x no encode and as string with quotes empty element for parameters is okay
# separate handling for windows and linux # separate handling for windows and linux
quotes = (1,2) quotes = [1,2]
'''if os.name == 'nt': '''if os.name == 'nt':
command = web.ub.config.config_converterpath + u' "' + file_path + format_old_ext + u'" "' + \ command = config.config_converterpath + u' "' + file_path + format_old_ext + u'" "' + \
file_path + format_new_ext + u'" ' + web.ub.config.config_calibre file_path + format_new_ext + u'" ' + config.config_calibre
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
command = command.encode(sys.getfilesystemencoding()) command = command.encode(sys.getfilesystemencoding())
else:''' else:'''
@ -317,13 +317,13 @@ class WorkerThread(threading.Thread):
if conv_error: if conv_error:
error_message = _(u"Kindlegen failed with Error %(error)s. Message: %(message)s", error_message = _(u"Kindlegen failed with Error %(error)s. Message: %(message)s",
error=conv_error.group(1), message=conv_error.group(2).strip()) 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: else:
while p.poll() is None: while p.poll() is None:
nextline = p.stdout.readline() nextline = p.stdout.readline()
if os.name == 'nt' and sys.version_info < (3, 0): if os.name == 'nt' and sys.version_info < (3, 0):
nextline = nextline.decode('windows-1252') 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 # parse progress string from calibre-converter
progress = re.search("(\d+)%\s.*", nextline) progress = re.search("(\d+)%\s.*", nextline)
if progress: if progress:
@ -353,7 +353,7 @@ class WorkerThread(threading.Thread):
return file_path + format_new_ext return file_path + format_new_ext
else: else:
error_message = format_new_ext.upper() + ' format not found on disk' 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: if not error_message:
error_message = 'Ebook converter failed with unknown error' error_message = 'Ebook converter failed with unknown error'
self._handleError(error_message) self._handleError(error_message)
@ -414,7 +414,6 @@ class WorkerThread(threading.Thread):
def _send_raw_email(self): def _send_raw_email(self):
self.queue[self.current]['starttime'] = datetime.now() self.queue[self.current]['starttime'] = datetime.now()
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime'] self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
# self.queue[self.current]['status'] = STAT_STARTED
self.UIqueue[self.current]['stat'] = STAT_STARTED self.UIqueue[self.current]['stat'] = STAT_STARTED
obj=self.queue[self.current] obj=self.queue[self.current]
# create MIME message # create MIME message
@ -446,8 +445,11 @@ class WorkerThread(threading.Thread):
# send email # send email
timeout = 600 # set timeout to 5mins timeout = 600 # set timeout to 5mins
org_stderr = sys.stderr # redirect output to logfile on python2 pn python3 debugoutput is caught with overwritten
sys.stderr = StderrLogger() # _print_debug function
if sys.version_info < (3, 0):
org_smtpstderr = smtplib.stderr
smtplib.stderr = StderrLogger()
if use_ssl == 2: if use_ssl == 2:
self.asyncSMTP = email_SSL(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout) 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) self.asyncSMTP = email(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout)
# link to logginglevel # link to logginglevel
if web.ub.config.config_log_level != logging.DEBUG: if config.config_log_level != logging.DEBUG:
self.asyncSMTP.set_debuglevel(0) self.asyncSMTP.set_debuglevel(0)
else: else:
self.asyncSMTP.set_debuglevel(1) 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.sendmail(obj['settings']["mail_from"], obj['recipent'], msg)
self.asyncSMTP.quit() self.asyncSMTP.quit()
self._handleSuccess() self._handleSuccess()
sys.stderr = org_stderr
if sys.version_info < (3, 0):
smtplib.stderr = org_smtpstderr
except (MemoryError) as e: except (MemoryError) as e:
self._handleError(u'Error sending email: ' + e.message) self._handleError(u'Error sending email: ' + e.message)
@ -497,7 +501,7 @@ class WorkerThread(threading.Thread):
return retVal return retVal
def _handleError(self, error_message): def _handleError(self, error_message):
web.app.logger.error(error_message) app.logger.error(error_message)
# self.queue[self.current]['status'] = STAT_FAIL # self.queue[self.current]['status'] = STAT_FAIL
self.UIqueue[self.current]['stat'] = STAT_FAIL self.UIqueue[self.current]['stat'] = STAT_FAIL
self.UIqueue[self.current]['progress'] = "100 %" self.UIqueue[self.current]['progress'] = "100 %"
@ -519,13 +523,12 @@ class StderrLogger(object):
buffer = '' buffer = ''
def __init__(self): def __init__(self):
self.logger = web.app.logger self.logger = app.logger
def write(self, message): def write(self, message):
try: try:
if message == '\n': if message == '\n':
self.logger.debug(self.buffer) self.logger.debug(self.buffer.replace("\n","\\n"))
print(self.buffer)
self.buffer = '' self.buffer = ''
else: else:
self.buffer += message self.buffer += message

@ -11,12 +11,19 @@ PyDrive==1.3.1
PyYAML==3.12 PyYAML==3.12
rsa==3.4.2 rsa==3.4.2
six==1.10.0 six==1.10.0
# goodreads # goodreads
goodreads>=0.3.2 goodreads>=0.3.2
python-Levenshtein>=0.12.0 python-Levenshtein>=0.12.0
# ldap login # ldap login
python_ldap>=3.0.0 python_ldap>=3.0.0
# other # other
lxml>=3.8.0 lxml>=3.8.0
rarfile>=2.7 rarfile>=2.7
natsort>=2.2.0 natsort>=2.2.0
# Oauth Login
flask-dance>=0.13.0
sqlalchemy_utils>=0.33.5

@ -13,5 +13,3 @@ SQLAlchemy>=1.1.0
tornado>=4.1 tornado>=4.1
Wand>=0.4.4 Wand>=0.4.4
unidecode>=0.04.19 unidecode>=0.04.19
flask-dance>=0.13.0
sqlalchemy_utils>=0.33.5

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save