mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-24 18:47:23 +00:00
Merge branch 'Develop'
# Conflicts: # cps/db.py # cps/templates/user_edit.html
This commit is contained in:
commit
146068c936
11
cps.py
11
cps.py
@ -41,6 +41,14 @@ from cps.shelf import shelf
|
|||||||
from cps.admin import admi
|
from cps.admin import admi
|
||||||
from cps.gdrive import gdrive
|
from cps.gdrive import gdrive
|
||||||
from cps.editbooks import editbook
|
from cps.editbooks import editbook
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cps.kobo import kobo, get_kobo_activated
|
||||||
|
from cps.kobo_auth import kobo_auth
|
||||||
|
kobo_available = get_kobo_activated()
|
||||||
|
except ImportError:
|
||||||
|
kobo_available = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cps.oauth_bb import oauth
|
from cps.oauth_bb import oauth
|
||||||
oauth_available = True
|
oauth_available = True
|
||||||
@ -58,6 +66,9 @@ def main():
|
|||||||
app.register_blueprint(admi)
|
app.register_blueprint(admi)
|
||||||
app.register_blueprint(gdrive)
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
|
if kobo_available:
|
||||||
|
app.register_blueprint(kobo)
|
||||||
|
app.register_blueprint(kobo_auth)
|
||||||
if oauth_available:
|
if oauth_available:
|
||||||
app.register_blueprint(oauth)
|
app.register_blueprint(oauth)
|
||||||
success = web_server.start()
|
success = web_server.start()
|
||||||
|
@ -70,7 +70,7 @@ _VERSIONS = OrderedDict(
|
|||||||
Unidecode = unidecode_version,
|
Unidecode = unidecode_version,
|
||||||
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else u'not installed',
|
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else u'not installed',
|
||||||
Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed',
|
Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed',
|
||||||
|
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else u'not installed',
|
||||||
)
|
)
|
||||||
_VERSIONS.update(uploader.get_versions())
|
_VERSIONS.update(uploader.get_versions())
|
||||||
|
|
||||||
|
282
cps/admin.py
282
cps/admin.py
@ -44,7 +44,8 @@ from .web import admin_required, render_title_template, before_request, unconfig
|
|||||||
|
|
||||||
feature_support = {
|
feature_support = {
|
||||||
'ldap': False, # bool(services.ldap),
|
'ldap': False, # bool(services.ldap),
|
||||||
'goodreads': bool(services.goodreads_support)
|
'goodreads': bool(services.goodreads_support),
|
||||||
|
'kobo': bool(services.kobo)
|
||||||
}
|
}
|
||||||
|
|
||||||
# try:
|
# try:
|
||||||
@ -143,7 +144,10 @@ def configuration():
|
|||||||
def view_configuration():
|
def view_configuration():
|
||||||
readColumn = db.session.query(db.Custom_Columns)\
|
readColumn = db.session.query(db.Custom_Columns)\
|
||||||
.filter(and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all()
|
.filter(and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all()
|
||||||
|
restrictColumns= db.session.query(db.Custom_Columns)\
|
||||||
|
.filter(and_(db.Custom_Columns.datatype == 'text',db.Custom_Columns.mark_for_delete == 0)).all()
|
||||||
return render_title_template("config_view_edit.html", conf=config, readColumns=readColumn,
|
return render_title_template("config_view_edit.html", conf=config, readColumns=readColumn,
|
||||||
|
restrictColumns=restrictColumns,
|
||||||
title=_(u"UI Configuration"), page="uiconfig")
|
title=_(u"UI Configuration"), page="uiconfig")
|
||||||
|
|
||||||
|
|
||||||
@ -159,7 +163,7 @@ def update_view_configuration():
|
|||||||
|
|
||||||
_config_string("config_calibre_web_title")
|
_config_string("config_calibre_web_title")
|
||||||
_config_string("config_columns_to_ignore")
|
_config_string("config_columns_to_ignore")
|
||||||
_config_string("config_mature_content_tags")
|
# _config_string("config_mature_content_tags")
|
||||||
reboot_required |= _config_string("config_title_regex")
|
reboot_required |= _config_string("config_title_regex")
|
||||||
|
|
||||||
_config_int("config_read_column")
|
_config_int("config_read_column")
|
||||||
@ -167,6 +171,7 @@ def update_view_configuration():
|
|||||||
_config_int("config_random_books")
|
_config_int("config_random_books")
|
||||||
_config_int("config_books_per_page")
|
_config_int("config_books_per_page")
|
||||||
_config_int("config_authors_max")
|
_config_int("config_authors_max")
|
||||||
|
_config_int("config_restricted_column")
|
||||||
|
|
||||||
if config.config_google_drive_watch_changes_response:
|
if config.config_google_drive_watch_changes_response:
|
||||||
config.config_google_drive_watch_changes_response = json.dumps(config.config_google_drive_watch_changes_response)
|
config.config_google_drive_watch_changes_response = json.dumps(config.config_google_drive_watch_changes_response)
|
||||||
@ -175,8 +180,6 @@ def update_view_configuration():
|
|||||||
config.config_default_role &= ~constants.ROLE_ANONYMOUS
|
config.config_default_role &= ~constants.ROLE_ANONYMOUS
|
||||||
|
|
||||||
config.config_default_show = sum(int(k[5:]) for k in to_save if k.startswith('show_'))
|
config.config_default_show = sum(int(k[5:]) for k in to_save if k.startswith('show_'))
|
||||||
if "Show_mature_content" in to_save:
|
|
||||||
config.config_default_show |= constants.MATURE_CONTENT
|
|
||||||
if "Show_detail_random" in to_save:
|
if "Show_detail_random" in to_save:
|
||||||
config.config_default_show |= constants.DETAIL_RANDOM
|
config.config_default_show |= constants.DETAIL_RANDOM
|
||||||
|
|
||||||
@ -201,7 +204,6 @@ def edit_domain(allow):
|
|||||||
# value: 'superuser!' //new value
|
# value: 'superuser!' //new value
|
||||||
vals = request.form.to_dict()
|
vals = request.form.to_dict()
|
||||||
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
|
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
|
||||||
# domain_name = request.args.get('domain')
|
|
||||||
answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower()
|
answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower()
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
return ""
|
return ""
|
||||||
@ -246,6 +248,228 @@ def list_domain(allow):
|
|||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@admi.route("/ajax/editrestriction/<int:type>", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def edit_restriction(type):
|
||||||
|
element = request.form.to_dict()
|
||||||
|
if element['id'].startswith('a'):
|
||||||
|
if type == 0: # Tags as template
|
||||||
|
elementlist = config.list_allowed_tags()
|
||||||
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
|
config.config_allowed_tags = ','.join(elementlist)
|
||||||
|
config.save()
|
||||||
|
if type == 1: # CustomC
|
||||||
|
elementlist = config.list_allowed_column_values()
|
||||||
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
|
config.config_allowed_column_value = ','.join(elementlist)
|
||||||
|
config.save()
|
||||||
|
if type == 2: # Tags per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
elementlist = usr.list_allowed_tags()
|
||||||
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
|
usr.allowed_tags = ','.join(elementlist)
|
||||||
|
ub.session.commit()
|
||||||
|
if type == 3: # CColumn per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
elementlist = usr.list_allowed_column_values()
|
||||||
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
|
usr.allowed_column_value = ','.join(elementlist)
|
||||||
|
ub.session.commit()
|
||||||
|
if element['id'].startswith('d'):
|
||||||
|
if type == 0: # Tags as template
|
||||||
|
elementlist = config.list_denied_tags()
|
||||||
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
|
config.config_denied_tags = ','.join(elementlist)
|
||||||
|
config.save()
|
||||||
|
if type == 1: # CustomC
|
||||||
|
elementlist = config.list_denied_column_values()
|
||||||
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
|
config.config_denied_column_value = ','.join(elementlist)
|
||||||
|
config.save()
|
||||||
|
pass
|
||||||
|
if type == 2: # Tags per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
elementlist = usr.list_denied_tags()
|
||||||
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
|
usr.denied_tags = ','.join(elementlist)
|
||||||
|
ub.session.commit()
|
||||||
|
if type == 3: # CColumn per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
elementlist = usr.list_denied_column_values()
|
||||||
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
|
usr.denied_column_value = ','.join(elementlist)
|
||||||
|
ub.session.commit()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def restriction_addition(element, list_func):
|
||||||
|
elementlist = list_func()
|
||||||
|
if elementlist == ['']:
|
||||||
|
elementlist = []
|
||||||
|
if not element['add_element'] in elementlist:
|
||||||
|
elementlist += [element['add_element']]
|
||||||
|
return ','.join(elementlist)
|
||||||
|
|
||||||
|
|
||||||
|
def restriction_deletion(element, list_func):
|
||||||
|
elementlist = list_func()
|
||||||
|
if element['Element'] in elementlist:
|
||||||
|
elementlist.remove(element['Element'])
|
||||||
|
return ','.join(elementlist)
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/ajax/addrestriction/<int:type>", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def add_restriction(type):
|
||||||
|
element = request.form.to_dict()
|
||||||
|
if type == 0: # Tags as template
|
||||||
|
if 'submit_allow' in element:
|
||||||
|
config.config_allowed_tags = restriction_addition(element, config.list_allowed_tags)
|
||||||
|
config.save()
|
||||||
|
elif 'submit_deny' in element:
|
||||||
|
config.config_denied_tags = restriction_addition(element, config.list_denied_tags)
|
||||||
|
config.save()
|
||||||
|
if type == 1: # CCustom as template
|
||||||
|
if 'submit_allow' in element:
|
||||||
|
config.config_allowed_column_value = restriction_addition(element, config.list_denied_column_values)
|
||||||
|
config.save()
|
||||||
|
elif 'submit_deny' in element:
|
||||||
|
config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values)
|
||||||
|
config.save()
|
||||||
|
if type == 2: # Tags per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
if 'submit_allow' in element:
|
||||||
|
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
|
||||||
|
ub.session.commit()
|
||||||
|
elif 'submit_deny' in element:
|
||||||
|
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
|
||||||
|
ub.session.commit()
|
||||||
|
if type == 3: # CustomC per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
if 'submit_allow' in element:
|
||||||
|
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
|
||||||
|
ub.session.commit()
|
||||||
|
elif 'submit_deny' in element:
|
||||||
|
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
|
||||||
|
ub.session.commit()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@admi.route("/ajax/deleterestriction/<int:type>", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def delete_restriction(type):
|
||||||
|
element = request.form.to_dict()
|
||||||
|
if type == 0: # Tags as template
|
||||||
|
if element['id'].startswith('a'):
|
||||||
|
config.config_allowed_tags = restriction_deletion(element, config.list_allowed_tags)
|
||||||
|
config.save()
|
||||||
|
elif element['id'].startswith('d'):
|
||||||
|
config.config_denied_tags = restriction_deletion(element, config.list_denied_tags)
|
||||||
|
config.save()
|
||||||
|
elif type == 1: # CustomC as template
|
||||||
|
if element['id'].startswith('a'):
|
||||||
|
config.config_allowed_column_value = restriction_deletion(element, config.list_allowed_column_values)
|
||||||
|
config.save()
|
||||||
|
elif element['id'].startswith('d'):
|
||||||
|
config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values)
|
||||||
|
config.save()
|
||||||
|
elif type == 2: # Tags per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
if element['id'].startswith('a'):
|
||||||
|
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
|
||||||
|
ub.session.commit()
|
||||||
|
elif element['id'].startswith('d'):
|
||||||
|
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
|
||||||
|
ub.session.commit()
|
||||||
|
elif type == 3: # Columns per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True: # select current user if admins are editing their own rights
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
if element['id'].startswith('a'):
|
||||||
|
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
|
||||||
|
ub.session.commit()
|
||||||
|
elif element['id'].startswith('d'):
|
||||||
|
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
|
||||||
|
ub.session.commit()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
#@admi.route("/ajax/listrestriction/<int:type>/<int:user_id>", defaults={'user_id': '0'})
|
||||||
|
@admi.route("/ajax/listrestriction/<int:type>")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def list_restriction(type):
|
||||||
|
if type == 0: # Tags as template
|
||||||
|
restrict = [{'Element': x, 'type':_('deny'), 'id': 'd'+str(i) }
|
||||||
|
for i,x in enumerate(config.list_denied_tags()) if x != '' ]
|
||||||
|
allow = [{'Element': x, 'type':_('allow'), 'id': 'a'+str(i) }
|
||||||
|
for i,x in enumerate(config.list_allowed_tags()) if x != '']
|
||||||
|
json_dumps = restrict + allow
|
||||||
|
elif type == 1: # CustomC as template
|
||||||
|
restrict = [{'Element': x, 'type':_('deny'), 'id': 'd'+str(i) }
|
||||||
|
for i,x in enumerate(config.list_denied_column_values()) if x != '' ]
|
||||||
|
allow = [{'Element': x, 'type':_('allow'), 'id': 'a'+str(i) }
|
||||||
|
for i,x in enumerate(config.list_allowed_column_values()) if x != '']
|
||||||
|
json_dumps = restrict + allow
|
||||||
|
elif type == 2: # Tags per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id == usr_id).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
restrict = [{'Element': x, 'type':_('deny'), 'id': 'd'+str(i) }
|
||||||
|
for i,x in enumerate(usr.list_denied_tags()) if x != '' ]
|
||||||
|
allow = [{'Element': x, 'type':_('allow'), 'id': 'a'+str(i) }
|
||||||
|
for i,x in enumerate(usr.list_allowed_tags()) if x != '']
|
||||||
|
json_dumps = restrict + allow
|
||||||
|
elif type == 3: # CustomC per user
|
||||||
|
usr_id = os.path.split(request.referrer)[-1]
|
||||||
|
if usr_id.isdigit() == True:
|
||||||
|
usr = ub.session.query(ub.User).filter(ub.User.id==usr_id).first()
|
||||||
|
else:
|
||||||
|
usr = current_user
|
||||||
|
restrict = [{'Element': x, 'type':_('deny'), 'id': 'd'+str(i) }
|
||||||
|
for i,x in enumerate(usr.list_denied_column_values()) if x != '' ]
|
||||||
|
allow = [{'Element': x, 'type':_('allow'), 'id': 'a'+str(i) }
|
||||||
|
for i,x in enumerate(usr.list_allowed_column_values()) if x != '']
|
||||||
|
json_dumps = restrict + allow
|
||||||
|
else:
|
||||||
|
json_dumps=""
|
||||||
|
js = json.dumps(json_dumps)
|
||||||
|
response = make_response(js.replace("'", '"'))
|
||||||
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
|
return response
|
||||||
|
|
||||||
@admi.route("/config", methods=["GET", "POST"])
|
@admi.route("/config", methods=["GET", "POST"])
|
||||||
@unconfigured
|
@unconfigured
|
||||||
@ -261,7 +485,6 @@ def _configuration_update_helper():
|
|||||||
db_change = False
|
db_change = False
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
|
|
||||||
# _config_dict = lambda x: config.set_from_dictionary(to_save, x, lambda y: y['id'])
|
|
||||||
_config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
|
_config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
|
||||||
_config_int = lambda x: config.set_from_dictionary(to_save, x, int)
|
_config_int = lambda x: config.set_from_dictionary(to_save, x, int)
|
||||||
_config_checkbox = lambda x: config.set_from_dictionary(to_save, x, lambda y: y == "on", False)
|
_config_checkbox = lambda x: config.set_from_dictionary(to_save, x, lambda y: y == "on", False)
|
||||||
@ -304,6 +527,9 @@ def _configuration_update_helper():
|
|||||||
_config_checkbox_int("config_uploading")
|
_config_checkbox_int("config_uploading")
|
||||||
_config_checkbox_int("config_anonbrowse")
|
_config_checkbox_int("config_anonbrowse")
|
||||||
_config_checkbox_int("config_public_reg")
|
_config_checkbox_int("config_public_reg")
|
||||||
|
reboot_required |= _config_checkbox_int("config_kobo_sync")
|
||||||
|
_config_checkbox_int("config_kobo_proxy")
|
||||||
|
|
||||||
|
|
||||||
_config_int("config_ebookconverter")
|
_config_int("config_ebookconverter")
|
||||||
_config_string("config_calibre")
|
_config_string("config_calibre")
|
||||||
@ -338,7 +564,7 @@ def _configuration_update_helper():
|
|||||||
# Remote login configuration
|
# Remote login configuration
|
||||||
_config_checkbox("config_remote_login")
|
_config_checkbox("config_remote_login")
|
||||||
if not config.config_remote_login:
|
if not config.config_remote_login:
|
||||||
ub.session.query(ub.RemoteAuthToken).delete()
|
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type==0).delete()
|
||||||
|
|
||||||
# Goodreads configuration
|
# Goodreads configuration
|
||||||
_config_checkbox("config_use_goodreads")
|
_config_checkbox("config_use_goodreads")
|
||||||
@ -448,10 +674,11 @@ def new_user():
|
|||||||
content = ub.User()
|
content = ub.User()
|
||||||
languages = speaking_language()
|
languages = speaking_language()
|
||||||
translations = [LC('en')] + babel.list_translations()
|
translations = [LC('en')] + babel.list_translations()
|
||||||
|
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
content.default_language = to_save["default_language"]
|
content.default_language = to_save["default_language"]
|
||||||
content.mature_content = "Show_mature_content" in to_save
|
# content.mature_content = "Show_mature_content" in to_save
|
||||||
content.locale = to_save.get("locale", content.locale)
|
content.locale = to_save.get("locale", content.locale)
|
||||||
|
|
||||||
content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_'))
|
content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_'))
|
||||||
@ -463,7 +690,8 @@ def new_user():
|
|||||||
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
|
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
|
||||||
flash(_(u"Please fill out all fields!"), category="error")
|
flash(_(u"Please fill out all fields!"), category="error")
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||||
registered_oauth=oauth_check, title=_(u"Add new user"))
|
registered_oauth=oauth_check, kobo_support=kobo_support,
|
||||||
|
title=_(u"Add new user"))
|
||||||
content.password = generate_password_hash(to_save["password"])
|
content.password = generate_password_hash(to_save["password"])
|
||||||
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\
|
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\
|
||||||
.first()
|
.first()
|
||||||
@ -474,15 +702,20 @@ def new_user():
|
|||||||
if config.config_public_reg and not check_valid_domain(to_save["email"]):
|
if config.config_public_reg and not check_valid_domain(to_save["email"]):
|
||||||
flash(_(u"E-mail is not from valid domain"), category="error")
|
flash(_(u"E-mail is not from valid domain"), category="error")
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||||
registered_oauth=oauth_check, title=_(u"Add new user"))
|
registered_oauth=oauth_check, kobo_support=kobo_support,
|
||||||
|
title=_(u"Add new user"))
|
||||||
else:
|
else:
|
||||||
content.email = to_save["email"]
|
content.email = to_save["email"]
|
||||||
else:
|
else:
|
||||||
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
|
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||||
registered_oauth=oauth_check)
|
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||||
try:
|
try:
|
||||||
|
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)
|
ub.session.add(content)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
|
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
|
||||||
@ -493,10 +726,9 @@ def new_user():
|
|||||||
else:
|
else:
|
||||||
content.role = config.config_default_role
|
content.role = config.config_default_role
|
||||||
content.sidebar_view = config.config_default_show
|
content.sidebar_view = config.config_default_show
|
||||||
content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT)
|
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||||
registered_oauth=oauth_check)
|
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/mailsettings")
|
@admi.route("/admin/mailsettings")
|
||||||
@ -551,6 +783,7 @@ def edit_user(user_id):
|
|||||||
downloads = list()
|
downloads = list()
|
||||||
languages = speaking_language()
|
languages = speaking_language()
|
||||||
translations = babel.list_translations() + [LC('en')]
|
translations = babel.list_translations() + [LC('en')]
|
||||||
|
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||||
for book in content.downloads:
|
for book in content.downloads:
|
||||||
downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
|
downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
|
||||||
if downloadbook:
|
if downloadbook:
|
||||||
@ -596,8 +829,6 @@ def edit_user(user_id):
|
|||||||
else:
|
else:
|
||||||
content.sidebar_view &= ~constants.DETAIL_RANDOM
|
content.sidebar_view &= ~constants.DETAIL_RANDOM
|
||||||
|
|
||||||
content.mature_content = "Show_mature_content" in to_save
|
|
||||||
|
|
||||||
if "default_language" in to_save:
|
if "default_language" in to_save:
|
||||||
content.default_language = to_save["default_language"]
|
content.default_language = to_save["default_language"]
|
||||||
if "locale" in to_save and to_save["locale"]:
|
if "locale" in to_save and to_save["locale"]:
|
||||||
@ -609,9 +840,15 @@ def edit_user(user_id):
|
|||||||
content.email = to_save["email"]
|
content.email = to_save["email"]
|
||||||
else:
|
else:
|
||||||
flash(_(u"Found an existing account for this e-mail address."), category="error")
|
flash(_(u"Found an existing account for this e-mail address."), category="error")
|
||||||
return render_title_template("user_edit.html", translations=translations, languages=languages,
|
return render_title_template("user_edit.html",
|
||||||
|
translations=translations,
|
||||||
|
languages=languages,
|
||||||
mail_configured = config.get_mail_server_configured(),
|
mail_configured = config.get_mail_server_configured(),
|
||||||
new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check,
|
kobo_support=kobo_support,
|
||||||
|
new_user=0,
|
||||||
|
content=content,
|
||||||
|
downloads=downloads,
|
||||||
|
registered_oauth=oauth_check,
|
||||||
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
||||||
if "nickname" in to_save and to_save["nickname"] != content.nickname:
|
if "nickname" in to_save and to_save["nickname"] != content.nickname:
|
||||||
# Query User nickname, if not existing, change
|
# Query User nickname, if not existing, change
|
||||||
@ -626,6 +863,7 @@ def edit_user(user_id):
|
|||||||
new_user=0, content=content,
|
new_user=0, content=content,
|
||||||
downloads=downloads,
|
downloads=downloads,
|
||||||
registered_oauth=oauth_check,
|
registered_oauth=oauth_check,
|
||||||
|
kobo_support=kobo_support,
|
||||||
title=_(u"Edit User %(nick)s", nick=content.nickname),
|
title=_(u"Edit User %(nick)s", nick=content.nickname),
|
||||||
page="edituser")
|
page="edituser")
|
||||||
|
|
||||||
@ -637,9 +875,15 @@ def edit_user(user_id):
|
|||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"An unknown error occured."), category="error")
|
flash(_(u"An unknown error occured."), category="error")
|
||||||
return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0,
|
return render_title_template("user_edit.html",
|
||||||
content=content, downloads=downloads, registered_oauth=oauth_check,
|
translations=translations,
|
||||||
|
languages=languages,
|
||||||
|
new_user=0,
|
||||||
|
content=content,
|
||||||
|
downloads=downloads,
|
||||||
|
registered_oauth=oauth_check,
|
||||||
mail_configured=config.get_mail_server_configured(),
|
mail_configured=config.get_mail_server_configured(),
|
||||||
|
kobo_support=kobo_support,
|
||||||
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import sys
|
|||||||
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean
|
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
from . import constants, cli, logger
|
from . import constants, cli, logger, ub
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@ -68,12 +68,18 @@ class _Settings(_Base):
|
|||||||
config_anonbrowse = Column(SmallInteger, default=0)
|
config_anonbrowse = Column(SmallInteger, default=0)
|
||||||
config_public_reg = Column(SmallInteger, default=0)
|
config_public_reg = Column(SmallInteger, default=0)
|
||||||
config_remote_login = Column(Boolean, default=False)
|
config_remote_login = Column(Boolean, default=False)
|
||||||
|
config_kobo_sync = Column(Boolean, default=False)
|
||||||
|
|
||||||
config_default_role = Column(SmallInteger, default=0)
|
config_default_role = Column(SmallInteger, default=0)
|
||||||
config_default_show = Column(SmallInteger, default=6143)
|
config_default_show = Column(SmallInteger, default=6143)
|
||||||
config_columns_to_ignore = Column(String)
|
config_columns_to_ignore = Column(String)
|
||||||
|
|
||||||
|
config_denied_tags = Column(String, default="")
|
||||||
|
config_allowed_tags = Column(String, default="")
|
||||||
|
config_restricted_column = Column(SmallInteger, default=0)
|
||||||
|
config_denied_column_value = Column(String, default="")
|
||||||
|
config_allowed_column_value = Column(String, default="")
|
||||||
|
|
||||||
config_use_google_drive = Column(Boolean, default=False)
|
config_use_google_drive = Column(Boolean, default=False)
|
||||||
config_google_drive_folder = Column(String)
|
config_google_drive_folder = Column(String)
|
||||||
config_google_drive_watch_changes_response = Column(String)
|
config_google_drive_watch_changes_response = Column(String)
|
||||||
@ -84,7 +90,8 @@ class _Settings(_Base):
|
|||||||
|
|
||||||
config_login_type = Column(Integer, default=0)
|
config_login_type = Column(Integer, default=0)
|
||||||
|
|
||||||
# config_oauth_provider = Column(Integer)
|
config_kobo_proxy = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
|
||||||
config_ldap_provider_url = Column(String, default='localhost')
|
config_ldap_provider_url = Column(String, default='localhost')
|
||||||
config_ldap_port = Column(SmallInteger, default=389)
|
config_ldap_port = Column(SmallInteger, default=389)
|
||||||
@ -179,11 +186,20 @@ class _ConfigSQL(object):
|
|||||||
def show_detail_random(self):
|
def show_detail_random(self):
|
||||||
return self.show_element_new_user(constants.DETAIL_RANDOM)
|
return self.show_element_new_user(constants.DETAIL_RANDOM)
|
||||||
|
|
||||||
def show_mature_content(self):
|
def list_denied_tags(self):
|
||||||
return self.show_element_new_user(constants.MATURE_CONTENT)
|
mct = self.config_denied_tags.split(",")
|
||||||
|
return [t.strip() for t in mct]
|
||||||
|
|
||||||
def mature_content_tags(self):
|
def list_allowed_tags(self):
|
||||||
mct = self.config_mature_content_tags.split(",")
|
mct = self.config_allowed_tags.split(",")
|
||||||
|
return [t.strip() for t in mct]
|
||||||
|
|
||||||
|
def list_denied_column_values(self):
|
||||||
|
mct = self.config_denied_column_value.split(",")
|
||||||
|
return [t.strip() for t in mct]
|
||||||
|
|
||||||
|
def list_allowed_column_values(self):
|
||||||
|
mct = self.config_allowed_column_value.split(",")
|
||||||
return [t.strip() for t in mct]
|
return [t.strip() for t in mct]
|
||||||
|
|
||||||
def get_log_level(self):
|
def get_log_level(self):
|
||||||
@ -323,5 +339,12 @@ def load_configuration(session):
|
|||||||
if not session.query(_Settings).count():
|
if not session.query(_Settings).count():
|
||||||
session.add(_Settings())
|
session.add(_Settings())
|
||||||
session.commit()
|
session.commit()
|
||||||
|
conf = _ConfigSQL(session)
|
||||||
return _ConfigSQL(session)
|
# Migrate from global restrictions to user based restrictions
|
||||||
|
if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
|
||||||
|
conf.config_denied_tags = conf.config_mature_content_tags
|
||||||
|
conf.save()
|
||||||
|
session.query(ub.User).filter(ub.User.mature_content != True). \
|
||||||
|
update({"restricted_tags": conf.config_mature_content_tags}, synchronize_session=False)
|
||||||
|
session.commit()
|
||||||
|
return conf
|
||||||
|
@ -25,7 +25,7 @@ import ast
|
|||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy import Table, Column, ForeignKey
|
from sqlalchemy import Table, Column, ForeignKey
|
||||||
from sqlalchemy import String, Integer, Boolean, Float
|
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
||||||
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
|
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
@ -251,10 +251,10 @@ class Books(Base):
|
|||||||
title = Column(String)
|
title = Column(String)
|
||||||
sort = Column(String)
|
sort = Column(String)
|
||||||
author_sort = Column(String)
|
author_sort = Column(String)
|
||||||
timestamp = Column(String)
|
timestamp = Column(TIMESTAMP)
|
||||||
pubdate = Column(String)
|
pubdate = Column(String)
|
||||||
series_index = Column(String)
|
series_index = Column(String)
|
||||||
last_modified = Column(String)
|
last_modified = Column(TIMESTAMP)
|
||||||
path = Column(String)
|
path = Column(String)
|
||||||
has_cover = Column(Integer)
|
has_cover = Column(Integer)
|
||||||
uuid = Column(String)
|
uuid = Column(String)
|
||||||
|
@ -448,32 +448,46 @@ def delete_book(book, calibrepath, book_format):
|
|||||||
return delete_book_file(book, calibrepath, book_format)
|
return delete_book_file(book, calibrepath, book_format)
|
||||||
|
|
||||||
|
|
||||||
def get_book_cover(book_id):
|
def get_cover_on_failure(use_generic_cover):
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
if use_generic_cover:
|
||||||
if book.has_cover:
|
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_book_cover(book_id):
|
||||||
|
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
|
||||||
|
return get_book_cover_internal(book, use_generic_cover_on_failure=True)
|
||||||
|
|
||||||
|
def get_book_cover_with_uuid(book_uuid,
|
||||||
|
use_generic_cover_on_failure=True):
|
||||||
|
book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first()
|
||||||
|
return get_book_cover_internal(book, use_generic_cover_on_failure)
|
||||||
|
|
||||||
|
def get_book_cover_internal(book,
|
||||||
|
use_generic_cover_on_failure):
|
||||||
|
if book and book.has_cover:
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
try:
|
try:
|
||||||
if not gd.is_gdrive_ready():
|
if not gd.is_gdrive_ready():
|
||||||
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
path=gd.get_cover_via_gdrive(book.path)
|
path=gd.get_cover_via_gdrive(book.path)
|
||||||
if path:
|
if path:
|
||||||
return redirect(path)
|
return redirect(path)
|
||||||
else:
|
else:
|
||||||
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
||||||
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
# traceback.print_exc()
|
# traceback.print_exc()
|
||||||
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
else:
|
else:
|
||||||
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
||||||
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
|
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
|
||||||
return send_from_directory(cover_file_path, "cover.jpg")
|
return send_from_directory(cover_file_path, "cover.jpg")
|
||||||
else:
|
else:
|
||||||
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
else:
|
else:
|
||||||
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
|
|
||||||
|
|
||||||
# saves book cover from url
|
# saves book cover from url
|
||||||
@ -674,20 +688,40 @@ def common_filters():
|
|||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
||||||
else:
|
else:
|
||||||
lang_filter = true()
|
lang_filter = true()
|
||||||
content_rating_filter = false() if current_user.mature_content else \
|
negtags_list = current_user.list_denied_tags()
|
||||||
db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags()))
|
postags_list = current_user.list_allowed_tags()
|
||||||
return and_(lang_filter, ~content_rating_filter)
|
neg_content_tags_filter = false() if negtags_list == [''] else db.Books.tags.any(db.Tags.name.in_(negtags_list))
|
||||||
|
pos_content_tags_filter = true() if postags_list == [''] else db.Books.tags.any(db.Tags.name.in_(postags_list))
|
||||||
|
if config.config_restricted_column:
|
||||||
|
pos_cc_list = current_user.allowed_column_value.split(',')
|
||||||
|
pos_content_cc_filter = true() if pos_cc_list == [''] else \
|
||||||
|
getattr(db.Books, 'custom_column_' + str(config.config_restricted_column)).\
|
||||||
|
any(db.cc_classes[config.config_restricted_column].value.in_(pos_cc_list))
|
||||||
|
neg_cc_list = current_user.denied_column_value.split(',')
|
||||||
|
neg_content_cc_filter = false() if neg_cc_list == [''] else \
|
||||||
|
getattr(db.Books, 'custom_column_' + str(config.config_restricted_column)).\
|
||||||
|
any(db.cc_classes[config.config_restricted_column].value.in_(neg_cc_list))
|
||||||
|
else:
|
||||||
|
pos_content_cc_filter = true()
|
||||||
|
neg_content_cc_filter = false()
|
||||||
|
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
|
||||||
|
pos_content_cc_filter, ~neg_content_cc_filter)
|
||||||
|
|
||||||
|
|
||||||
def tags_filters():
|
def tags_filters():
|
||||||
return ~(false() if current_user.mature_content else \
|
negtags_list = current_user.list_denied_tags()
|
||||||
db.Tags.name.in_(config.mature_content_tags()))
|
postags_list = current_user.list_allowed_tags()
|
||||||
# return db.session.query(db.Tags).filter(~content_rating_filter).order_by(db.Tags.name).all()
|
neg_content_tags_filter = false() if negtags_list == [''] else db.Tags.name.in_(negtags_list)
|
||||||
|
pos_content_tags_filter = true() if postags_list == [''] else db.Tags.name.in_(postags_list)
|
||||||
|
return and_(pos_content_tags_filter, ~neg_content_tags_filter)
|
||||||
|
# return ~(false()) if postags_list == [''] else db.Tags.in_(postags_list)
|
||||||
|
|
||||||
|
|
||||||
# Creates for all stored languages a translated speaking name in the array for the UI
|
# Creates for all stored languages a translated speaking name in the array for the UI
|
||||||
def speaking_language(languages=None):
|
def speaking_language(languages=None):
|
||||||
if not languages:
|
if not languages:
|
||||||
languages = db.session.query(db.Languages).all()
|
languages = db.session.query(db.Languages).join(db.books_languages_link).join(db.Books).filter(common_filters())\
|
||||||
|
.group_by(text('books_languages_link.lang_code')).all()
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
try:
|
try:
|
||||||
cur_l = LC.parse(lang.lang_code)
|
cur_l = LC.parse(lang.lang_code)
|
||||||
@ -774,7 +808,7 @@ def get_cc_columns():
|
|||||||
cc = []
|
cc = []
|
||||||
for col in tmpcc:
|
for col in tmpcc:
|
||||||
r = re.compile(config.config_columns_to_ignore)
|
r = re.compile(config.config_columns_to_ignore)
|
||||||
if r.match(col.label):
|
if not r.match(col.name):
|
||||||
cc.append(col)
|
cc.append(col)
|
||||||
else:
|
else:
|
||||||
cc = tmpcc
|
cc = tmpcc
|
||||||
|
622
cps/kobo.py
Normal file
622
cps/kobo.py
Normal file
@ -0,0 +1,622 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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 sys
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from time import gmtime, strftime
|
||||||
|
try:
|
||||||
|
from urllib import unquote
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
request,
|
||||||
|
make_response,
|
||||||
|
jsonify,
|
||||||
|
current_app,
|
||||||
|
url_for,
|
||||||
|
redirect,
|
||||||
|
abort
|
||||||
|
)
|
||||||
|
from flask_login import login_required
|
||||||
|
from werkzeug.datastructures import Headers
|
||||||
|
from sqlalchemy import func
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from . import config, logger, kobo_auth, db, helper
|
||||||
|
from .services import SyncToken as SyncToken
|
||||||
|
from .web import download_required
|
||||||
|
from .kobo_auth import requires_kobo_auth
|
||||||
|
|
||||||
|
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
||||||
|
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
||||||
|
|
||||||
|
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
|
||||||
|
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo)
|
||||||
|
kobo_auth.register_url_value_preprocessor(kobo)
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
def get_store_url_for_current_request():
|
||||||
|
# Programmatically modify the current url to point to the official Kobo store
|
||||||
|
base, sep, request_path_with_auth_token = request.full_path.rpartition("/kobo/")
|
||||||
|
auth_token, sep, request_path = request_path_with_auth_token.rstrip("?").partition(
|
||||||
|
"/"
|
||||||
|
)
|
||||||
|
return KOBO_STOREAPI_URL + "/" + request_path
|
||||||
|
|
||||||
|
|
||||||
|
CONNECTION_SPECIFIC_HEADERS = [
|
||||||
|
"connection",
|
||||||
|
"content-encoding",
|
||||||
|
"content-length",
|
||||||
|
"transfer-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_kobo_activated():
|
||||||
|
return config.config_kobo_sync
|
||||||
|
|
||||||
|
|
||||||
|
def make_request_to_kobo_store(sync_token=None):
|
||||||
|
outgoing_headers = Headers(request.headers)
|
||||||
|
outgoing_headers.remove("Host")
|
||||||
|
if sync_token:
|
||||||
|
sync_token.set_kobo_store_header(outgoing_headers)
|
||||||
|
|
||||||
|
store_response = requests.request(
|
||||||
|
method=request.method,
|
||||||
|
url=get_store_url_for_current_request(),
|
||||||
|
headers=outgoing_headers,
|
||||||
|
data=request.get_data(),
|
||||||
|
allow_redirects=False,
|
||||||
|
timeout=(2, 10)
|
||||||
|
)
|
||||||
|
return store_response
|
||||||
|
|
||||||
|
|
||||||
|
def redirect_or_proxy_request():
|
||||||
|
if config.config_kobo_proxy:
|
||||||
|
if request.method == "GET":
|
||||||
|
return redirect(get_store_url_for_current_request(), 307)
|
||||||
|
if request.method == "DELETE":
|
||||||
|
log.info('Delete Book')
|
||||||
|
return make_response(jsonify({}))
|
||||||
|
else:
|
||||||
|
# The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves.
|
||||||
|
store_response = make_request_to_kobo_store()
|
||||||
|
|
||||||
|
response_headers = store_response.headers
|
||||||
|
for header_key in CONNECTION_SPECIFIC_HEADERS:
|
||||||
|
response_headers.pop(header_key, default=None)
|
||||||
|
|
||||||
|
return make_response(
|
||||||
|
store_response.content, store_response.status_code, response_headers.items()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return make_response(jsonify({}))
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/library/sync")
|
||||||
|
@requires_kobo_auth
|
||||||
|
@download_required
|
||||||
|
def HandleSyncRequest():
|
||||||
|
sync_token = SyncToken.SyncToken.from_headers(request.headers)
|
||||||
|
log.info("Kobo library sync request received.")
|
||||||
|
if not current_app.wsgi_app.is_proxied:
|
||||||
|
log.debug('Kobo: Received unproxied request, changed request port to server port')
|
||||||
|
|
||||||
|
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header
|
||||||
|
# instead so that the device triggers another sync.
|
||||||
|
|
||||||
|
new_books_last_modified = sync_token.books_last_modified
|
||||||
|
new_books_last_created = sync_token.books_last_created
|
||||||
|
entitlements = []
|
||||||
|
|
||||||
|
# We reload the book database so that the user get's a fresh view of the library
|
||||||
|
# in case of external changes (e.g: adding a book through Calibre).
|
||||||
|
db.reconnect_db(config)
|
||||||
|
|
||||||
|
# sqlite gives unexpected results when performing the last_modified comparison without the datetime cast.
|
||||||
|
# It looks like it's treating the db.Books.last_modified field as a string and may fail
|
||||||
|
# the comparison because of the +00:00 suffix.
|
||||||
|
changed_entries = (
|
||||||
|
db.session.query(db.Books)
|
||||||
|
.join(db.Data)
|
||||||
|
.filter(func.datetime(db.Books.last_modified) > sync_token.books_last_modified)
|
||||||
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
for book in changed_entries:
|
||||||
|
entitlement = {
|
||||||
|
"BookEntitlement": create_book_entitlement(book),
|
||||||
|
"BookMetadata": get_metadata(book),
|
||||||
|
"ReadingState": reading_state(book),
|
||||||
|
}
|
||||||
|
if book.timestamp > sync_token.books_last_created:
|
||||||
|
entitlements.append({"NewEntitlement": entitlement})
|
||||||
|
else:
|
||||||
|
entitlements.append({"ChangedEntitlement": entitlement})
|
||||||
|
|
||||||
|
new_books_last_modified = max(
|
||||||
|
book.last_modified, sync_token.books_last_modified
|
||||||
|
)
|
||||||
|
new_books_last_created = max(book.timestamp, sync_token.books_last_created)
|
||||||
|
|
||||||
|
sync_token.books_last_created = new_books_last_created
|
||||||
|
sync_token.books_last_modified = new_books_last_modified
|
||||||
|
|
||||||
|
if config.config_kobo_proxy:
|
||||||
|
return generate_sync_response(request, sync_token, entitlements)
|
||||||
|
|
||||||
|
return make_response(jsonify(entitlements))
|
||||||
|
# Missing feature: Detect server-side book deletions.
|
||||||
|
|
||||||
|
|
||||||
|
def generate_sync_response(request, sync_token, entitlements):
|
||||||
|
extra_headers = {}
|
||||||
|
if config.config_kobo_proxy:
|
||||||
|
# Merge in sync results from the official Kobo store.
|
||||||
|
try:
|
||||||
|
store_response = make_request_to_kobo_store(sync_token)
|
||||||
|
|
||||||
|
store_entitlements = store_response.json()
|
||||||
|
entitlements += store_entitlements
|
||||||
|
sync_token.merge_from_store_response(store_response)
|
||||||
|
extra_headers["x-kobo-sync"] = store_response.headers.get("x-kobo-sync")
|
||||||
|
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
|
||||||
|
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
|
||||||
|
sync_token.to_headers(extra_headers)
|
||||||
|
|
||||||
|
response = make_response(jsonify(entitlements), extra_headers)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/library/<book_uuid>/metadata")
|
||||||
|
@requires_kobo_auth
|
||||||
|
@download_required
|
||||||
|
def HandleMetadataRequest(book_uuid):
|
||||||
|
if not current_app.wsgi_app.is_proxied:
|
||||||
|
log.debug('Kobo: Received unproxied request, changed request port to server port')
|
||||||
|
log.info("Kobo library metadata request received for book %s" % book_uuid)
|
||||||
|
book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first()
|
||||||
|
if not book or not book.data:
|
||||||
|
log.info(u"Book %s not found in database", book_uuid)
|
||||||
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
metadata = get_metadata(book)
|
||||||
|
return jsonify([metadata])
|
||||||
|
|
||||||
|
|
||||||
|
def get_download_url_for_book(book, book_format):
|
||||||
|
if not current_app.wsgi_app.is_proxied:
|
||||||
|
return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format(
|
||||||
|
url_scheme=request.environ['wsgi.url_scheme'],
|
||||||
|
url_base=request.environ['SERVER_NAME'],
|
||||||
|
url_port=config.config_port,
|
||||||
|
book_id=book.id,
|
||||||
|
book_format=book_format.lower()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return url_for(
|
||||||
|
"web.download_link",
|
||||||
|
book_id=book.id,
|
||||||
|
book_format=book_format.lower(),
|
||||||
|
_external=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_book_entitlement(book):
|
||||||
|
book_uuid = book.uuid
|
||||||
|
return {
|
||||||
|
"Accessibility": "Full",
|
||||||
|
"ActivePeriod": {"From": current_time(),},
|
||||||
|
"Created": book.timestamp,
|
||||||
|
"CrossRevisionId": book_uuid,
|
||||||
|
"Id": book_uuid,
|
||||||
|
"IsHiddenFromArchive": False,
|
||||||
|
"IsLocked": False,
|
||||||
|
# Setting this to true removes from the device.
|
||||||
|
"IsRemoved": False,
|
||||||
|
"LastModified": book.last_modified,
|
||||||
|
"OriginCategory": "Imported",
|
||||||
|
"RevisionId": book_uuid,
|
||||||
|
"Status": "Active",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def current_time():
|
||||||
|
return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
|
||||||
|
|
||||||
|
|
||||||
|
def get_description(book):
|
||||||
|
if not book.comments:
|
||||||
|
return None
|
||||||
|
return book.comments[0].text
|
||||||
|
|
||||||
|
|
||||||
|
# TODO handle multiple authors
|
||||||
|
def get_author(book):
|
||||||
|
if not book.authors:
|
||||||
|
return None
|
||||||
|
return book.authors[0].name
|
||||||
|
|
||||||
|
|
||||||
|
def get_publisher(book):
|
||||||
|
if not book.publishers:
|
||||||
|
return None
|
||||||
|
return book.publishers[0].name
|
||||||
|
|
||||||
|
|
||||||
|
def get_series(book):
|
||||||
|
if not book.series:
|
||||||
|
return None
|
||||||
|
return book.series[0].name
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata(book):
|
||||||
|
download_urls = []
|
||||||
|
for book_data in book.data:
|
||||||
|
if book_data.format not in KOBO_FORMATS:
|
||||||
|
continue
|
||||||
|
for kobo_format in KOBO_FORMATS[book_data.format]:
|
||||||
|
# log.debug('Id: %s, Format: %s' % (book.id, kobo_format))
|
||||||
|
download_urls.append(
|
||||||
|
{
|
||||||
|
"Format": kobo_format,
|
||||||
|
"Size": book_data.uncompressed_size,
|
||||||
|
"Url": get_download_url_for_book(book, book_data.format),
|
||||||
|
# The Kobo forma accepts platforms: (Generic, Android)
|
||||||
|
"Platform": "Generic",
|
||||||
|
# "DrmType": "None", # Not required
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
book_uuid = book.uuid
|
||||||
|
metadata = {
|
||||||
|
"Categories": ["00000000-0000-0000-0000-000000000001",],
|
||||||
|
"Contributors": get_author(book),
|
||||||
|
"CoverImageId": book_uuid,
|
||||||
|
"CrossRevisionId": book_uuid,
|
||||||
|
"CurrentDisplayPrice": {"CurrencyCode": "USD", "TotalAmount": 0},
|
||||||
|
"CurrentLoveDisplayPrice": {"TotalAmount": 0},
|
||||||
|
"Description": get_description(book),
|
||||||
|
"DownloadUrls": download_urls,
|
||||||
|
"EntitlementId": book_uuid,
|
||||||
|
"ExternalIds": [],
|
||||||
|
"Genre": "00000000-0000-0000-0000-000000000001",
|
||||||
|
"IsEligibleForKoboLove": False,
|
||||||
|
"IsInternetArchive": False,
|
||||||
|
"IsPreOrder": False,
|
||||||
|
"IsSocialEnabled": True,
|
||||||
|
"Language": "en",
|
||||||
|
"PhoneticPronunciations": {},
|
||||||
|
"PublicationDate": book.pubdate,
|
||||||
|
"Publisher": {"Imprint": "", "Name": get_publisher(book),},
|
||||||
|
"RevisionId": book_uuid,
|
||||||
|
"Title": book.title,
|
||||||
|
"WorkId": book_uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
if get_series(book):
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
name = get_series(book).encode("utf-8")
|
||||||
|
else:
|
||||||
|
name = get_series(book)
|
||||||
|
metadata["Series"] = {
|
||||||
|
"Name": get_series(book),
|
||||||
|
"Number": book.series_index,
|
||||||
|
"NumberFloat": float(book.series_index),
|
||||||
|
# Get a deterministic id based on the series name.
|
||||||
|
"Id": uuid.uuid3(uuid.NAMESPACE_DNS, name),
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def reading_state(book):
|
||||||
|
# TODO: Implement
|
||||||
|
reading_state = {
|
||||||
|
# "StatusInfo": {
|
||||||
|
# "LastModified": get_single_cc_value(book, "lastreadtimestamp"),
|
||||||
|
# "Status": get_single_cc_value(book, "reading_status"),
|
||||||
|
# }
|
||||||
|
# TODO: CurrentBookmark, Location
|
||||||
|
}
|
||||||
|
return reading_state
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/<book_uuid>/image.jpg")
|
||||||
|
@requires_kobo_auth
|
||||||
|
def HandleCoverImageRequest(book_uuid):
|
||||||
|
log.debug("Cover request received for book %s" % book_uuid)
|
||||||
|
book_cover = helper.get_book_cover_with_uuid(
|
||||||
|
book_uuid, use_generic_cover_on_failure=False
|
||||||
|
)
|
||||||
|
if not book_cover:
|
||||||
|
if config.config_kobo_proxy:
|
||||||
|
return redirect(get_store_url_for_current_request(), 307)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
return book_cover
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("")
|
||||||
|
def TopLevelEndpoint():
|
||||||
|
return make_response(jsonify({}))
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Implement the following routes
|
||||||
|
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||||
|
@kobo.route("/v1/library/<book_uuid>/state", methods=["PUT"])
|
||||||
|
@kobo.route("/v1/library/tags", methods=["POST"])
|
||||||
|
@kobo.route("/v1/library/tags/<shelf_name>", methods=["POST"])
|
||||||
|
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE"])
|
||||||
|
def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_id=None):
|
||||||
|
log.debug("Alternative Request received:")
|
||||||
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Implement the following routes
|
||||||
|
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/user/recommendations", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
||||||
|
def HandleUserRequest(dummy=None):
|
||||||
|
log.debug("Unimplemented User Request received: %s", request.base_url)
|
||||||
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
||||||
|
def HandleProductsRequest(dummy=None):
|
||||||
|
log.debug("Unimplemented Products Request received: %s", request.base_url)
|
||||||
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.app_errorhandler(404)
|
||||||
|
def handle_404(err):
|
||||||
|
# This handler acts as a catch-all for endpoints that we don't have an interest in
|
||||||
|
# implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc)
|
||||||
|
log.debug("Unknown Request received: %s", request.base_url)
|
||||||
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
|
def make_calibre_web_auth_response():
|
||||||
|
# As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for
|
||||||
|
# authentation (nor for authorization). We return a dummy response just to keep the device happy.
|
||||||
|
content = request.get_json()
|
||||||
|
AccessToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
||||||
|
RefreshToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"AccessToken": AccessToken,
|
||||||
|
"RefreshToken": RefreshToken,
|
||||||
|
"TokenType": "Bearer",
|
||||||
|
"TrackingId": str(uuid.uuid4()),
|
||||||
|
"UserKey": content['UserKey'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||||
|
@requires_kobo_auth
|
||||||
|
def HandleAuthRequest():
|
||||||
|
log.debug('Kobo Auth request')
|
||||||
|
if config.config_kobo_proxy:
|
||||||
|
try:
|
||||||
|
return redirect_or_proxy_request()
|
||||||
|
except:
|
||||||
|
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
|
||||||
|
return make_calibre_web_auth_response()
|
||||||
|
|
||||||
|
|
||||||
|
def make_calibre_web_init_response(calibre_web_url):
|
||||||
|
resources = NATIVE_KOBO_RESOURCES(calibre_web_url)
|
||||||
|
response = make_response(jsonify({"Resources": resources}))
|
||||||
|
response.headers["x-kobo-apitoken"] = "e30="
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/initialization")
|
||||||
|
@requires_kobo_auth
|
||||||
|
def HandleInitRequest():
|
||||||
|
log.info('Init')
|
||||||
|
|
||||||
|
if not current_app.wsgi_app.is_proxied:
|
||||||
|
log.debug('Kobo: Received unproxied request, changed request port to server port')
|
||||||
|
if request.environ['SERVER_NAME'] != '::':
|
||||||
|
calibre_web_url = "{url_scheme}://{url_base}:{url_port}".format(
|
||||||
|
url_scheme=request.environ['wsgi.url_scheme'],
|
||||||
|
url_base=request.environ['SERVER_NAME'],
|
||||||
|
url_port=config.config_port
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.debug('Kobo: Received unproxied request, on IPV6 host')
|
||||||
|
calibre_web_url = url_for("web.index", _external=True).strip("/")
|
||||||
|
else:
|
||||||
|
calibre_web_url = url_for("web.index", _external=True).strip("/")
|
||||||
|
|
||||||
|
if config.config_kobo_proxy:
|
||||||
|
try:
|
||||||
|
store_response = make_request_to_kobo_store()
|
||||||
|
|
||||||
|
store_response_json = store_response.json()
|
||||||
|
if "Resources" in store_response_json:
|
||||||
|
kobo_resources = store_response_json["Resources"]
|
||||||
|
# calibre_web_url = url_for("web.index", _external=True).strip("/")
|
||||||
|
kobo_resources["image_host"] = calibre_web_url
|
||||||
|
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
||||||
|
auth_token = kobo_auth.get_auth_token(),
|
||||||
|
book_uuid="{ImageId}"))
|
||||||
|
kobo_resources["image_url_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
||||||
|
auth_token = kobo_auth.get_auth_token(),
|
||||||
|
book_uuid="{ImageId}"))
|
||||||
|
|
||||||
|
return make_response(store_response_json, store_response.status_code)
|
||||||
|
except:
|
||||||
|
log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.")
|
||||||
|
|
||||||
|
return make_calibre_web_init_response(calibre_web_url)
|
||||||
|
|
||||||
|
|
||||||
|
def NATIVE_KOBO_RESOURCES(calibre_web_url):
|
||||||
|
return {
|
||||||
|
"account_page": "https://secure.kobobooks.com/profile",
|
||||||
|
"account_page_rakuten": "https://my.rakuten.co.jp/",
|
||||||
|
"add_entitlement": "https://storeapi.kobo.com/v1/library/{RevisionIds}",
|
||||||
|
"affiliaterequest": "https://storeapi.kobo.com/v1/affiliate",
|
||||||
|
"audiobook_subscription_orange_deal_inclusion_url": "https://authorize.kobo.com/inclusion",
|
||||||
|
"authorproduct_recommendations": "https://storeapi.kobo.com/v1/products/books/authors/recommendations",
|
||||||
|
"autocomplete": "https://storeapi.kobo.com/v1/products/autocomplete",
|
||||||
|
"blackstone_header": {"key": "x-amz-request-payer", "value": "requester"},
|
||||||
|
"book": "https://storeapi.kobo.com/v1/products/books/{ProductId}",
|
||||||
|
"book_detail_page": "https://store.kobobooks.com/{culture}/ebook/{slug}",
|
||||||
|
"book_detail_page_rakuten": "http://books.rakuten.co.jp/rk/{crossrevisionid}",
|
||||||
|
"book_landing_page": "https://store.kobobooks.com/ebooks",
|
||||||
|
"book_subscription": "https://storeapi.kobo.com/v1/products/books/subscriptions",
|
||||||
|
"categories": "https://storeapi.kobo.com/v1/categories",
|
||||||
|
"categories_page": "https://store.kobobooks.com/ebooks/categories",
|
||||||
|
"category": "https://storeapi.kobo.com/v1/categories/{CategoryId}",
|
||||||
|
"category_featured_lists": "https://storeapi.kobo.com/v1/categories/{CategoryId}/featured",
|
||||||
|
"category_products": "https://storeapi.kobo.com/v1/categories/{CategoryId}/products",
|
||||||
|
"checkout_borrowed_book": "https://storeapi.kobo.com/v1/library/borrow",
|
||||||
|
"configuration_data": "https://storeapi.kobo.com/v1/configuration",
|
||||||
|
"content_access_book": "https://storeapi.kobo.com/v1/products/books/{ProductId}/access",
|
||||||
|
"customer_care_live_chat": "https://v2.zopim.com/widget/livechat.html?key=Y6gwUmnu4OATxN3Tli4Av9bYN319BTdO",
|
||||||
|
"daily_deal": "https://storeapi.kobo.com/v1/products/dailydeal",
|
||||||
|
"deals": "https://storeapi.kobo.com/v1/deals",
|
||||||
|
"delete_entitlement": "https://storeapi.kobo.com/v1/library/{Ids}",
|
||||||
|
"delete_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}",
|
||||||
|
"delete_tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/items/delete",
|
||||||
|
"device_auth": "https://storeapi.kobo.com/v1/auth/device",
|
||||||
|
"device_refresh": "https://storeapi.kobo.com/v1/auth/refresh",
|
||||||
|
"dictionary_host": "https://kbdownload1-a.akamaihd.net",
|
||||||
|
"discovery_host": "https://discovery.kobobooks.com",
|
||||||
|
"eula_page": "https://www.kobo.com/termsofuse?style=onestore",
|
||||||
|
"exchange_auth": "https://storeapi.kobo.com/v1/auth/exchange",
|
||||||
|
"external_book": "https://storeapi.kobo.com/v1/products/books/external/{Ids}",
|
||||||
|
"facebook_sso_page": "https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://store.kobobooks.com/",
|
||||||
|
"featured_list": "https://storeapi.kobo.com/v1/products/featured/{FeaturedListId}",
|
||||||
|
"featured_lists": "https://storeapi.kobo.com/v1/products/featured",
|
||||||
|
"free_books_page": {
|
||||||
|
"EN": "https://www.kobo.com/{region}/{language}/p/free-ebooks",
|
||||||
|
"FR": "https://www.kobo.com/{region}/{language}/p/livres-gratuits",
|
||||||
|
"IT": "https://www.kobo.com/{region}/{language}/p/libri-gratuiti",
|
||||||
|
"NL": "https://www.kobo.com/{region}/{language}/List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg",
|
||||||
|
"PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis",
|
||||||
|
},
|
||||||
|
"fte_feedback": "https://storeapi.kobo.com/v1/products/ftefeedback",
|
||||||
|
"get_tests_request": "https://storeapi.kobo.com/v1/analytics/gettests",
|
||||||
|
"giftcard_epd_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem-ereader",
|
||||||
|
"giftcard_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem",
|
||||||
|
"help_page": "http://www.kobo.com/help",
|
||||||
|
"image_host": calibre_web_url,
|
||||||
|
"image_url_quality_template": unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
||||||
|
auth_token = kobo_auth.get_auth_token(),
|
||||||
|
book_uuid="{ImageId}")),
|
||||||
|
"image_url_template": unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
||||||
|
auth_token = kobo_auth.get_auth_token(),
|
||||||
|
book_uuid="{ImageId}")),
|
||||||
|
"kobo_audiobooks_enabled": "False",
|
||||||
|
"kobo_audiobooks_orange_deal_enabled": "False",
|
||||||
|
"kobo_audiobooks_subscriptions_enabled": "False",
|
||||||
|
"kobo_nativeborrow_enabled": "True",
|
||||||
|
"kobo_onestorelibrary_enabled": "False",
|
||||||
|
"kobo_redeem_enabled": "True",
|
||||||
|
"kobo_shelfie_enabled": "False",
|
||||||
|
"kobo_subscriptions_enabled": "False",
|
||||||
|
"kobo_superpoints_enabled": "False",
|
||||||
|
"kobo_wishlist_enabled": "True",
|
||||||
|
"library_book": "https://storeapi.kobo.com/v1/user/library/books/{LibraryItemId}",
|
||||||
|
"library_items": "https://storeapi.kobo.com/v1/user/library",
|
||||||
|
"library_metadata": "https://storeapi.kobo.com/v1/library/{Ids}/metadata",
|
||||||
|
"library_prices": "https://storeapi.kobo.com/v1/user/library/previews/prices",
|
||||||
|
"library_stack": "https://storeapi.kobo.com/v1/user/library/stacks/{LibraryItemId}",
|
||||||
|
"library_sync": "https://storeapi.kobo.com/v1/library/sync",
|
||||||
|
"love_dashboard_page": "https://store.kobobooks.com/{culture}/kobosuperpoints",
|
||||||
|
"love_points_redemption_page": "https://store.kobobooks.com/{culture}/KoboSuperPointsRedemption?productId={ProductId}",
|
||||||
|
"magazine_landing_page": "https://store.kobobooks.com/emagazines",
|
||||||
|
"notifications_registration_issue": "https://storeapi.kobo.com/v1/notifications/registration",
|
||||||
|
"oauth_host": "https://oauth.kobo.com",
|
||||||
|
"overdrive_account": "https://auth.overdrive.com/account",
|
||||||
|
"overdrive_library": "https://{libraryKey}.auth.overdrive.com/library",
|
||||||
|
"overdrive_library_finder_host": "https://libraryfinder.api.overdrive.com",
|
||||||
|
"overdrive_thunder_host": "https://thunder.api.overdrive.com",
|
||||||
|
"password_retrieval_page": "https://www.kobobooks.com/passwordretrieval.html",
|
||||||
|
"post_analytics_event": "https://storeapi.kobo.com/v1/analytics/event",
|
||||||
|
"privacy_page": "https://www.kobo.com/privacypolicy?style=onestore",
|
||||||
|
"product_nextread": "https://storeapi.kobo.com/v1/products/{ProductIds}/nextread",
|
||||||
|
"product_prices": "https://storeapi.kobo.com/v1/products/{ProductIds}/prices",
|
||||||
|
"product_recommendations": "https://storeapi.kobo.com/v1/products/{ProductId}/recommendations",
|
||||||
|
"product_reviews": "https://storeapi.kobo.com/v1/products/{ProductIds}/reviews",
|
||||||
|
"products": "https://storeapi.kobo.com/v1/products",
|
||||||
|
"provider_external_sign_in_page": "https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://store.kobobooks.com/",
|
||||||
|
"purchase_buy": "https://www.kobo.com/checkout/createpurchase/",
|
||||||
|
"purchase_buy_templated": "https://www.kobo.com/{culture}/checkout/createpurchase/{ProductId}",
|
||||||
|
"quickbuy_checkout": "https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout",
|
||||||
|
"quickbuy_create": "https://storeapi.kobo.com/v1/store/quickbuy/purchase",
|
||||||
|
"rating": "https://storeapi.kobo.com/v1/products/{ProductId}/rating/{Rating}",
|
||||||
|
"reading_state": "https://storeapi.kobo.com/v1/library/{Ids}/state",
|
||||||
|
"redeem_interstitial_page": "https://store.kobobooks.com",
|
||||||
|
"registration_page": "https://authorize.kobo.com/signup?returnUrl=http://store.kobobooks.com/",
|
||||||
|
"related_items": "https://storeapi.kobo.com/v1/products/{Id}/related",
|
||||||
|
"remaining_book_series": "https://storeapi.kobo.com/v1/products/books/series/{SeriesId}",
|
||||||
|
"rename_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}",
|
||||||
|
"review": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}",
|
||||||
|
"review_sentiment": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}/sentiment/{Sentiment}",
|
||||||
|
"shelfie_recommendations": "https://storeapi.kobo.com/v1/user/recommendations/shelfie",
|
||||||
|
"sign_in_page": "https://authorize.kobo.com/signin?returnUrl=http://store.kobobooks.com/",
|
||||||
|
"social_authorization_host": "https://social.kobobooks.com:8443",
|
||||||
|
"social_host": "https://social.kobobooks.com",
|
||||||
|
"stacks_host_productId": "https://store.kobobooks.com/collections/byproductid/",
|
||||||
|
"store_home": "www.kobo.com/{region}/{language}",
|
||||||
|
"store_host": "store.kobobooks.com",
|
||||||
|
"store_newreleases": "https://store.kobobooks.com/{culture}/List/new-releases/961XUjtsU0qxkFItWOutGA",
|
||||||
|
"store_search": "https://store.kobobooks.com/{culture}/Search?Query={query}",
|
||||||
|
"store_top50": "https://store.kobobooks.com/{culture}/ebooks/Top",
|
||||||
|
"tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/Items",
|
||||||
|
"tags": "https://storeapi.kobo.com/v1/library/tags",
|
||||||
|
"taste_profile": "https://storeapi.kobo.com/v1/products/tasteprofile",
|
||||||
|
"update_accessibility_to_preview": "https://storeapi.kobo.com/v1/library/{EntitlementIds}/preview",
|
||||||
|
"use_one_store": "False",
|
||||||
|
"user_loyalty_benefits": "https://storeapi.kobo.com/v1/user/loyalty/benefits",
|
||||||
|
"user_platform": "https://storeapi.kobo.com/v1/user/platform",
|
||||||
|
"user_profile": "https://storeapi.kobo.com/v1/user/profile",
|
||||||
|
"user_ratings": "https://storeapi.kobo.com/v1/user/ratings",
|
||||||
|
"user_recommendations": "https://storeapi.kobo.com/v1/user/recommendations",
|
||||||
|
"user_reviews": "https://storeapi.kobo.com/v1/user/reviews",
|
||||||
|
"user_wishlist": "https://storeapi.kobo.com/v1/user/wishlist",
|
||||||
|
"userguide_host": "https://kbdownload1-a.akamaihd.net",
|
||||||
|
"wishlist_page": "https://store.kobobooks.com/{region}/{language}/account/wishlist",
|
||||||
|
}
|
163
cps/kobo_auth.py
Normal file
163
cps/kobo_auth.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
"""This module is used to control authentication/authorization of Kobo sync requests.
|
||||||
|
This module also includes research notes into the auth protocol used by Kobo devices.
|
||||||
|
|
||||||
|
Log-in:
|
||||||
|
When first booting a Kobo device the user must sign into a Kobo (or affiliate) account.
|
||||||
|
Upon successful sign-in, the user is redirected to
|
||||||
|
https://auth.kobobooks.com/CrossDomainSignIn?id=<some id>
|
||||||
|
which serves the following response:
|
||||||
|
<script type='text/javascript'>location.href='kobo://UserAuthenticated?userId=<redacted>&userKey<redacted>&email=<redacted>&returnUrl=https%3a%2f%2fwww.kobo.com';</script>.
|
||||||
|
And triggers the insertion of a userKey into the device's User table.
|
||||||
|
|
||||||
|
Together, the device's DeviceId and UserKey act as an *irrevocable* authentication
|
||||||
|
token to most (if not all) Kobo APIs. In fact, in most cases only the UserKey is
|
||||||
|
required to authorize the API call.
|
||||||
|
|
||||||
|
Changing Kobo password *does not* invalidate user keys! This is apparently a known
|
||||||
|
issue for a few years now https://www.mobileread.com/forums/showpost.php?p=3476851&postcount=13
|
||||||
|
(although this poster hypothesised that Kobo could blacklist a DeviceId, many endpoints
|
||||||
|
will still grant access given the userkey.)
|
||||||
|
|
||||||
|
Official Kobo Store Api authorization:
|
||||||
|
* For most of the endpoints we care about (sync, metadata, tags, etc), the userKey is
|
||||||
|
passed in the x-kobo-userkey header, and is sufficient to authorize the API call.
|
||||||
|
* Some endpoints (e.g: AnnotationService) instead make use of Bearer tokens pass through
|
||||||
|
an authorization header. To get a BearerToken, the device makes a POST request to the
|
||||||
|
v1/auth/device endpoint with the secret UserKey and the device's DeviceId.
|
||||||
|
* The book download endpoint passes an auth token as a URL param instead of a header.
|
||||||
|
|
||||||
|
Our implementation:
|
||||||
|
We pretty much ignore all of the above. To authenticate the user, we generate a random
|
||||||
|
and unique token that they append to the CalibreWeb Url when setting up the api_store
|
||||||
|
setting on the device.
|
||||||
|
Thus, every request from the device to the api_store will hit CalibreWeb with the
|
||||||
|
auth_token in the url (e.g: https://mylibrary.com/<auth_token>/v1/library/sync).
|
||||||
|
In addition, once authenticated we also set the login cookie on the response that will
|
||||||
|
be sent back for the duration of the session to authorize subsequent API calls (in
|
||||||
|
particular calls to non-Kobo specific endpoints such as the CalibreWeb book download).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from binascii import hexlify
|
||||||
|
from datetime import datetime
|
||||||
|
from os import urandom
|
||||||
|
import os
|
||||||
|
|
||||||
|
from flask import g, Blueprint, url_for, abort, request
|
||||||
|
from flask_login import login_user, login_required
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
from . import logger, ub, lm
|
||||||
|
from .web import render_title_template
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import wraps
|
||||||
|
except ImportError:
|
||||||
|
pass # We're not using Python 3
|
||||||
|
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def register_url_value_preprocessor(kobo):
|
||||||
|
@kobo.url_value_preprocessor
|
||||||
|
def pop_auth_token(endpoint, values):
|
||||||
|
g.auth_token = values.pop("auth_token")
|
||||||
|
|
||||||
|
|
||||||
|
def disable_failed_auth_redirect_for_blueprint(bp):
|
||||||
|
lm.blueprint_login_views[bp.name] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_token():
|
||||||
|
if "auth_token" in g:
|
||||||
|
return g.get("auth_token")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def requires_kobo_auth(f):
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
auth_token = get_auth_token()
|
||||||
|
if auth_token is not None:
|
||||||
|
user = (
|
||||||
|
ub.session.query(ub.User)
|
||||||
|
.join(ub.RemoteAuthToken)
|
||||||
|
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if user is not None:
|
||||||
|
login_user(user)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
log.debug("Received Kobo request without a recognizable auth token.")
|
||||||
|
return abort(401)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
||||||
|
|
||||||
|
|
||||||
|
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||||
|
@login_required
|
||||||
|
def generate_auth_token(user_id):
|
||||||
|
host = ':'.join(request.host.rsplit(':')[0:-1])
|
||||||
|
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'):
|
||||||
|
warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device')
|
||||||
|
return render_title_template(
|
||||||
|
"generate_kobo_auth_url.html",
|
||||||
|
title=_(u"Kobo Set-up"),
|
||||||
|
warning = warning
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||||
|
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
|
||||||
|
ub.RemoteAuthToken.user_id == user_id
|
||||||
|
).filter(ub.RemoteAuthToken.token_type==1).first()
|
||||||
|
|
||||||
|
if not auth_token:
|
||||||
|
auth_token = ub.RemoteAuthToken()
|
||||||
|
auth_token.user_id = user_id
|
||||||
|
auth_token.expiration = datetime.max
|
||||||
|
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
|
||||||
|
auth_token.token_type = 1
|
||||||
|
|
||||||
|
ub.session.add(auth_token)
|
||||||
|
ub.session.commit()
|
||||||
|
return render_title_template(
|
||||||
|
"generate_kobo_auth_url.html",
|
||||||
|
title=_(u"Kobo Set-up"),
|
||||||
|
kobo_auth_url=url_for(
|
||||||
|
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
|
||||||
|
),
|
||||||
|
warning = False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@kobo_auth.route("/deleteauthtoken/<int:user_id>")
|
||||||
|
@login_required
|
||||||
|
def delete_auth_token(user_id):
|
||||||
|
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||||
|
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||||
|
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||||
|
ub.session.commit()
|
||||||
|
return ""
|
@ -59,10 +59,13 @@ class ReverseProxied(object):
|
|||||||
|
|
||||||
def __init__(self, application):
|
def __init__(self, application):
|
||||||
self.app = application
|
self.app = application
|
||||||
|
self.proxied = False
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
|
self.proxied = False
|
||||||
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
||||||
if script_name:
|
if script_name:
|
||||||
|
self.proxied = True
|
||||||
environ['SCRIPT_NAME'] = script_name
|
environ['SCRIPT_NAME'] = script_name
|
||||||
path_info = environ.get('PATH_INFO', '')
|
path_info = environ.get('PATH_INFO', '')
|
||||||
if path_info and path_info.startswith(script_name):
|
if path_info and path_info.startswith(script_name):
|
||||||
@ -75,3 +78,7 @@ class ReverseProxied(object):
|
|||||||
if servr:
|
if servr:
|
||||||
environ['HTTP_HOST'] = servr
|
environ['HTTP_HOST'] = servr
|
||||||
return self.app(environ, start_response)
|
return self.app(environ, start_response)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_proxied(self):
|
||||||
|
return self.proxied
|
||||||
|
@ -55,6 +55,7 @@ class WebServer(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
signal.signal(signal.SIGINT, self._killServer)
|
signal.signal(signal.SIGINT, self._killServer)
|
||||||
signal.signal(signal.SIGTERM, self._killServer)
|
signal.signal(signal.SIGTERM, self._killServer)
|
||||||
|
signal.signal(signal.SIGQUIT, self._killServer)
|
||||||
|
|
||||||
self.wsgiserver = None
|
self.wsgiserver = None
|
||||||
self.access_logger = None
|
self.access_logger = None
|
||||||
|
148
cps/services/SyncToken.py
Normal file
148
cps/services/SyncToken.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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 sys
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from jsonschema import validate, exceptions, __version__
|
||||||
|
from datetime import datetime
|
||||||
|
try:
|
||||||
|
from urllib import unquote
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from flask import json
|
||||||
|
from .. import logger as log
|
||||||
|
|
||||||
|
|
||||||
|
def b64encode_json(json_data):
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
return b64encode(json.dumps(json_data))
|
||||||
|
else:
|
||||||
|
return b64encode(json.dumps(json_data).encode())
|
||||||
|
|
||||||
|
|
||||||
|
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
|
||||||
|
def to_epoch_timestamp(datetime_object):
|
||||||
|
return (datetime_object - datetime(1970, 1, 1)).total_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
class SyncToken():
|
||||||
|
""" The SyncToken is used to persist state accross requests.
|
||||||
|
When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
|
||||||
|
As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
books_last_created: Datetime representing the newest book that the device knows about.
|
||||||
|
books_last_modified: Datetime representing the last modified book that the device knows about.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
|
||||||
|
VERSION = "1-0-0"
|
||||||
|
MIN_VERSION = "1-0-0"
|
||||||
|
|
||||||
|
token_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"version": {"type": "string"}, "data": {"type": "object"},},
|
||||||
|
}
|
||||||
|
# This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
|
||||||
|
# A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db.
|
||||||
|
data_schema_v1 = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"raw_kobo_store_token": {"type": "string"},
|
||||||
|
"books_last_modified": {"type": "string"},
|
||||||
|
"books_last_created": {"type": "string"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
raw_kobo_store_token="",
|
||||||
|
books_last_created=datetime.min,
|
||||||
|
books_last_modified=datetime.min,
|
||||||
|
):
|
||||||
|
self.raw_kobo_store_token = raw_kobo_store_token
|
||||||
|
self.books_last_created = books_last_created
|
||||||
|
self.books_last_modified = books_last_modified
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_headers(headers):
|
||||||
|
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
|
||||||
|
if sync_token_header == "":
|
||||||
|
return SyncToken()
|
||||||
|
|
||||||
|
# On the first sync from a Kobo device, we may receive the SyncToken
|
||||||
|
# from the official Kobo store. Without digging too deep into it, that
|
||||||
|
# token is of the form [b64encoded blob].[b64encoded blob 2]
|
||||||
|
if "." in sync_token_header:
|
||||||
|
return SyncToken(raw_kobo_store_token=sync_token_header)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sync_token_json = json.loads(
|
||||||
|
b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4))
|
||||||
|
)
|
||||||
|
validate(sync_token_json, SyncToken.token_schema)
|
||||||
|
if sync_token_json["version"] < SyncToken.MIN_VERSION:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
data_json = sync_token_json["data"]
|
||||||
|
validate(sync_token_json, SyncToken.data_schema_v1)
|
||||||
|
except (exceptions.ValidationError, ValueError) as e:
|
||||||
|
log.error("Sync token contents do not follow the expected json schema.")
|
||||||
|
return SyncToken()
|
||||||
|
|
||||||
|
raw_kobo_store_token = data_json["raw_kobo_store_token"]
|
||||||
|
try:
|
||||||
|
books_last_modified = datetime.utcfromtimestamp(
|
||||||
|
data_json["books_last_modified"]
|
||||||
|
)
|
||||||
|
books_last_created = datetime.utcfromtimestamp(
|
||||||
|
data_json["books_last_created"]
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||||
|
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||||
|
|
||||||
|
return SyncToken(
|
||||||
|
raw_kobo_store_token=raw_kobo_store_token,
|
||||||
|
books_last_created=books_last_created,
|
||||||
|
books_last_modified=books_last_modified,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_kobo_store_header(self, store_headers):
|
||||||
|
store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token)
|
||||||
|
|
||||||
|
def merge_from_store_response(self, store_response):
|
||||||
|
self.raw_kobo_store_token = store_response.headers.get(
|
||||||
|
SyncToken.SYNC_TOKEN_HEADER, ""
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_headers(self, headers):
|
||||||
|
headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
|
||||||
|
|
||||||
|
def build_sync_token(self):
|
||||||
|
token = {
|
||||||
|
"version": SyncToken.VERSION,
|
||||||
|
"data": {
|
||||||
|
"raw_kobo_store_token": self.raw_kobo_store_token,
|
||||||
|
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
|
||||||
|
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return b64encode_json(token)
|
@ -35,4 +35,10 @@ except ImportError as err:
|
|||||||
log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err)
|
log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err)
|
||||||
ldap = None
|
ldap = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import SyncToken as SyncToken
|
||||||
|
kobo = True
|
||||||
|
except ImportError as err:
|
||||||
|
log.debug("cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
|
||||||
|
kobo = None
|
||||||
|
SyncToken = None
|
||||||
|
17
cps/shelf.py
17
cps/shelf.py
@ -293,6 +293,8 @@ def show_shelf(shelf_type, shelf_id):
|
|||||||
if cur_book:
|
if cur_book:
|
||||||
result.append(cur_book)
|
result.append(cur_book)
|
||||||
else:
|
else:
|
||||||
|
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
|
||||||
|
if not cur_book:
|
||||||
log.info('Not existing book %s in %s deleted', book.book_id, shelf)
|
log.info('Not existing book %s in %s deleted', book.book_id, shelf)
|
||||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete()
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete()
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
@ -329,9 +331,18 @@ def order_shelf(shelf_id):
|
|||||||
.order_by(ub.BookShelf.order.asc()).all()
|
.order_by(ub.BookShelf.order.asc()).all()
|
||||||
for book in books_in_shelf2:
|
for book in books_in_shelf2:
|
||||||
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).filter(common_filters()).first()
|
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).filter(common_filters()).first()
|
||||||
result.append(cur_book)
|
if cur_book:
|
||||||
#books_list = [ b.book_id for b in books_in_shelf2]
|
result.append({'title':cur_book.title,
|
||||||
#result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all()
|
'id':cur_book.id,
|
||||||
|
'author':cur_book.authors,
|
||||||
|
'series':cur_book.series,
|
||||||
|
'series_index':cur_book.series_index})
|
||||||
|
else:
|
||||||
|
cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
|
||||||
|
result.append({'title':_('Hidden Book'),
|
||||||
|
'id':cur_book.id,
|
||||||
|
'author':[],
|
||||||
|
'series':[]})
|
||||||
return render_title_template('shelf_order.html', entries=result,
|
return render_title_template('shelf_order.html', entries=result,
|
||||||
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
||||||
shelf=shelf, page="shelforder")
|
shelf=shelf, page="shelforder")
|
||||||
|
@ -26,7 +26,7 @@ html.http-error {
|
|||||||
|
|
||||||
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
|
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
|
||||||
body { margin-bottom: 40px;}
|
body { margin-bottom: 40px;}
|
||||||
a{color: #45b29d}a:hover{color: #444;}
|
a{color: #45b29d} /*a:hover{color: #444;}*/
|
||||||
.navigation .nav-head{text-transform:uppercase;color:#999;margin:20px 0}.navigation .nav-head:nth-child(1n+2){border-top:1px solid #ccc;padding-top:20px}
|
.navigation .nav-head{text-transform:uppercase;color:#999;margin:20px 0}.navigation .nav-head:nth-child(1n+2){border-top:1px solid #ccc;padding-top:20px}
|
||||||
.navigation li a{color:#444;text-decoration:none;display:block;padding:10px}.navigation li a:hover{background:rgba(153,153,153,0.4);border-radius:5px}
|
.navigation li a{color:#444;text-decoration:none;display:block;padding:10px}.navigation li a:hover{background:rgba(153,153,153,0.4);border-radius:5px}
|
||||||
.navigation li a span{margin-right:10px}
|
.navigation li a span{margin-right:10px}
|
||||||
@ -82,6 +82,12 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
|
|||||||
.spinner {margin:0 41%;}
|
.spinner {margin:0 41%;}
|
||||||
.spinner2 {margin:0 41%;}
|
.spinner2 {margin:0 41%;}
|
||||||
|
|
||||||
|
table .bg-dark-danger {background-color: #d9534f; color: #fff;}
|
||||||
|
table .bg-dark-danger a {color: #fff;}
|
||||||
|
table .bg-dark-danger:hover {background-color: #c9302c;}
|
||||||
|
table .bg-primary:hover {background-color: #1C5484;}
|
||||||
|
table .bg-primary a {color: #fff;}
|
||||||
|
|
||||||
.block-label {display: block;}
|
.block-label {display: block;}
|
||||||
.fake-input {position: absolute; pointer-events: none; top: 0;}
|
.fake-input {position: absolute; pointer-events: none; top: 0;}
|
||||||
|
|
||||||
|
@ -228,6 +228,41 @@ $(function() {
|
|||||||
$(this).find(".modal-body").html("...");
|
$(this).find(".modal-body").html("...");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#modal_kobo_token")
|
||||||
|
.on("show.bs.modal", function(e) {
|
||||||
|
var $modalBody = $(this).find(".modal-body");
|
||||||
|
|
||||||
|
// Prevent static assets from loading multiple times
|
||||||
|
var useCache = function(options) {
|
||||||
|
options.async = true;
|
||||||
|
options.cache = true;
|
||||||
|
};
|
||||||
|
preFilters.add(useCache);
|
||||||
|
|
||||||
|
$.get(e.relatedTarget.href).done(function(content) {
|
||||||
|
$modalBody.html(content);
|
||||||
|
preFilters.remove(useCache);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("hidden.bs.modal", function() {
|
||||||
|
$(this).find(".modal-body").html("...");
|
||||||
|
$("#config_delete_kobo_token").show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btndeletetoken").click(function() {
|
||||||
|
//get data-id attribute of the clicked element
|
||||||
|
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length-1].src;
|
||||||
|
var path = src.substring(0,src.lastIndexOf("/"));
|
||||||
|
// var domainId = $(this).value("domainId");
|
||||||
|
$.ajax({
|
||||||
|
method:"get",
|
||||||
|
url: path + "/../../kobo_auth/deleteauthtoken/" + this.value,
|
||||||
|
});
|
||||||
|
$("#modalDeleteToken").modal("hide");
|
||||||
|
$("#config_delete_kobo_token").hide();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
$(window).resize(function() {
|
$(window).resize(function() {
|
||||||
$(".discover .row").isotope("layout");
|
$(".discover .row").isotope("layout");
|
||||||
});
|
});
|
||||||
|
@ -93,6 +93,116 @@ $(function() {
|
|||||||
var domainId = $(e.relatedTarget).data("domain-id");
|
var domainId = $(e.relatedTarget).data("domain-id");
|
||||||
$(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
|
$(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#restrictModal').on('hidden.bs.modal', function () {
|
||||||
|
// Destroy table and remove hooks for buttons
|
||||||
|
$("#restrict-elements-table").unbind();
|
||||||
|
$('#restrict-elements-table').bootstrapTable('destroy');
|
||||||
|
$("[id^=submit_]").unbind();
|
||||||
|
$('#h1').addClass('hidden');
|
||||||
|
$('#h2').addClass('hidden');
|
||||||
|
$('#h3').addClass('hidden');
|
||||||
|
$('#h4').addClass('hidden');
|
||||||
|
});
|
||||||
|
function startTable(type){
|
||||||
|
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length-1].src;
|
||||||
|
var path = src.substring(0,src.lastIndexOf("/"));
|
||||||
|
$("#restrict-elements-table").bootstrapTable({
|
||||||
|
formatNoMatches: function () {
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
url: path + "/../../ajax/listrestriction/" + type,
|
||||||
|
rowStyle: function(row, index) {
|
||||||
|
console.log('Reihe :' + row + ' Index :'+ index);
|
||||||
|
if (row.id.charAt(0) == 'a') {
|
||||||
|
return {classes: 'bg-primary'}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {classes: 'bg-dark-danger'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickCell: function (field, value, row, $element) {
|
||||||
|
if(field == 3){
|
||||||
|
console.log("element")
|
||||||
|
$.ajax ({
|
||||||
|
type: 'Post',
|
||||||
|
data: 'id=' + row.id + '&type=' + row.type + "&Element=" + row.Element,
|
||||||
|
url: path + "/../../ajax/deleterestriction/" + type,
|
||||||
|
async: true,
|
||||||
|
timeout: 900,
|
||||||
|
success:function(data) {
|
||||||
|
$.ajax({
|
||||||
|
method:"get",
|
||||||
|
url: path + "/../../ajax/listrestriction/"+type,
|
||||||
|
async: true,
|
||||||
|
timeout: 900,
|
||||||
|
success:function(data) {
|
||||||
|
$("#restrict-elements-table").bootstrapTable("load", data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
striped: false
|
||||||
|
});
|
||||||
|
$("#restrict-elements-table").removeClass('table-hover');
|
||||||
|
$("#restrict-elements-table").on('editable-save.bs.table', function (e, field, row, old, $el) {
|
||||||
|
console.log("Hallo");
|
||||||
|
$.ajax({
|
||||||
|
url: path + "/../../ajax/editrestriction/"+type,
|
||||||
|
type: 'Post',
|
||||||
|
data: row //$(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$("[id^=submit_]").click(function(event) {
|
||||||
|
// event.stopPropagation();
|
||||||
|
// event.preventDefault();
|
||||||
|
$(this)[0].blur();
|
||||||
|
console.log($(this)[0].name);
|
||||||
|
$.ajax({
|
||||||
|
url: path + "/../../ajax/addrestriction/"+type,
|
||||||
|
type: 'Post',
|
||||||
|
data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
|
||||||
|
success: function () {
|
||||||
|
$.ajax ({
|
||||||
|
method:"get",
|
||||||
|
url: path + "/../../ajax/listrestriction/"+type,
|
||||||
|
async: true,
|
||||||
|
timeout: 900,
|
||||||
|
success:function(data) {
|
||||||
|
$("#restrict-elements-table").bootstrapTable("load", data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('#get_column_values').on('click',function()
|
||||||
|
{
|
||||||
|
startTable(1);
|
||||||
|
$('#h2').removeClass('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#get_tags').on('click',function()
|
||||||
|
{
|
||||||
|
startTable(0);
|
||||||
|
$('#h1').removeClass('hidden');
|
||||||
|
});
|
||||||
|
$('#get_user_column_values').on('click',function()
|
||||||
|
{
|
||||||
|
startTable(3);
|
||||||
|
$('#h4').removeClass('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#get_user_tags').on('click',function()
|
||||||
|
{
|
||||||
|
startTable(2);
|
||||||
|
$(this)[0].blur();
|
||||||
|
$('#h3').removeClass('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Function for deleting domain restrictions */
|
/* Function for deleting domain restrictions */
|
||||||
@ -104,3 +214,12 @@ function TableActions (value, row, index) {
|
|||||||
"</a>"
|
"</a>"
|
||||||
].join("");
|
].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Function for deleting domain restrictions */
|
||||||
|
function RestrictionActions (value, row, index) {
|
||||||
|
return [
|
||||||
|
"<div class=\"danger remove\" data-restriction-id=\"" + row.id + "\" title=\"Remove\">",
|
||||||
|
"<i class=\"glyphicon glyphicon-trash\"></i>",
|
||||||
|
"</div>"
|
||||||
|
].join("");
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="discover">
|
<div class="discover" xmlns:text-indent="http://www.w3.org/1999/xhtml">
|
||||||
<h2>{{title}}</h2>
|
<h2>{{title}}</h2>
|
||||||
<form role="form" method="POST" autocomplete="off">
|
<form role="form" method="POST" autocomplete="off">
|
||||||
<div class="panel-group">
|
<div class="panel-group">
|
||||||
@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if show_back_button %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
@ -169,6 +169,18 @@
|
|||||||
<input type="checkbox" id="config_remote_login" name="config_remote_login" {% if config.config_remote_login %}checked{% endif %}>
|
<input type="checkbox" id="config_remote_login" name="config_remote_login" {% if config.config_remote_login %}checked{% endif %}>
|
||||||
<label for="config_remote_login">{{_('Enable remote login ("magic link")')}}</label>
|
<label for="config_remote_login">{{_('Enable remote login ("magic link")')}}</label>
|
||||||
</div>
|
</div>
|
||||||
|
{% if feature_support['kobo'] %}
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="config_kobo_sync" name="config_kobo_sync" data-control="kobo-settings" {% if config.config_kobo_sync %}checked{% endif %}>
|
||||||
|
<label for="config_kobo_sync">{{_('Enable Kobo sync')}}</label>
|
||||||
|
</div>
|
||||||
|
<div data-related="kobo-settings">
|
||||||
|
<div class="form-group" style="text-indent:10px;">
|
||||||
|
<input type="checkbox" id="config_kobo_proxy" name="config_kobo_proxy" {% if config.config_kobo_proxy %}checked{% endif %}>
|
||||||
|
<label for="config_kobo_proxy">{{_('Proxy unknown requests to Kobo Store')}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if feature_support['goodreads'] %}
|
{% if feature_support['goodreads'] %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="checkbox" id="config_use_goodreads" name="config_use_goodreads" data-control="goodreads-settings" {% if config.config_use_goodreads %}checked{% endif %}>
|
<input type="checkbox" id="config_use_goodreads" name="config_use_goodreads" data-control="goodreads-settings" {% if config.config_use_goodreads %}checked{% endif %}>
|
||||||
@ -322,11 +334,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
{% if not show_login_button %}
|
||||||
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
|
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||||
|
{% endif %}
|
||||||
{% if show_back_button %}
|
{% if show_back_button %}
|
||||||
<a href="{{ url_for('admin.admin') }}" class="btn btn-default">{{_('Back')}}</a>
|
<a href="{{ url_for('admin.admin') }}" class="btn btn-default">{{_('Back')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
{% block header %}
|
||||||
|
<link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
|
||||||
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h2>{{title}}</h2>
|
<h2>{{title}}</h2>
|
||||||
@ -51,16 +55,19 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="config_restricted_column">{{_('View restriction based on Calibre column')}}</label>
|
||||||
|
<select name="config_restricted_column" id="config_restricted_column" class="form-control">
|
||||||
|
<option value="0" {% if conf.config_restricted_column == 0 %}selected{% endif %}>{{ _('None') }}</option>
|
||||||
|
{% for restrictColumn in restrictColumns %}
|
||||||
|
<option value="{{ restrictColumn.id }}" {% if restrictColumn.id == conf.config_restricted_column %}selected{% endif %}>{{ restrictColumn.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label>
|
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label>
|
||||||
<input type="text" class="form-control" name="config_title_regex" id="config_title_regex" value="{% if conf.config_title_regex != None %}{{ conf.config_title_regex }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" name="config_title_regex" id="config_title_regex" value="{% if conf.config_title_regex != None %}{{ conf.config_title_regex }}{% endif %}" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label for="config_mature_content_tags">{{_('Tags for Mature Content')}}</label>
|
|
||||||
<input type="text" class="form-control" name="config_mature_content_tags" id="config_mature_content_tags"
|
|
||||||
value="{% if conf.config_mature_content_tags != None%}{{ conf.config_mature_content_tags }}{% endif %}"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -134,11 +141,8 @@
|
|||||||
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if conf.show_detail_random() %}checked{% endif %}>
|
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if conf.show_detail_random() %}checked{% endif %}>
|
||||||
<label for="Show_detail_random">{{_('Show random books in detail view')}}</label>
|
<label for="Show_detail_random">{{_('Show random books in detail view')}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<a href="#" id="get_tags" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/denied Tags')}}</a>
|
||||||
<input type="checkbox" name="Show_mature_content" id="Show_mature_content" {% if conf.show_mature_content() %}checked{% endif %}>
|
<a href="#" id="get_column_values" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/denied custom column values')}}</a>
|
||||||
<label for="Show_mature_content">{{_('Show mature content')}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -149,6 +153,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block modal %}
|
||||||
|
{{ restrict_modal() }}
|
||||||
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$('.collapse').on('shown.bs.collapse', function(){
|
$('.collapse').on('shown.bs.collapse', function(){
|
||||||
@ -157,4 +164,8 @@
|
|||||||
$(this).parent().find(".glyphicon-minus").removeClass("glyphicon-minus").addClass("glyphicon-plus");
|
$(this).parent().find(".glyphicon-minus").removeClass("glyphicon-minus").addClass("glyphicon-plus");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
14
cps/templates/generate_kobo_auth_url.html
Normal file
14
cps/templates/generate_kobo_auth_url.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "fragment.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="well">
|
||||||
|
<p>
|
||||||
|
{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% if not warning %}{{_('api_endpoint=')}}{{kobo_auth_url}}{% else %}{{warning}}{% endif %}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{_('Please note that every visit to this current page invalidates any previously generated Authentication url for this user.')}}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,3 +1,4 @@
|
|||||||
|
{% from 'modal_restriction.html' import restrict_modal %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ g.user.locale }}">
|
<html lang="{{ g.user.locale }}">
|
||||||
<head>
|
<head>
|
||||||
@ -11,6 +12,7 @@
|
|||||||
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
|
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
|
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
|
||||||
|
{% block header %}{% endblock %}
|
||||||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen">
|
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen">
|
||||||
<link href="{{ url_for('static', filename='css/upload.css') }}" rel="stylesheet" media="screen">
|
<link href="{{ url_for('static', filename='css/upload.css') }}" rel="stylesheet" media="screen">
|
||||||
{% if g.current_theme == 1 %}
|
{% if g.current_theme == 1 %}
|
||||||
@ -22,8 +24,6 @@
|
|||||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
|
|
||||||
{% block header %}{% endblock %}
|
|
||||||
</head>
|
</head>
|
||||||
<body class="{{ page }}" data-text="{{_('Home')}}" data-textback="{{_('Back')}}">
|
<body class="{{ page }}" data-text="{{_('Home')}}" data-textback="{{_('Back')}}">
|
||||||
<!-- Static navbar -->
|
<!-- Static navbar -->
|
||||||
|
39
cps/templates/modal_restriction.html
Normal file
39
cps/templates/modal_restriction.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{% macro restrict_modal() %}
|
||||||
|
<div class="modal fade" id="restrictModal" tabindex="-1" role="dialog" aria-labelledby="restrictModalLabel">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title hidden" id="h1">{{_('Select allowed/denied Tags')}}</h4>
|
||||||
|
<h4 class="modal-title hidden" id="h2">{{_('Select allowed/denied Custom Column values')}}</h4>
|
||||||
|
<h4 class="modal-title hidden" id="h3">{{_('Select allowed/denied Tags of user')}}</h4>
|
||||||
|
<h4 class="modal-title hidden" id="h4">{{_('Select allowed/denied Custom Column values of user')}}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<table class="table table-no-bordered" id="restrict-elements-table" data-id-field="id" data-show-header="false" data-editable-mode="inline">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-field="Element" id="Element" data-editable-type="text" data-editable="true" data-editable-title="{{_('Enter Tag')}}"></th>
|
||||||
|
<th data-field="type" id="type" data-visible="true"></th>
|
||||||
|
<th data-field="id" id="id" data-visible="false"></th>
|
||||||
|
<th data-align="right" data-formatter="RestrictionActions"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
<form id="add_restriction" action="" method="POST">
|
||||||
|
<div class="form-group required">
|
||||||
|
<label for="add_element">{{_('Add View Restriction')}}</label>
|
||||||
|
<input type="text" class="form-control" name="add_element" id="add_element" >
|
||||||
|
</div>
|
||||||
|
<div class="form-group required">
|
||||||
|
<input type="button" class="btn btn-default" value="{{_('Allow')}}" name="submit_allow" id="submit_allow" data-dismiss="static">
|
||||||
|
<input type="button" class="btn btn-default" value="{{_('Deny')}}" name="submit_deny" id="submit_restrict" data-dismiss="static">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" id="restrict_close" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
@ -57,7 +57,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if kobo_support and not new_user %}
|
||||||
|
<label>{{ _('Kobo Sync Token')}}</label>
|
||||||
|
<div class="form-group col">
|
||||||
|
<a class="btn btn-default" id="config_create_kobo_token" data-toggle="modal" data-target="#modal_kobo_token" data-remote="false" href="{{ url_for('kobo_auth.generate_auth_token', user_id=content.id) }}">{{_('Create/View')}}</a>
|
||||||
|
<div class="btn btn-danger" id="config_delete_kobo_token" data-toggle="modal" data-target="#modalDeleteToken" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
{% for element in sidebar %}
|
{% for element in sidebar %}
|
||||||
@ -73,6 +79,10 @@
|
|||||||
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
|
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
|
||||||
<label for="Show_detail_random">{{_('Show random books in detail view')}}</label>
|
<label for="Show_detail_random">{{_('Show random books in detail view')}}</label>
|
||||||
</div>
|
</div>
|
||||||
|
{% if ( g.user and g.user.role_admin() and not new_user ) %}
|
||||||
|
<a href="#" id="get_user_tags" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/denied Tags')}}</a>
|
||||||
|
<a href="#" id="get_user_column_values" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/denied custom column values')}}</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
{% if g.user and g.user.role_admin() and not profile %}
|
{% if g.user and g.user.role_admin() and not profile %}
|
||||||
@ -82,10 +92,6 @@
|
|||||||
<label for="admin_role">{{_('Admin user')}}</label>
|
<label for="admin_role">{{_('Admin user')}}</label>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-group">
|
|
||||||
<input type="checkbox" name="Show_mature_content" id="Show_mature_content" {% if content.mature_content %}checked{% endif %}>
|
|
||||||
<label for="Show_mature_content">{{_('Show mature content')}}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="checkbox" name="download_role" id="download_role" {% if content.role_download() %}checked{% endif %}>
|
<input type="checkbox" name="download_role" id="download_role" {% if content.role_download() %}checked{% endif %}>
|
||||||
<label for="download_role">{{_('Allow Downloads')}}</label>
|
<label for="download_role">{{_('Allow Downloads')}}</label>
|
||||||
@ -146,4 +152,42 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="modal_kobo_token" tabindex="-1" role="dialog" aria-labelledby="kobo_tokenModalLabel">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 class="modal-title" id="kobo_tokenModalLabel">{{_('Generate Kobo Auth URL')}}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">...</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modalDeleteToken" class="modal fade" role="dialog">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger">
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<p>{{_('Do you really want to delete the Kobo Token?')}}</p>
|
||||||
|
<button type="button" class="btn btn-danger" id="btndeletetoken" value="{{content.id}}">{{_('Delete')}}</button>
|
||||||
|
<button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Back')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block modal %}
|
||||||
|
{{ restrict_modal() }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
54
cps/ub.py
54
cps/ub.py
@ -156,6 +156,22 @@ class UserBase:
|
|||||||
def show_detail_random(self):
|
def show_detail_random(self):
|
||||||
return self.check_visibility(constants.DETAIL_RANDOM)
|
return self.check_visibility(constants.DETAIL_RANDOM)
|
||||||
|
|
||||||
|
def list_denied_tags(self):
|
||||||
|
mct = self.denied_tags.split(",")
|
||||||
|
return [t.strip() for t in mct]
|
||||||
|
|
||||||
|
def list_allowed_tags(self):
|
||||||
|
mct = self.allowed_tags.split(",")
|
||||||
|
return [t.strip() for t in mct]
|
||||||
|
|
||||||
|
def list_denied_column_values(self):
|
||||||
|
mct = self.denied_column_value.split(",")
|
||||||
|
return [t.strip() for t in mct]
|
||||||
|
|
||||||
|
def list_allowed_column_values(self):
|
||||||
|
mct = self.allowed_column_value.split(",")
|
||||||
|
return [t.strip() for t in mct]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<User %r>' % self.nickname
|
return '<User %r>' % self.nickname
|
||||||
|
|
||||||
@ -178,6 +194,11 @@ class User(UserBase, Base):
|
|||||||
sidebar_view = Column(Integer, default=1)
|
sidebar_view = Column(Integer, default=1)
|
||||||
default_language = Column(String(3), default="all")
|
default_language = Column(String(3), default="all")
|
||||||
mature_content = Column(Boolean, default=True)
|
mature_content = Column(Boolean, default=True)
|
||||||
|
denied_tags = Column(String, default="")
|
||||||
|
allowed_tags = Column(String, default="")
|
||||||
|
denied_column_value = Column(String, default="")
|
||||||
|
allowed_column_value = Column(String, default="")
|
||||||
|
remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic')
|
||||||
|
|
||||||
|
|
||||||
if oauth_support:
|
if oauth_support:
|
||||||
@ -213,9 +234,10 @@ class Anonymous(AnonymousUserMixin, UserBase):
|
|||||||
self.locale = data.locale
|
self.locale = data.locale
|
||||||
self.mature_content = data.mature_content
|
self.mature_content = data.mature_content
|
||||||
self.kindle_mail = data.kindle_mail
|
self.kindle_mail = data.kindle_mail
|
||||||
|
self.denied_tags = data.denied_tags
|
||||||
# settings = session.query(config).first()
|
self.allowed_tags = data.allowed_tags
|
||||||
# self.anon_browse = settings.config_anonbrowse
|
self.denied_column_value = data.denied_column_value
|
||||||
|
self.allowed_column_value = data.allowed_column_value
|
||||||
|
|
||||||
def role_admin(self):
|
def role_admin(self):
|
||||||
return False
|
return False
|
||||||
@ -311,6 +333,7 @@ class RemoteAuthToken(Base):
|
|||||||
user_id = Column(Integer, ForeignKey('user.id'))
|
user_id = Column(Integer, ForeignKey('user.id'))
|
||||||
verified = Column(Boolean, default=False)
|
verified = Column(Boolean, default=False)
|
||||||
expiration = Column(DateTime)
|
expiration = Column(DateTime)
|
||||||
|
token_type = Column(Integer, default=0)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.auth_token = (hexlify(os.urandom(4))).decode('utf-8')
|
self.auth_token = (hexlify(os.urandom(4))).decode('utf-8')
|
||||||
@ -342,6 +365,15 @@ def migrate_Database(session):
|
|||||||
conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER")
|
conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER")
|
||||||
conn.execute("update registration set 'allow' = 1")
|
conn.execute("update registration set 'allow' = 1")
|
||||||
session.commit()
|
session.commit()
|
||||||
|
try:
|
||||||
|
session.query(exists().where(RemoteAuthToken.token_type)).scalar()
|
||||||
|
session.commit()
|
||||||
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
|
conn = engine.connect()
|
||||||
|
conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0")
|
||||||
|
conn.execute("update remote_auth_token set 'token_type' = 0")
|
||||||
|
session.commit()
|
||||||
|
|
||||||
# Handle table exists, but no content
|
# Handle table exists, but no content
|
||||||
cnt = session.query(Registration).count()
|
cnt = session.query(Registration).count()
|
||||||
if not cnt:
|
if not cnt:
|
||||||
@ -378,12 +410,19 @@ def migrate_Database(session):
|
|||||||
'side_autor': constants.SIDEBAR_AUTHOR,
|
'side_autor': constants.SIDEBAR_AUTHOR,
|
||||||
'detail_random': constants.DETAIL_RANDOM})
|
'detail_random': constants.DETAIL_RANDOM})
|
||||||
session.commit()
|
session.commit()
|
||||||
try:
|
'''try:
|
||||||
session.query(exists().where(User.mature_content)).scalar()
|
session.query(exists().where(User.mature_content)).scalar()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
conn = engine.connect()
|
conn = engine.connect()
|
||||||
conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1")
|
conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1")'''
|
||||||
|
try:
|
||||||
|
session.query(exists().where(User.denied_tags)).scalar()
|
||||||
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
|
conn = engine.connect()
|
||||||
|
conn.execute("ALTER TABLE user ADD column `denied_tags` String DEFAULT ''")
|
||||||
|
conn.execute("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''")
|
||||||
|
conn.execute("ALTER TABLE user ADD column `denied_column_value` DEFAULT ''")
|
||||||
|
conn.execute("ALTER TABLE user ADD column `allowed_column_value` DEFAULT ''")
|
||||||
if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() is None:
|
if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() is None:
|
||||||
create_anonymous_user(session)
|
create_anonymous_user(session)
|
||||||
try:
|
try:
|
||||||
@ -424,7 +463,8 @@ def migrate_Database(session):
|
|||||||
def clean_database(session):
|
def clean_database(session):
|
||||||
# Remove expired remote login tokens
|
# Remove expired remote login tokens
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).delete()
|
session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\
|
||||||
|
filter(RemoteAuthToken.token_type !=1 ).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
41
cps/web.py
41
cps/web.py
@ -27,7 +27,7 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import traceback
|
import traceback
|
||||||
import sys
|
import binascii
|
||||||
|
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.dates import format_date
|
from babel.dates import format_date
|
||||||
@ -42,7 +42,7 @@ from werkzeug.exceptions import default_exceptions
|
|||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
from . import constants, config, logger, isoLanguages, services, worker
|
from . import constants, logger, isoLanguages, services, worker
|
||||||
from . import searched_ids, lm, babel, db, ub, config, get_locale, app
|
from . import searched_ids, lm, babel, db, ub, config, get_locale, app
|
||||||
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
|
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
|
||||||
from .helper import common_filters, get_search_results, fill_indexpage, speaking_language, check_valid_domain, \
|
from .helper import common_filters, get_search_results, fill_indexpage, speaking_language, check_valid_domain, \
|
||||||
@ -54,7 +54,8 @@ from .redirect import redirect_back
|
|||||||
|
|
||||||
feature_support = {
|
feature_support = {
|
||||||
'ldap': False, # bool(services.ldap),
|
'ldap': False, # bool(services.ldap),
|
||||||
'goodreads': bool(services.goodreads_support)
|
'goodreads': bool(services.goodreads_support),
|
||||||
|
'kobo': bool(services.kobo)
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -149,7 +150,7 @@ def load_user_from_auth_header(header_val):
|
|||||||
header_val = base64.b64decode(header_val).decode('utf-8')
|
header_val = base64.b64decode(header_val).decode('utf-8')
|
||||||
basic_username = header_val.split(':')[0]
|
basic_username = header_val.split(':')[0]
|
||||||
basic_password = header_val.split(':')[1]
|
basic_password = header_val.split(':')[1]
|
||||||
except (TypeError, UnicodeDecodeError):
|
except (TypeError, UnicodeDecodeError, binascii.Error):
|
||||||
pass
|
pass
|
||||||
user = _fetch_user_by_name(basic_username)
|
user = _fetch_user_by_name(basic_username)
|
||||||
if user and check_password_hash(str(user.password), basic_password):
|
if user and check_password_hash(str(user.password), basic_password):
|
||||||
@ -459,12 +460,6 @@ def get_matching_tags():
|
|||||||
if len(exclude_tag_inputs) > 0:
|
if len(exclude_tag_inputs) > 0:
|
||||||
for tag in exclude_tag_inputs:
|
for tag in exclude_tag_inputs:
|
||||||
q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
|
q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
|
||||||
'''if len(include_extension_inputs) > 0:
|
|
||||||
for tag in exclude_tag_inputs:
|
|
||||||
q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
|
|
||||||
if len(exclude_extension_inputs) > 0:
|
|
||||||
for tag in exclude_tag_inputs:
|
|
||||||
q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))'''
|
|
||||||
for book in q:
|
for book in q:
|
||||||
for tag in book.tags:
|
for tag in book.tags:
|
||||||
if tag.id not in tag_dict['tags']:
|
if tag.id not in tag_dict['tags']:
|
||||||
@ -966,11 +961,13 @@ def advanced_search():
|
|||||||
return render_title_template('search.html', searchterm=searchterm,
|
return render_title_template('search.html', searchterm=searchterm,
|
||||||
entries=q, title=_(u"search"), page="search")
|
entries=q, title=_(u"search"), page="search")
|
||||||
# prepare data for search-form
|
# prepare data for search-form
|
||||||
# tags = db.session.query(db.Tags).order_by(db.Tags.name).all()
|
tags = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\
|
||||||
tags = db.session.query(db.Tags).filter(tags_filters()).order_by(db.Tags.name).all()
|
.group_by(text('books_tags_link.tag')).order_by(db.Tags.name).all()
|
||||||
series = db.session.query(db.Series).order_by(db.Series.name).all()
|
series = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\
|
||||||
extensions = db.session.query(db.Data) \
|
.group_by(text('books_series_link.series')).order_by(db.Series.name).filter(common_filters()).all()
|
||||||
|
extensions = db.session.query(db.Data).join(db.Books).filter(common_filters())\
|
||||||
.group_by(db.Data.format).order_by(db.Data.format).all()
|
.group_by(db.Data.format).order_by(db.Data.format).all()
|
||||||
|
|
||||||
if current_user.filter_language() == u"all":
|
if current_user.filter_language() == u"all":
|
||||||
languages = speaking_language()
|
languages = speaking_language()
|
||||||
else:
|
else:
|
||||||
@ -1091,10 +1088,10 @@ def register():
|
|||||||
if not to_save["nickname"] or not to_save["email"]:
|
if not to_save["nickname"] or not to_save["email"]:
|
||||||
flash(_(u"Please fill out all fields!"), category="error")
|
flash(_(u"Please fill out all fields!"), category="error")
|
||||||
return render_title_template('register.html', title=_(u"register"), page="register")
|
return render_title_template('register.html', title=_(u"register"), page="register")
|
||||||
|
|
||||||
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"]
|
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"]
|
||||||
.lower()).first()
|
.lower()).first()
|
||||||
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()).first()
|
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()).first()
|
||||||
|
|
||||||
if not existing_user and not existing_email:
|
if not existing_user and not existing_email:
|
||||||
content = ub.User()
|
content = ub.User()
|
||||||
# content.password = generate_password_hash(to_save["password"])
|
# content.password = generate_password_hash(to_save["password"])
|
||||||
@ -1105,7 +1102,7 @@ def register():
|
|||||||
content.password = generate_password_hash(password)
|
content.password = generate_password_hash(password)
|
||||||
content.role = config.config_default_role
|
content.role = config.config_default_role
|
||||||
content.sidebar_view = config.config_default_show
|
content.sidebar_view = config.config_default_show
|
||||||
content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT)
|
#content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT)
|
||||||
try:
|
try:
|
||||||
ub.session.add(content)
|
ub.session.add(content)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
@ -1294,10 +1291,12 @@ def profile():
|
|||||||
downloads = list()
|
downloads = list()
|
||||||
languages = speaking_language()
|
languages = speaking_language()
|
||||||
translations = babel.list_translations() + [LC('en')]
|
translations = babel.list_translations() + [LC('en')]
|
||||||
|
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||||
if feature_support['oauth']:
|
if feature_support['oauth']:
|
||||||
oauth_status = get_oauth_status()
|
oauth_status = get_oauth_status()
|
||||||
else:
|
else:
|
||||||
oauth_status = None
|
oauth_status = None
|
||||||
|
|
||||||
for book in current_user.downloads:
|
for book in current_user.downloads:
|
||||||
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
|
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
|
||||||
if downloadBook:
|
if downloadBook:
|
||||||
@ -1312,11 +1311,14 @@ def profile():
|
|||||||
current_user.password = generate_password_hash(to_save["password"])
|
current_user.password = generate_password_hash(to_save["password"])
|
||||||
if "kindle_mail" in to_save and to_save["kindle_mail"] != current_user.kindle_mail:
|
if "kindle_mail" in to_save and to_save["kindle_mail"] != current_user.kindle_mail:
|
||||||
current_user.kindle_mail = to_save["kindle_mail"]
|
current_user.kindle_mail = to_save["kindle_mail"]
|
||||||
|
if "allowed_tags" in to_save and to_save["allowed_tags"] != current_user.allowed_tags:
|
||||||
|
current_user.allowed_tags = to_save["allowed_tags"].strip()
|
||||||
if to_save["email"] and to_save["email"] != current_user.email:
|
if to_save["email"] and to_save["email"] != current_user.email:
|
||||||
if config.config_public_reg and not check_valid_domain(to_save["email"]):
|
if config.config_public_reg and not check_valid_domain(to_save["email"]):
|
||||||
flash(_(u"E-mail is not from valid domain"), category="error")
|
flash(_(u"E-mail is not from valid domain"), category="error")
|
||||||
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
|
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
|
||||||
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
|
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
|
||||||
|
kobo_support=kobo_support,
|
||||||
registered_oauth=oauth_check, oauth_status=oauth_status)
|
registered_oauth=oauth_check, oauth_status=oauth_status)
|
||||||
if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
|
if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
|
||||||
# Query User nickname, if not existing, change
|
# Query User nickname, if not existing, change
|
||||||
@ -1327,6 +1329,7 @@ def profile():
|
|||||||
return render_title_template("user_edit.html",
|
return render_title_template("user_edit.html",
|
||||||
translations=translations,
|
translations=translations,
|
||||||
languages=languages,
|
languages=languages,
|
||||||
|
kobo_support=kobo_support,
|
||||||
new_user=0, content=current_user,
|
new_user=0, content=current_user,
|
||||||
downloads=downloads,
|
downloads=downloads,
|
||||||
registered_oauth=oauth_check,
|
registered_oauth=oauth_check,
|
||||||
@ -1349,7 +1352,7 @@ def profile():
|
|||||||
if "Show_detail_random" in to_save:
|
if "Show_detail_random" in to_save:
|
||||||
current_user.sidebar_view += constants.DETAIL_RANDOM
|
current_user.sidebar_view += constants.DETAIL_RANDOM
|
||||||
|
|
||||||
current_user.mature_content = "Show_mature_content" in to_save
|
#current_user.mature_content = "Show_mature_content" in to_save
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
@ -1358,13 +1361,13 @@ def profile():
|
|||||||
flash(_(u"Found an existing account for this e-mail address."), category="error")
|
flash(_(u"Found an existing account for this e-mail address."), category="error")
|
||||||
log.debug(u"Found an existing account for this e-mail address.")
|
log.debug(u"Found an existing account for this e-mail address.")
|
||||||
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
|
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
|
||||||
translations=translations,
|
translations=translations, kobo_support=kobo_support,
|
||||||
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
|
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
|
||||||
registered_oauth=oauth_check, oauth_status=oauth_status)
|
registered_oauth=oauth_check, oauth_status=oauth_status)
|
||||||
flash(_(u"Profile updated"), category="success")
|
flash(_(u"Profile updated"), category="success")
|
||||||
log.debug(u"Profile updated")
|
log.debug(u"Profile updated")
|
||||||
return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages,
|
return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages,
|
||||||
content=current_user, downloads=downloads,
|
content=current_user, downloads=downloads, kobo_support=kobo_support,
|
||||||
title= _(u"%(name)s's profile", name=current_user.nickname),
|
title= _(u"%(name)s's profile", name=current_user.nickname),
|
||||||
page="me", registered_oauth=oauth_check, oauth_status=oauth_status)
|
page="me", registered_oauth=oauth_check, oauth_status=oauth_status)
|
||||||
|
|
||||||
|
@ -32,3 +32,7 @@ rarfile>=2.7
|
|||||||
# other
|
# other
|
||||||
natsort>=2.2.0,<7.1.0
|
natsort>=2.2.0,<7.1.0
|
||||||
git+https://github.com/OzzieIsaacs/comicapi.git@ad8bfe5a1c31db882480433f86db2c5c57634a3f#egg=comicapi
|
git+https://github.com/OzzieIsaacs/comicapi.git@ad8bfe5a1c31db882480433f86db2c5c57634a3f#egg=comicapi
|
||||||
|
|
||||||
|
#Kobo integration
|
||||||
|
jsonschema>=3.2.0,<3.3.0
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user