Make Kobo optional
move jsonschema dependency to optional-requirements.txt Added version of jsonschema to about section Added additional column to RemoteAuthToken table Update configuration of Kobo sync protocol
This commit is contained in:
parent
2798dd5916
commit
79a9ef4859
14
cps.py
14
cps.py
|
@ -41,8 +41,13 @@ from cps.shelf import shelf
|
|||
from cps.admin import admi
|
||||
from cps.gdrive import gdrive
|
||||
from cps.editbooks import editbook
|
||||
from cps.kobo import kobo
|
||||
from cps.kobo_auth import kobo_auth
|
||||
|
||||
try:
|
||||
from cps.kobo import kobo
|
||||
from cps.kobo_auth import kobo_auth
|
||||
kobo_available = True
|
||||
except ImportError:
|
||||
kobo_available = False
|
||||
|
||||
try:
|
||||
from cps.oauth_bb import oauth
|
||||
|
@ -61,8 +66,9 @@ def main():
|
|||
app.register_blueprint(admi)
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(editbook)
|
||||
app.register_blueprint(kobo)
|
||||
app.register_blueprint(kobo_auth)
|
||||
if kobo_available:
|
||||
app.register_blueprint(kobo)
|
||||
app.register_blueprint(kobo_auth)
|
||||
if oauth_available:
|
||||
app.register_blueprint(oauth)
|
||||
success = web_server.start()
|
||||
|
|
|
@ -67,6 +67,7 @@ _VERSIONS = OrderedDict(
|
|||
Unidecode = unidecode_version,
|
||||
Flask_SimpleLDAP = u'installed' if bool(services.ldap) 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())
|
||||
|
|
47
cps/admin.py
47
cps/admin.py
|
@ -45,7 +45,8 @@ from .web import admin_required, render_title_template, before_request, unconfig
|
|||
|
||||
feature_support = {
|
||||
'ldap': False, # bool(services.ldap),
|
||||
'goodreads': bool(services.goodreads_support)
|
||||
'goodreads': bool(services.goodreads_support),
|
||||
'kobo': bool(services.kobo)
|
||||
}
|
||||
|
||||
# try:
|
||||
|
@ -63,6 +64,7 @@ except ImportError:
|
|||
oauth_check = {}
|
||||
|
||||
|
||||
|
||||
feature_support['gdrive'] = gdrive_support
|
||||
admi = Blueprint('admin', __name__)
|
||||
log = logger.create()
|
||||
|
@ -568,7 +570,7 @@ def _configuration_update_helper():
|
|||
# Remote login configuration
|
||||
_config_checkbox("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
|
||||
_config_checkbox("config_use_goodreads")
|
||||
|
@ -693,7 +695,8 @@ def new_user():
|
|||
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
|
||||
flash(_(u"Please fill out all fields!"), category="error")
|
||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||
registered_oauth=oauth_check, title=_(u"Add new user"))
|
||||
registered_oauth=oauth_check, feature_support=feature_support,
|
||||
title=_(u"Add new user"))
|
||||
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())\
|
||||
.first()
|
||||
|
@ -704,14 +707,15 @@ def new_user():
|
|||
if config.config_public_reg and not check_valid_domain(to_save["email"]):
|
||||
flash(_(u"E-mail is not from valid domain"), category="error")
|
||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||
registered_oauth=oauth_check, title=_(u"Add new user"))
|
||||
registered_oauth=oauth_check, feature_support=feature_support,
|
||||
title=_(u"Add new user"))
|
||||
else:
|
||||
content.email = to_save["email"]
|
||||
else:
|
||||
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,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
registered_oauth=oauth_check)
|
||||
feature_support=feature_support, registered_oauth=oauth_check)
|
||||
try:
|
||||
ub.session.add(content)
|
||||
ub.session.commit()
|
||||
|
@ -729,7 +733,7 @@ def new_user():
|
|||
# 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,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
registered_oauth=oauth_check)
|
||||
feature_support=feature_support, registered_oauth=oauth_check)
|
||||
|
||||
|
||||
@admi.route("/admin/mailsettings")
|
||||
|
@ -850,8 +854,14 @@ def edit_user(user_id):
|
|||
content.kobo_user_key_hash = kobo_user_key_hash
|
||||
else:
|
||||
flash(_(u"Found an existing account for this Kobo UserKey."), category="error")
|
||||
return render_title_template("user_edit.html", translations=translations, languages=languages,
|
||||
new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check,
|
||||
return render_title_template("user_edit.html",
|
||||
translations=translations,
|
||||
languages=languages,
|
||||
new_user=0,
|
||||
content=content,
|
||||
downloads=downloads,
|
||||
registered_oauth=oauth_check,
|
||||
feature_support=feature_support,
|
||||
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
||||
if to_save["email"] and to_save["email"] != content.email:
|
||||
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \
|
||||
|
@ -860,9 +870,15 @@ def edit_user(user_id):
|
|||
content.email = to_save["email"]
|
||||
else:
|
||||
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(),
|
||||
new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check,
|
||||
feature_support=feature_support,
|
||||
new_user=0,
|
||||
content=content,
|
||||
downloads=downloads,
|
||||
registered_oauth=oauth_check,
|
||||
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
||||
if "nickname" in to_save and to_save["nickname"] != content.nickname:
|
||||
# Query User nickname, if not existing, change
|
||||
|
@ -877,6 +893,7 @@ def edit_user(user_id):
|
|||
new_user=0, content=content,
|
||||
downloads=downloads,
|
||||
registered_oauth=oauth_check,
|
||||
feature_support=feature_support,
|
||||
title=_(u"Edit User %(nick)s", nick=content.nickname),
|
||||
page="edituser")
|
||||
|
||||
|
@ -888,9 +905,15 @@ def edit_user(user_id):
|
|||
except IntegrityError:
|
||||
ub.session.rollback()
|
||||
flash(_(u"An unknown error occured."), category="error")
|
||||
return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0,
|
||||
content=content, downloads=downloads, registered_oauth=oauth_check,
|
||||
return render_title_template("user_edit.html",
|
||||
translations=translations,
|
||||
languages=languages,
|
||||
new_user=0,
|
||||
content=content,
|
||||
downloads=downloads,
|
||||
registered_oauth=oauth_check,
|
||||
mail_configured=config.get_mail_server_configured(),
|
||||
feature_support=feature_support,
|
||||
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
||||
|
||||
|
||||
|
|
127
cps/kobo.py
127
cps/kobo.py
|
@ -17,10 +17,8 @@
|
|||
# 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 os
|
||||
import sys
|
||||
import uuid
|
||||
from base64 import b64decode, b64encode
|
||||
from datetime import datetime
|
||||
from time import gmtime, strftime
|
||||
try:
|
||||
|
@ -28,14 +26,12 @@ try:
|
|||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
|
||||
from jsonschema import validate, exceptions
|
||||
from flask import (
|
||||
Blueprint,
|
||||
request,
|
||||
make_response,
|
||||
jsonify,
|
||||
json,
|
||||
current_app,
|
||||
url_for,
|
||||
redirect,
|
||||
)
|
||||
|
@ -44,7 +40,7 @@ from werkzeug.datastructures import Headers
|
|||
from sqlalchemy import func
|
||||
import requests
|
||||
|
||||
from . import config, logger, kobo_auth, db, helper
|
||||
from . import config, logger, kobo_auth, db, helper, services
|
||||
from .web import download_required
|
||||
|
||||
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB", "EPUB3"]}
|
||||
|
@ -56,19 +52,6 @@ kobo_auth.register_url_value_preprocessor(kobo)
|
|||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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/")
|
||||
|
@ -110,117 +93,11 @@ def redirect_or_proxy_request():
|
|||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@kobo.route("/v1/library/sync")
|
||||
@login_required
|
||||
@download_required
|
||||
def HandleSyncRequest():
|
||||
sync_token = SyncToken.from_headers(request.headers)
|
||||
sync_token = services.SyncToken.from_headers(request.headers)
|
||||
log.info("Kobo library sync request received.")
|
||||
|
||||
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header
|
||||
|
|
|
@ -23,13 +23,13 @@ This module also includes research notes into the auth protocol used by Kobo dev
|
|||
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
|
@ -95,7 +95,7 @@ def load_user_from_kobo_request(request):
|
|||
user = (
|
||||
ub.session.query(ub.User)
|
||||
.join(ub.RemoteAuthToken)
|
||||
.filter(ub.RemoteAuthToken.auth_token == auth_token)
|
||||
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
|
||||
.first()
|
||||
)
|
||||
if user is not None:
|
||||
|
@ -108,21 +108,23 @@ def load_user_from_kobo_request(request):
|
|||
kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
||||
|
||||
|
||||
@kobo_auth.route("/generate_auth_token")
|
||||
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||
@login_required
|
||||
def generate_auth_token():
|
||||
def generate_auth_token(user_id):
|
||||
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||
ub.session.query(ub.RemoteAuthToken).filter(
|
||||
ub.RemoteAuthToken.user_id == current_user.id
|
||||
).delete()
|
||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
|
||||
ub.RemoteAuthToken.user_id == user_id
|
||||
).filter(ub.RemoteAuthToken.token_type==1).first()
|
||||
|
||||
auth_token = ub.RemoteAuthToken()
|
||||
auth_token.user_id = current_user.id
|
||||
auth_token.expiration = datetime.max
|
||||
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
|
||||
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()
|
||||
ub.session.add(auth_token)
|
||||
ub.session.commit()
|
||||
|
||||
return render_title_template(
|
||||
"generate_kobo_auth_url.html",
|
||||
|
@ -131,3 +133,13 @@ def generate_auth_token():
|
|||
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@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 ""
|
||||
|
|
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,9 @@ except ImportError as err:
|
|||
log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err)
|
||||
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
|
||||
|
|
|
@ -228,6 +228,41 @@ $(function() {
|
|||
$(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() {
|
||||
$(".discover .row").isotope("layout");
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{% extends "layout.html" %}
|
||||
{% extends "fragment.html" %}
|
||||
{% block body %}
|
||||
<div class="well">
|
||||
<h2 style="margin-top: 0">{{_('Generate Kobo Auth URL')}}</h2>
|
||||
<p>
|
||||
{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a>.
|
||||
</p>
|
||||
|
@ -12,4 +11,4 @@
|
|||
{{_('Please note that every visit to this current page invalidates any previously generated Authentication url for this user.')}}</a>.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -59,6 +59,13 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if feature_support['kobo'] %}
|
||||
<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 %}
|
||||
<div class="col-sm-6">
|
||||
{% for element in sidebar %}
|
||||
{% if element['config_show'] %}
|
||||
|
@ -146,6 +153,35 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</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() }}
|
||||
|
|
11
cps/ub.py
11
cps/ub.py
|
@ -199,6 +199,7 @@ class User(UserBase, Base):
|
|||
allowed_tags = Column(String, default="")
|
||||
restricted_column_value = Column(String, default="")
|
||||
allowed_column_value = Column(String, default="")
|
||||
remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic')
|
||||
|
||||
|
||||
if oauth_support:
|
||||
|
@ -333,6 +334,7 @@ class RemoteAuthToken(Base):
|
|||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
verified = Column(Boolean, default=False)
|
||||
expiration = Column(DateTime)
|
||||
token_type = Column(Integer, default=0)
|
||||
|
||||
def __init__(self):
|
||||
self.auth_token = (hexlify(os.urandom(4))).decode('utf-8')
|
||||
|
@ -364,6 +366,15 @@ def migrate_Database(session):
|
|||
conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER")
|
||||
conn.execute("update registration set 'allow' = 1")
|
||||
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
|
||||
cnt = session.query(Registration).count()
|
||||
if not cnt:
|
||||
|
|
|
@ -55,7 +55,8 @@ from .redirect import redirect_back
|
|||
|
||||
feature_support = {
|
||||
'ldap': False, # bool(services.ldap),
|
||||
'goodreads': bool(services.goodreads_support)
|
||||
'goodreads': bool(services.goodreads_support),
|
||||
'kobo': bool(services.kobo)
|
||||
}
|
||||
|
||||
try:
|
||||
|
@ -1319,6 +1320,7 @@ def profile():
|
|||
flash(_(u"E-mail is not from valid domain"), category="error")
|
||||
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
|
||||
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
|
||||
feature_support=feature_support,
|
||||
registered_oauth=oauth_check, oauth_status=oauth_status)
|
||||
if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
|
||||
# Query User nickname, if not existing, change
|
||||
|
@ -1329,6 +1331,7 @@ def profile():
|
|||
return render_title_template("user_edit.html",
|
||||
translations=translations,
|
||||
languages=languages,
|
||||
feature_support=feature_support,
|
||||
new_user=0, content=current_user,
|
||||
downloads=downloads,
|
||||
registered_oauth=oauth_check,
|
||||
|
@ -1360,13 +1363,13 @@ def profile():
|
|||
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.")
|
||||
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
|
||||
translations=translations,
|
||||
translations=translations, feature_support=feature_support,
|
||||
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
|
||||
registered_oauth=oauth_check, oauth_status=oauth_status)
|
||||
flash(_(u"Profile updated"), category="success")
|
||||
log.debug(u"Profile updated")
|
||||
return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages,
|
||||
content=current_user, downloads=downloads,
|
||||
content=current_user, downloads=downloads, feature_support=feature_support,
|
||||
title= _(u"%(name)s's profile", name=current_user.nickname),
|
||||
page="me", registered_oauth=oauth_check, oauth_status=oauth_status)
|
||||
|
||||
|
|
|
@ -32,3 +32,6 @@ rarfile>=2.7
|
|||
# other
|
||||
natsort>=2.2.0
|
||||
git+https://github.com/OzzieIsaacs/comicapi.git@5346716578b2843f54d522f44d01bc8d25001d24#egg=comicapi
|
||||
|
||||
#kobo integration
|
||||
jsonschema>=3.2.0
|
||||
|
|
|
@ -13,4 +13,3 @@ SQLAlchemy>=1.1.0
|
|||
tornado>=4.1
|
||||
Wand>=0.4.4
|
||||
unidecode>=0.04.19
|
||||
jsonschema>=3.2.0
|
||||
|
|
Loading…
Reference in New Issue
Block a user