|
|
|
@ -28,6 +28,7 @@ import json
|
|
|
|
|
import mimetypes
|
|
|
|
|
import traceback
|
|
|
|
|
import binascii
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
from babel import Locale as LC
|
|
|
|
|
from babel.dates import format_date
|
|
|
|
@ -53,13 +54,14 @@ from .pagination import Pagination
|
|
|
|
|
from .redirect import redirect_back
|
|
|
|
|
|
|
|
|
|
feature_support = {
|
|
|
|
|
'ldap': False, # bool(services.ldap),
|
|
|
|
|
'ldap': bool(services.ldap),
|
|
|
|
|
'goodreads': bool(services.goodreads_support),
|
|
|
|
|
'kobo': bool(services.kobo)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
|
|
|
|
|
|
|
|
|
|
feature_support['oauth'] = True
|
|
|
|
|
except ImportError:
|
|
|
|
|
feature_support['oauth'] = False
|
|
|
|
@ -109,14 +111,15 @@ for ex in default_exceptions:
|
|
|
|
|
elif ex == 500:
|
|
|
|
|
app.register_error_handler(ex, internal_error)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web = Blueprint('web', __name__)
|
|
|
|
|
log = logger.create()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ################################### Login logic and rights management ###############################################
|
|
|
|
|
def _fetch_user_by_name(username):
|
|
|
|
|
return ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@lm.user_loader
|
|
|
|
|
def load_user(user_id):
|
|
|
|
|
return ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
|
|
|
@ -164,6 +167,7 @@ def login_required_if_no_ano(func):
|
|
|
|
|
if config.config_anonbrowse == 1:
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
return login_required(func)(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
return decorated_view
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -256,7 +260,8 @@ def edit_required(f):
|
|
|
|
|
# Returns the template for rendering and includes the instance name
|
|
|
|
|
def render_title_template(*args, **kwargs):
|
|
|
|
|
sidebar = ub.get_sidebar_config(kwargs)
|
|
|
|
|
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, accept=constants.EXTENSIONS_UPLOAD,
|
|
|
|
|
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
|
|
|
|
|
accept=constants.EXTENSIONS_UPLOAD,
|
|
|
|
|
*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -268,11 +273,81 @@ def before_request():
|
|
|
|
|
g.allow_upload = config.config_uploading
|
|
|
|
|
g.current_theme = config.config_theme
|
|
|
|
|
g.config_authors_max = config.config_authors_max
|
|
|
|
|
g.shelves_access = ub.session.query(ub.Shelf).filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
|
|
|
|
if not config.db_configured and request.endpoint not in ('admin.basic_configuration', 'login') and '/static/' not in request.path:
|
|
|
|
|
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
|
|
|
|
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
|
|
|
|
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'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/import_ldap_users')
|
|
|
|
|
def import_ldap_users():
|
|
|
|
|
showtext = {}
|
|
|
|
|
try:
|
|
|
|
|
new_users = services.ldap.get_group_members(config.config_ldap_group_name)
|
|
|
|
|
except (services.ldap.LDAPException, TypeError, AttributeError) as e:
|
|
|
|
|
log.debug(e)
|
|
|
|
|
showtext['text'] = _(u'Error: %(ldaperror)s', ldaperror=e)
|
|
|
|
|
return json.dumps(showtext)
|
|
|
|
|
if not new_users:
|
|
|
|
|
log.debug('LDAP empty response')
|
|
|
|
|
showtext['text'] = _(u'Error: No user returned in response of LDAP server')
|
|
|
|
|
return json.dumps(showtext)
|
|
|
|
|
|
|
|
|
|
for username in new_users:
|
|
|
|
|
user = username.decode('utf-8')
|
|
|
|
|
if '=' in user:
|
|
|
|
|
match = re.search("([a-zA-Z0-9-]+)=%s", config.config_ldap_user_object, re.IGNORECASE | re.UNICODE)
|
|
|
|
|
if match:
|
|
|
|
|
match_filter = match.group(1)
|
|
|
|
|
match = re.search(match_filter + "=([[\d\w-]+)", user, re.IGNORECASE | re.UNICODE)
|
|
|
|
|
if match:
|
|
|
|
|
user = match.group(1)
|
|
|
|
|
else:
|
|
|
|
|
log.warning("Could Not Parse LDAP User: %s", user)
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
log.warning("Could Not Parse LDAP User: %s", user)
|
|
|
|
|
continue
|
|
|
|
|
if ub.session.query(ub.User).filter(ub.User.nickname == user.lower()).first():
|
|
|
|
|
log.warning("LDAP User: %s Already in Database", user)
|
|
|
|
|
continue
|
|
|
|
|
user_data = services.ldap.get_object_details(user=user,
|
|
|
|
|
group=None,
|
|
|
|
|
query_filter=None,
|
|
|
|
|
dn_only=False)
|
|
|
|
|
if user_data:
|
|
|
|
|
content = ub.User()
|
|
|
|
|
content.nickname = user
|
|
|
|
|
content.password = '' # dummy password which will be replaced by ldap one
|
|
|
|
|
if 'mail' in user_data:
|
|
|
|
|
content.email = user_data['mail'][0].decode('utf-8')
|
|
|
|
|
if (len(user_data['mail']) > 1):
|
|
|
|
|
content.kindle_mail = user_data['mail'][1].decode('utf-8')
|
|
|
|
|
else:
|
|
|
|
|
log.debug('No Mail Field Found in LDAP Response')
|
|
|
|
|
content.email = user + '@email.com'
|
|
|
|
|
content.role = config.config_default_role
|
|
|
|
|
content.sidebar_view = config.config_default_show
|
|
|
|
|
content.allowed_tags = config.config_allowed_tags
|
|
|
|
|
content.denied_tags = config.config_denied_tags
|
|
|
|
|
content.allowed_column_value = config.config_allowed_column_value
|
|
|
|
|
content.denied_column_value = config.config_denied_column_value
|
|
|
|
|
ub.session.add(content)
|
|
|
|
|
try:
|
|
|
|
|
ub.session.commit()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.warning("Failed to create LDAP user: %s - %s", user, e)
|
|
|
|
|
ub.session.rollback()
|
|
|
|
|
showtext['text'] = _(u'Failed to Create at Least One LDAP User')
|
|
|
|
|
else:
|
|
|
|
|
log.warning("LDAP User: %s Not Found", user)
|
|
|
|
|
showtext['text'] = _(u'At Least One LDAP User Not Found in Database')
|
|
|
|
|
if not showtext:
|
|
|
|
|
showtext['text'] = _(u'User Successfully Imported')
|
|
|
|
|
return json.dumps(showtext)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ################################### data provider functions #########################################################
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -418,39 +493,34 @@ def get_comic_book(book_id, book_format, page):
|
|
|
|
|
# ################################### Typeahead ##################################################################
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@web.route("/get_authors_json")
|
|
|
|
|
@web.route("/get_authors_json", methods=['GET'])
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def get_authors_json():
|
|
|
|
|
if request.method == "GET":
|
|
|
|
|
return get_typeahead(db.Authors, request.args.get('q'), ('|', ','))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@web.route("/get_publishers_json")
|
|
|
|
|
@web.route("/get_publishers_json", methods=['GET'])
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def get_publishers_json():
|
|
|
|
|
if request.method == "GET":
|
|
|
|
|
return get_typeahead(db.Publishers, request.args.get('q'), ('|', ','))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@web.route("/get_tags_json")
|
|
|
|
|
@web.route("/get_tags_json", methods=['GET'])
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def get_tags_json():
|
|
|
|
|
if request.method == "GET":
|
|
|
|
|
return get_typeahead(db.Tags, request.args.get('q'), tag_filter=tags_filters())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@web.route("/get_series_json")
|
|
|
|
|
@web.route("/get_series_json", methods=['GET'])
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def get_series_json():
|
|
|
|
|
if request.method == "GET":
|
|
|
|
|
return get_typeahead(db.Series, request.args.get('q'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@web.route("/get_languages_json", methods=['GET', 'POST'])
|
|
|
|
|
@web.route("/get_languages_json", methods=['GET'])
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def get_languages_json():
|
|
|
|
|
if request.method == "GET":
|
|
|
|
|
query = request.args.get('q').lower()
|
|
|
|
|
query = (request.args.get('q') or '').lower()
|
|
|
|
|
language_names = isoLanguages.get_language_names(get_locale())
|
|
|
|
|
entries_start = [s for key, s in language_names.items() if s.lower().startswith(query.lower())]
|
|
|
|
|
if len(entries_start) < 5:
|
|
|
|
@ -461,19 +531,18 @@ def get_languages_json():
|
|
|
|
|
return json_dumps
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@web.route("/get_matching_tags", methods=['GET', 'POST'])
|
|
|
|
|
@web.route("/get_matching_tags", methods=['GET'])
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def get_matching_tags():
|
|
|
|
|
tag_dict = {'tags': []}
|
|
|
|
|
if request.method == "GET":
|
|
|
|
|
q = db.session.query(db.Books)
|
|
|
|
|
db.session.connection().connection.connection.create_function("lower", 1, lcase)
|
|
|
|
|
author_input = request.args.get('author_name')
|
|
|
|
|
title_input = request.args.get('book_title')
|
|
|
|
|
include_tag_inputs = request.args.getlist('include_tag')
|
|
|
|
|
exclude_tag_inputs = request.args.getlist('exclude_tag')
|
|
|
|
|
include_extension_inputs = request.args.getlist('include_extension')
|
|
|
|
|
exclude_extension_inputs = request.args.getlist('exclude_extension')
|
|
|
|
|
author_input = request.args.get('author_name') or ''
|
|
|
|
|
title_input = request.args.get('book_title') or ''
|
|
|
|
|
include_tag_inputs = request.args.getlist('include_tag') or ''
|
|
|
|
|
exclude_tag_inputs = request.args.getlist('exclude_tag') or ''
|
|
|
|
|
# include_extension_inputs = request.args.getlist('include_extension') or ''
|
|
|
|
|
# exclude_extension_inputs = request.args.getlist('exclude_extension') or ''
|
|
|
|
|
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")),
|
|
|
|
|
func.lower(db.Books.title).ilike("%" + title_input + "%"))
|
|
|
|
|
if len(include_tag_inputs) > 0:
|
|
|
|
@ -595,13 +664,13 @@ def render_hot_books(page):
|
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_author_books(page, author_id, order):
|
|
|
|
|
entries, __, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == author_id),
|
|
|
|
|
[order[0], db.Series.name, db.Books.series_index],
|
|
|
|
|
db.books_series_link, db.Series)
|
|
|
|
|
if entries is None or not len(entries):
|
|
|
|
|
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
|
|
|
|
|
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
|
|
|
|
|
category="error")
|
|
|
|
|
return redirect(url_for("web.index"))
|
|
|
|
|
|
|
|
|
|
author = db.session.query(db.Authors).get(author_id)
|
|
|
|
@ -656,7 +725,8 @@ def render_ratings_books(page, book_id, order):
|
|
|
|
|
def render_formats_books(page, book_id, order):
|
|
|
|
|
name = db.session.query(db.Data).filter(db.Data.format == book_id.upper()).first()
|
|
|
|
|
if name:
|
|
|
|
|
entries, random, pagination = fill_indexpage(page, db.Books, db.Books.data.any(db.Data.format == book_id.upper()),
|
|
|
|
|
entries, random, pagination = fill_indexpage(page, db.Books,
|
|
|
|
|
db.Books.data.any(db.Data.format == book_id.upper()),
|
|
|
|
|
[db.Books.timestamp.desc(), order[0]])
|
|
|
|
|
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
|
|
|
|
|
title=_(u"File format: %(format)s", format=name.format), page="formats")
|
|
|
|
@ -832,11 +902,13 @@ def reconnect():
|
|
|
|
|
db.reconnect_db(config)
|
|
|
|
|
return json.dumps({})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@web.route("/search", methods=["GET"])
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def search():
|
|
|
|
|
term = request.args.get("query").strip().lower()
|
|
|
|
|
term = request.args.get("query")
|
|
|
|
|
if term:
|
|
|
|
|
term.strip().lower()
|
|
|
|
|
entries = get_search_results(term)
|
|
|
|
|
ids = list()
|
|
|
|
|
for element in entries:
|
|
|
|
@ -1083,6 +1155,7 @@ def serve_book(book_id, book_format, anyname):
|
|
|
|
|
else:
|
|
|
|
|
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
|
|
|
|
|
@web.route("/download/<int:book_id>/<book_format>/<anyname>")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
@ -1186,17 +1259,25 @@ def login():
|
|
|
|
|
form = request.form.to_dict()
|
|
|
|
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == form['username'].strip().lower()) \
|
|
|
|
|
.first()
|
|
|
|
|
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user:
|
|
|
|
|
login_result = services.ldap.bind_user(form['username'], form['password'])
|
|
|
|
|
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "":
|
|
|
|
|
login_result, error = services.ldap.bind_user(form['username'], form['password'])
|
|
|
|
|
if login_result:
|
|
|
|
|
login_user(user, remember=True)
|
|
|
|
|
log.debug(u"You are now logged in as: '%s'", user.nickname)
|
|
|
|
|
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname),
|
|
|
|
|
category="success")
|
|
|
|
|
return redirect_back(url_for("web.index"))
|
|
|
|
|
if login_result is None:
|
|
|
|
|
log.error('Could not login. LDAP server down, please contact your administrator')
|
|
|
|
|
flash(_(u"Could not login. LDAP server down, please contact your administrator"), category="error")
|
|
|
|
|
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
|
|
|
|
|
and user.nickname != "Guest":
|
|
|
|
|
login_user(user, remember=True)
|
|
|
|
|
log.info("Local Fallback Login as: '%s'", user.nickname)
|
|
|
|
|
flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known",
|
|
|
|
|
nickname=user.nickname),
|
|
|
|
|
category="warning")
|
|
|
|
|
return redirect_back(url_for("web.index"))
|
|
|
|
|
elif login_result is None:
|
|
|
|
|
log.info(error)
|
|
|
|
|
flash(_(u"Could not login: %(message)s", message=error), category="error")
|
|
|
|
|
else:
|
|
|
|
|
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
|
|
|
|
|
log.info('LDAP Login failed for user "%s" IP-adress: %s', form['username'], ipAdress)
|
|
|
|
@ -1210,7 +1291,7 @@ def login():
|
|
|
|
|
flash(_(u"New Password was send to your email address"), category="info")
|
|
|
|
|
log.info('Password reset for user "%s" IP-adress: %s', form['username'], ipAdress)
|
|
|
|
|
else:
|
|
|
|
|
log.info(u"An unknown error occurred. Please try again later.")
|
|
|
|
|
log.info(u"An unknown error occurred. Please try again later")
|
|
|
|
|
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
|
|
|
|
else:
|
|
|
|
|
flash(_(u"Please enter valid username to reset password"), category="error")
|
|
|
|
@ -1220,13 +1301,23 @@ def login():
|
|
|
|
|
login_user(user, remember=True)
|
|
|
|
|
log.debug(u"You are now logged in as: '%s'", user.nickname)
|
|
|
|
|
flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
|
|
|
|
|
config.config_is_initial = False
|
|
|
|
|
return redirect_back(url_for("web.index"))
|
|
|
|
|
else:
|
|
|
|
|
log.info('Login failed for user "%s" IP-adress: %s', form['username'], ipAdress)
|
|
|
|
|
flash(_(u"Wrong Username or Password"), category="error")
|
|
|
|
|
|
|
|
|
|
if feature_support['oauth']:
|
|
|
|
|
oauth_status = get_oauth_status()
|
|
|
|
|
else:
|
|
|
|
|
oauth_status = None
|
|
|
|
|
next_url = url_for('web.index')
|
|
|
|
|
return render_title_template('login.html', title=_(u"login"), next_url=next_url, config=config,
|
|
|
|
|
return render_title_template('login.html',
|
|
|
|
|
title=_(u"login"),
|
|
|
|
|
next_url=next_url,
|
|
|
|
|
config=config,
|
|
|
|
|
# oauth_status=oauth_status,
|
|
|
|
|
oauth_check=oauth_check,
|
|
|
|
|
mail=config.get_mail_server_configured(), page="login")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|