mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-31 15:23:02 +00:00 
			
		
		
		
	Merge branch 'Develop'
# Conflicts: # cps/db.py # cps/templates/user_edit.html
This commit is contained in:
		
							
								
								
									
										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
											
										
									
								
							
		Reference in New Issue
	
	Block a user
	 Ozzieisaacs
					Ozzieisaacs