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 5 years ago
parent 1dc6f44828
commit 4230226716

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

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

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

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

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

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

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

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

@ -19,7 +19,7 @@
<div class="text-center more-stuff"><h4>{{_('Delete formats:')}}</h4>
{% for file in book.data %}
<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>
{% endfor %}
</div>
@ -28,7 +28,7 @@
{% if source_formats|length > 0 and conversion_formats|length > 0 %}
<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="text-left">
<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 %}>
<label for="config_remote_login">{{_('Enable remote login ("magic link")')}}</label>
</div>
{% if goodreads %}
{% if feature_support['goodreads'] %}
<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 %}>
<label for="config_use_goodreads">{{_('Use')}} Goodreads</label>
@ -176,6 +176,7 @@
</div>
</div>
{% endif %}
{% if feature_support['ldap'] %}
<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 %}>
<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">
</div>
</div>
{% endif %}
{% if feature_support['oauth'] %}
<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 %}>
<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">
</div>
</div>
{% endif %}
</div>
</div>
</div>

@ -57,9 +57,22 @@
</div>
{% 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 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">
<span class="glyphicon glyphicon-music"></span> {{_('Listen in browser')}}
<span class="caret"></span>
@ -77,7 +90,7 @@
{% endif %}
{% endfor %}
</ul>
</div>
</div-->
{% endif %}
{% endif %}

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

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

@ -161,7 +161,7 @@
{% if g.user.is_authenticated or g.user.is_anonymous %}
<li class="nav-head hidden-xs public-shelves">{{_('Public Shelves')}}</li>
{% 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 %}
<li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %}

@ -6,9 +6,9 @@
<Developer>Janeczku</Developer>
<Contact>https://github.com/janeczku/calibre-web</Contact>
<Url type="text/html"
template="{{url_for('search')}}?query={searchTerms}"/>
template="{{url_for('opds.search')}}?query={searchTerms}"/>
<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>
<Language>{{lang}}</Language>
<OutputEncoding>UTF-8</OutputEncoding>

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

@ -18,11 +18,10 @@
# 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 zipfile
import requests
import logging
import time
from io import BytesIO
import os
@ -35,7 +34,6 @@ import json
from flask_babel import gettext as _
from babel.dates import format_datetime
import server
def is_sha1(sha1):
if len(sha1) != 40:
@ -69,39 +67,45 @@ class Updater(threading.Thread):
def run(self):
try:
self.status = 1
r = requests.get(self._get_request_path(), stream=True)
app.logger.debug(u'Download update file')
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(self._get_request_path(), stream=True, headers=headers)
r.raise_for_status()
self.status = 2
app.logger.debug(u'Opening zipfile')
z = zipfile.ZipFile(BytesIO(r.content))
self.status = 3
app.logger.debug(u'Extracting zipfile')
tmp_dir = gettempdir()
z.extractall(tmp_dir)
foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1]
if not os.path.isdir(foldername):
self.status = 11
logging.getLogger('cps.web').info(u'Extracted contents of zipfile not found in temp folder')
app.logger.info(u'Extracted contents of zipfile not found in temp folder')
return
self.status = 4
app.logger.debug(u'Replacing files')
self.update_source(foldername, config.get_main_dir)
self.status = 6
app.logger.debug(u'Preparing restart of server')
time.sleep(2)
server.Server.setRestartTyp(True)
server.Server.stopServer()
Server.setRestartTyp(True)
Server.stopServer()
self.status = 7
time.sleep(2)
except requests.exceptions.HTTPError as ex:
logging.getLogger('cps.web').info( u'HTTP Error' + ' ' + str(ex))
app.logger.info( u'HTTP Error' + ' ' + str(ex))
self.status = 8
except requests.exceptions.ConnectionError:
logging.getLogger('cps.web').info(u'Connection error')
app.logger.info(u'Connection error')
self.status = 9
except requests.exceptions.Timeout:
logging.getLogger('cps.web').info(u'Timeout while establishing connection')
app.logger.info(u'Timeout while establishing connection')
self.status = 10
except requests.exceptions.RequestException:
self.status = 11
logging.getLogger('cps.web').info(u'General error')
app.logger.info(u'General error')
def get_update_status(self):
return self.status
@ -149,14 +153,14 @@ class Updater(threading.Thread):
if sys.platform == "win32" or sys.platform == "darwin":
change_permissions = False
else:
logging.getLogger('cps.web').debug('Update on OS-System : ' + sys.platform)
app.logger.debug('Update on OS-System : ' + sys.platform)
new_permissions = os.stat(root_dst_dir)
# print new_permissions
for src_dir, __, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
logging.getLogger('cps.web').debug('Create-Dir: '+dst_dir)
app.logger.debug('Create-Dir: '+dst_dir)
if change_permissions:
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
@ -166,20 +170,20 @@ class Updater(threading.Thread):
if os.path.exists(dst_file):
if change_permissions:
permission = os.stat(dst_file)
logging.getLogger('cps.web').debug('Remove file before copy: '+dst_file)
app.logger.debug('Remove file before copy: '+dst_file)
os.remove(dst_file)
else:
if change_permissions:
permission = new_permissions
shutil.move(src_file, dst_dir)
logging.getLogger('cps.web').debug('Move File '+src_file+' to '+dst_dir)
app.logger.debug('Move File '+src_file+' to '+dst_dir)
if change_permissions:
try:
os.chown(dst_file, permission.st_uid, permission.st_gid)
except (Exception) as e:
# ex = sys.exc_info()
old_permissions = os.stat(dst_file)
logging.getLogger('cps.web').debug('Fail change permissions of ' + str(dst_file) + '. Before: '
app.logger.debug('Fail change permissions of ' + str(dst_file) + '. Before: '
+ str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: '
+ str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e))
return
@ -215,15 +219,15 @@ class Updater(threading.Thread):
for item in remove_items:
item_path = os.path.join(destination, item[1:])
if os.path.isdir(item_path):
logging.getLogger('cps.web').debug("Delete dir " + item_path)
app.logger.debug("Delete dir " + item_path)
shutil.rmtree(item_path, ignore_errors=True)
else:
try:
logging.getLogger('cps.web').debug("Delete file " + item_path)
app.logger.debug("Delete file " + item_path)
# log_from_thread("Delete file " + item_path)
os.remove(item_path)
except Exception:
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
app.logger.debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True)
def _nightly_version_info(self):
@ -263,7 +267,8 @@ class Updater(threading.Thread):
status['update'] = True
try:
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'])
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'], headers=headers)
r.raise_for_status()
update_data = r.json()
except requests.exceptions.HTTPError as e:
@ -310,7 +315,8 @@ class Updater(threading.Thread):
# check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']:
try:
r = requests.get(parent_commit['url'])
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(parent_commit['url'], headers=headers)
r.raise_for_status()
parent_data = r.json()
@ -368,7 +374,8 @@ class Updater(threading.Thread):
# check if we are more than one update behind if so, go up the tree
if commit['sha'] != status['current_commit_hash']:
try:
r = requests.get(parent_commit['url'])
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(parent_commit['url'], headers=headers)
r.raise_for_status()
parent_data = r.json()
@ -492,7 +499,8 @@ class Updater(threading.Thread):
else:
status['current_commit_hash'] = version['version']
try:
r = requests.get(repository_url)
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(repository_url, headers=headers)
commit = r.json()
r.raise_for_status()
except requests.exceptions.HTTPError as e:

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

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

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

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

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