Merge branch 'master' into Develop

# Conflicts:
#	cps/config_sql.py
#	cps/ub.py
#	cps/web.py
This commit is contained in:
Ozzieisaacs 2020-04-16 20:12:27 +02:00
commit 9e159ed5ab
24 changed files with 1322 additions and 646 deletions

View File

@ -17,6 +17,9 @@ Steps to reproduce the behavior:
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
**Logfile**
Add content of calibre-web.log file or the relevant error, try to reproduce your problem with "debug" log-level to get more output.
**Expected behavior** **Expected behavior**
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.

View File

@ -23,12 +23,12 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- Send eBooks to Kindle devices with the click of a button - Send eBooks to Kindle devices with the click of a button
- Sync your Kobo devices through Calibre-Web with your Calibre library - Sync your Kobo devices through Calibre-Web with your Calibre library
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz) - Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz)
- Upload new books in many formats - Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
- Support for Calibre custom columns - Support for Calibre Custom Columns
- Ability to hide content based on categories and Custom Column content per user - Ability to hide content based on categories and Custom Column content per user
- Self-update capability - Self-update capability
- "Magic Link" login to make it easy to log on eReaders - "Magic Link" login to make it easy to log on eReaders
- Login via google/github oauth and via proxy authentication - Login via LDAP, google/github oauth and via proxy authentication
## Quick start ## Quick start

View File

@ -43,6 +43,11 @@ try:
except ImportError: except ImportError:
unidecode_version = _(u'not installed') unidecode_version = _(u'not installed')
try:
from flask_dance import __version__ as flask_danceVersion
except ImportError:
flask_danceVersion = None
from . import services from . import services
about = flask.Blueprint('about', __name__) about = flask.Blueprint('about', __name__)
@ -68,9 +73,11 @@ _VERSIONS = OrderedDict(
iso639=isoLanguages.__version__, iso639=isoLanguages.__version__,
pytz=pytz.__version__, pytz=pytz.__version__,
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 None,
Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed', python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else u'not installed', Goodreads = u'installed' if bool(services.goodreads_support) else None,
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
flask_dance = flask_danceVersion
) )
_VERSIONS.update(uploader.get_versions()) _VERSIONS.update(uploader.get_versions())

View File

@ -42,8 +42,10 @@ from .helper import speaking_language, check_valid_domain, send_test_mail, reset
from .gdriveutils import is_gdrive_ready, gdrive_support from .gdriveutils import is_gdrive_ready, gdrive_support
from .web import admin_required, render_title_template, before_request, unconfigured, login_required_if_no_ano from .web import admin_required, render_title_template, before_request, unconfigured, login_required_if_no_ano
log = logger.create()
feature_support = { feature_support = {
'ldap': False, # bool(services.ldap), 'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support), 'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo) 'kobo': bool(services.kobo)
} }
@ -57,7 +59,8 @@ feature_support = {
try: try:
from .oauth_bb import oauth_check, oauthblueprints from .oauth_bb import oauth_check, oauthblueprints
feature_support['oauth'] = True feature_support['oauth'] = True
except ImportError: except ImportError as err:
log.debug('Cannot import Flask-Dance, login with Oauth will not work: %s', err)
feature_support['oauth'] = False feature_support['oauth'] = False
oauthblueprints = [] oauthblueprints = []
oauth_check = {} oauth_check = {}
@ -65,7 +68,7 @@ except ImportError:
feature_support['gdrive'] = gdrive_support feature_support['gdrive'] = gdrive_support
admi = Blueprint('admin', __name__) admi = Blueprint('admin', __name__)
log = logger.create()
@admi.route("/admin") @admi.route("/admin")
@ -79,12 +82,12 @@ def admin_forbidden():
@admin_required @admin_required
def shutdown(): def shutdown():
task = int(request.args.get("parameter").strip()) task = int(request.args.get("parameter").strip())
showtext = {}
if task in (0, 1): # valid commandos received if task in (0, 1): # valid commandos received
# close all database connections # close all database connections
db.dispose() db.dispose()
ub.dispose() ub.dispose()
showtext = {}
if task == 0: if task == 0:
showtext['text'] = _(u'Server restarted, please reload page') showtext['text'] = _(u'Server restarted, please reload page')
else: else:
@ -96,9 +99,11 @@ def shutdown():
if task == 2: if task == 2:
log.warning("reconnecting to calibre database") log.warning("reconnecting to calibre database")
db.setup_db(config) db.setup_db(config)
return '{}' showtext['text'] = _(u'Reconnect successful')
return json.dumps(showtext)
abort(404) showtext['text'] = _(u'Unknown command')
return json.dumps(showtext), 400
@admi.route("/admin/view") @admi.route("/admin/view")
@ -502,7 +507,7 @@ def _configuration_update_helper():
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
gdrive_secrets = json.load(settings)['web'] gdrive_secrets = json.load(settings)['web']
if not gdrive_secrets: if not gdrive_secrets:
return _configuration_result('client_secrets.json is not configured for web application') return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
gdriveutils.update_settings( gdriveutils.update_settings(
gdrive_secrets['client_id'], gdrive_secrets['client_id'],
gdrive_secrets['client_secret'], gdrive_secrets['client_secret'],
@ -518,11 +523,11 @@ def _configuration_update_helper():
reboot_required |= _config_string("config_keyfile") reboot_required |= _config_string("config_keyfile")
if config.config_keyfile and not os.path.isfile(config.config_keyfile): if config.config_keyfile and not os.path.isfile(config.config_keyfile):
return _configuration_result('Keyfile location is not valid, please enter correct path', gdriveError) return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), gdriveError)
reboot_required |= _config_string("config_certfile") reboot_required |= _config_string("config_certfile")
if config.config_certfile and not os.path.isfile(config.config_certfile): if config.config_certfile and not os.path.isfile(config.config_certfile):
return _configuration_result('Certfile location is not valid, please enter correct path', gdriveError) return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), gdriveError)
_config_checkbox_int("config_uploading") _config_checkbox_int("config_uploading")
_config_checkbox_int("config_anonbrowse") _config_checkbox_int("config_anonbrowse")
@ -540,26 +545,57 @@ def _configuration_update_helper():
#LDAP configurator, #LDAP configurator,
if config.config_login_type == constants.LOGIN_LDAP: if config.config_login_type == constants.LOGIN_LDAP:
_config_string("config_ldap_provider_url") reboot_required |= _config_string("config_ldap_provider_url")
_config_int("config_ldap_port") reboot_required |= _config_int("config_ldap_port")
_config_string("config_ldap_schema") # _config_string("config_ldap_schema")
_config_string("config_ldap_dn") reboot_required |= _config_string("config_ldap_dn")
_config_string("config_ldap_user_object") reboot_required |= _config_string("config_ldap_serv_username")
if not config.config_ldap_provider_url or not config.config_ldap_port or not config.config_ldap_dn or not config.config_ldap_user_object: reboot_required |= _config_string("config_ldap_user_object")
return _configuration_result('Please enter a LDAP provider, port, DN and user object identifier', gdriveError) reboot_required |= _config_string("config_ldap_group_object_filter")
reboot_required |= _config_string("config_ldap_group_members_field")
reboot_required |= _config_checkbox("config_ldap_openldap")
reboot_required |= _config_int("config_ldap_encryption")
reboot_required |= _config_string("config_ldap_cert_path")
_config_string("config_ldap_group_name")
if "config_ldap_serv_password" in to_save and to_save["config_ldap_serv_password"] != "":
reboot_required |= 1
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8')
config.save()
_config_string("config_ldap_serv_username") if not config.config_ldap_provider_url \
if not config.config_ldap_serv_username or "config_ldap_serv_password" not in to_save: or not config.config_ldap_port \
return _configuration_result('Please enter a LDAP service account and password', gdriveError) or not config.config_ldap_dn \
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode) or not config.config_ldap_user_object:
return _configuration_result(_('Please Enter a LDAP Provider, '
'Port, DN and User Object Identifier'), gdriveError)
_config_checkbox("config_ldap_use_ssl")
_config_checkbox("config_ldap_use_tls") if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password):
_config_checkbox("config_ldap_openldap") return _configuration_result('Please Enter a LDAP Service Account and Password', gdriveError)
_config_checkbox("config_ldap_require_cert")
_config_string("config_ldap_cert_path") #_config_checkbox("config_ldap_use_ssl")
if config.config_ldap_cert_path and not os.path.isfile(config.config_ldap_cert_path): #_config_checkbox("config_ldap_use_tls")
return _configuration_result('LDAP Certfile location is not valid, please enter correct path', gdriveError) # reboot_required |= _config_checkbox("config_ldap_openldap")
# _config_checkbox("config_ldap_require_cert")
if config.config_ldap_group_object_filter:
if config.config_ldap_group_object_filter.count("%s") != 1:
return _configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'),
gdriveError)
if config.config_ldap_group_object_filter.count("(") != config.config_ldap_group_object_filter.count(")"):
return _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'),
gdriveError)
if config.config_ldap_user_object.count("%s") != 1:
return _configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'),
gdriveError)
if config.config_ldap_user_object.count("(") != config.config_ldap_user_object.count(")"):
return _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'),
gdriveError)
if config.config_ldap_cert_path and not os.path.isdir(config.config_ldap_cert_path):
return _configuration_result(_('LDAP Certificate Location is not Valid, Please Enter Correct Path'),
gdriveError)
# Remote login configuration # Remote login configuration
_config_checkbox("config_remote_login") _config_checkbox("config_remote_login")
@ -586,33 +622,32 @@ def _configuration_update_helper():
active_oauths = 0 active_oauths = 0
for element in oauthblueprints: for element in oauthblueprints:
if to_save["config_"+str(element['id'])+"_oauth_client_id"] \
and to_save["config_"+str(element['id'])+"_oauth_client_secret"]:
active_oauths += 1
element["active"] = 1
ub.session.query(ub.OAuthProvider).filter(ub.OAuthProvider.id == element['id']).update(
{"oauth_client_id":to_save["config_"+str(element['id'])+"_oauth_client_id"],
"oauth_client_secret":to_save["config_"+str(element['id'])+"_oauth_client_secret"],
"active":1})
if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \ if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']: or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
reboot_required = True reboot_required = True
element['oauth_client_id'] = to_save["config_" + str(element['id']) + "_oauth_client_id"] element['oauth_client_id'] = to_save["config_" + str(element['id']) + "_oauth_client_id"]
element['oauth_client_secret'] = to_save["config_" + str(element['id']) + "_oauth_client_secret"] element['oauth_client_secret'] = to_save["config_" + str(element['id']) + "_oauth_client_secret"]
if to_save["config_"+str(element['id'])+"_oauth_client_id"] \
and to_save["config_"+str(element['id'])+"_oauth_client_secret"]:
active_oauths += 1
element["active"] = 1
else: else:
ub.session.query(ub.OAuthProvider).filter(ub.OAuthProvider.id == element['id']).update(
{"active":0})
element["active"] = 0 element["active"] = 0
ub.session.query(ub.OAuthProvider).filter(ub.OAuthProvider.id == element['id']).update(
{"oauth_client_id":to_save["config_"+str(element['id'])+"_oauth_client_id"],
"oauth_client_secret":to_save["config_"+str(element['id'])+"_oauth_client_secret"],
"active":element["active"]})
reboot_required |= _config_int("config_log_level") reboot_required |= _config_int("config_log_level")
reboot_required |= _config_string("config_logfile") reboot_required |= _config_string("config_logfile")
if not logger.is_valid_logfile(config.config_logfile): if not logger.is_valid_logfile(config.config_logfile):
return _configuration_result('Logfile location is not valid, please enter correct path', gdriveError) return _configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'), gdriveError)
reboot_required |= _config_checkbox_int("config_access_log") reboot_required |= _config_checkbox_int("config_access_log")
reboot_required |= _config_string("config_access_logfile") reboot_required |= _config_string("config_access_logfile")
if not logger.is_valid_logfile(config.config_access_logfile): if not logger.is_valid_logfile(config.config_access_logfile):
return _configuration_result('Access Logfile location is not valid, please enter correct path', gdriveError) return _configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'), gdriveError)
# Rarfile Content configuration # Rarfile Content configuration
_config_string("config_rarfile_location") _config_string("config_rarfile_location")
@ -631,7 +666,7 @@ def _configuration_update_helper():
if db_change: if db_change:
# reload(db) # reload(db)
if not db.setup_db(config): if not db.setup_db(config):
return _configuration_result('DB location is not valid, please enter correct path', gdriveError) return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdriveError)
config.save() config.save()
flash(_(u"Calibre-Web configuration updated"), category="success") flash(_(u"Calibre-Web configuration updated"), category="success")
@ -657,7 +692,7 @@ def _configuration_result(error_flash=None, gdriveError=None):
show_login_button = config.db_configured and not current_user.is_authenticated show_login_button = config.db_configured and not current_user.is_authenticated
if error_flash: if error_flash:
config.load() config.load()
flash(_(error_flash), category="error") flash(error_flash, category="error")
show_login_button = False show_login_button = False
return render_title_template("config_edit.html", config=config, provider=oauthblueprints, return render_title_template("config_edit.html", config=config, provider=oauthblueprints,

View File

@ -71,7 +71,7 @@ class _Settings(_Base):
config_kobo_sync = 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=38911) config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
config_columns_to_ignore = Column(String) config_columns_to_ignore = Column(String)
config_denied_tags = Column(String, default="") config_denied_tags = Column(String, default="")
@ -93,18 +93,21 @@ class _Settings(_Base):
config_kobo_proxy = Column(Boolean, default=False) config_kobo_proxy = Column(Boolean, default=False)
config_ldap_provider_url = Column(String, default='localhost') config_ldap_provider_url = Column(String, default='example.org')
config_ldap_port = Column(SmallInteger, default=389) config_ldap_port = Column(SmallInteger, default=389)
config_ldap_schema = Column(String, default='ldap') # config_ldap_schema = Column(String, default='ldap')
config_ldap_serv_username = Column(String) config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
config_ldap_serv_password = Column(String) config_ldap_serv_password = Column(String, default="")
config_ldap_use_ssl = Column(Boolean, default=False) config_ldap_encryption = Column(SmallInteger, default=0)
config_ldap_use_tls = Column(Boolean, default=False) # config_ldap_use_tls = Column(Boolean, default=False)
config_ldap_require_cert = Column(Boolean, default=False) # config_ldap_require_cert = Column(Boolean, default=False)
config_ldap_cert_path = Column(String) config_ldap_cert_path = Column(String, default="")
config_ldap_dn = Column(String) config_ldap_dn = Column(String, default='dc=example,dc=org')
config_ldap_user_object = Column(String) config_ldap_user_object = Column(String, default='uid=%s')
config_ldap_openldap = Column(Boolean, default=False) config_ldap_openldap = Column(Boolean, default=True)
config_ldap_group_object_filter = Column(String, default='(&(objectclass=posixGroup)(cn=%s))')
config_ldap_group_members_field = Column(String, default='memberUid')
config_ldap_group_name = Column(String, default='calibreweb')
config_ebookconverter = Column(Integer, default=0) config_ebookconverter = Column(Integer, default=0)
config_converterpath = Column(String) config_converterpath = Column(String)
@ -212,7 +215,7 @@ class _ConfigSQL(object):
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER) return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER)
def set_from_dictionary(self, dictionary, field, convertor=None, default=None): def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
'''Possibly updates a field of this object. '''Possibly updates a field of this object.
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor. The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
@ -228,6 +231,9 @@ class _ConfigSQL(object):
return False return False
if convertor is not None: if convertor is not None:
if encode:
new_value = convertor(new_value.encode(encode))
else:
new_value = convertor(new_value) new_value = convertor(new_value)
current_value = self.__dict__.get(field) current_value = self.__dict__.get(field)
@ -277,7 +283,9 @@ class _ConfigSQL(object):
self._session.commit() self._session.commit()
self.load() self.load()
def invalidate(self): def invalidate(self, error=None):
if error:
log.error(error)
log.warning("invalidating configuration") log.warning("invalidating configuration")
self.db_configured = False self.db_configured = False
self.config_calibre_dir = None self.config_calibre_dir = None

View File

@ -344,14 +344,13 @@ def setup_db(config):
isolation_level="SERIALIZABLE", isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False}) connect_args={'check_same_thread': False})
conn = engine.connect() conn = engine.connect()
except: # conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
config.invalidate() except Exception as e:
config.invalidate(e)
return False return False
config.db_configured = True config.db_configured = True
update_title_sort(config, conn.connection) update_title_sort(config, conn.connection)
# conn.connection.create_function('lower', 1, lcase)
# conn.connection.create_function('upper', 1, ucase)
if not cc_classes: if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns") cc = conn.execute("SELECT id, datatype FROM custom_columns")

View File

@ -796,6 +796,7 @@ def fill_indexpage_with_archived_books(page, database, db_filter, order, allow_s
def get_typeahead(database, query, replace=('',''), tag_filter=true()): def get_typeahead(database, query, replace=('',''), tag_filter=true()):
query = query or ''
db.session.connection().connection.connection.create_function("lower", 1, lcase) db.session.connection().connection.connection.create_function("lower", 1, lcase)
entries = db.session.query(database).filter(tag_filter).filter(func.lower(database.name).ilike("%" + query + "%")).all() entries = db.session.query(database).filter(tag_filter).filter(func.lower(database.name).ilike("%" + query + "%")).all()
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries]) json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])

View File

@ -23,7 +23,18 @@ from flask import session
try: try:
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
backend_resultcode = False # prevent storing values with this resultcode
except ImportError:
# fails on flask-dance >1.3, due to renaming
try:
from flask_dance.consumer.storage.sqla import SQLAlchemyStorage as SQLAlchemyBackend
from flask_dance.consumer.storage.sqla import first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound
backend_resultcode = True # prevent storing values with this resultcode
except ImportError:
pass
try:
class OAuthBackend(SQLAlchemyBackend): class OAuthBackend(SQLAlchemyBackend):
""" """
Stores and retrieves OAuth tokens using a relational database through Stores and retrieves OAuth tokens using a relational database through
@ -39,7 +50,7 @@ try:
def get(self, blueprint, user=None, user_id=None): def get(self, blueprint, user=None, user_id=None):
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '': if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '':
return session[blueprint.name + '_oauth_token'] return session[self.provider_id + '_oauth_token']
# check cache # check cache
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
token = self.cache.get(cache_key) token = self.cache.get(cache_key)
@ -152,5 +163,5 @@ try:
blueprint=blueprint, user=user, user_id=user_id, blueprint=blueprint, user=user, user_id=user_id,
)) ))
except ImportError: except Exception:
pass pass

View File

@ -35,8 +35,7 @@ from sqlalchemy.orm.exc import NoResultFound
from . import constants, logger, config, app, ub from . import constants, logger, config, app, ub
from .web import login_required from .web import login_required
from .oauth import OAuthBackend from .oauth import OAuthBackend, backend_resultcode
# from .web import github_oauth_required
oauth_check = {} oauth_check = {}
@ -59,29 +58,29 @@ def oauth_required(f):
return inner return inner
def register_oauth_blueprint(id, show_name): def register_oauth_blueprint(cid, show_name):
oauth_check[id] = show_name oauth_check[cid] = show_name
def register_user_with_oauth(user=None): def register_user_with_oauth(user=None):
all_oauth = {} all_oauth = {}
for oauth in oauth_check.keys(): for oauth_key in oauth_check.keys():
if str(oauth) + '_oauth_user_id' in session and session[str(oauth) + '_oauth_user_id'] != '': if str(oauth_key) + '_oauth_user_id' in session and session[str(oauth_key) + '_oauth_user_id'] != '':
all_oauth[oauth] = oauth_check[oauth] all_oauth[oauth_key] = oauth_check[oauth_key]
if len(all_oauth.keys()) == 0: if len(all_oauth.keys()) == 0:
return return
if user is None: if user is None:
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success") flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
else: else:
for oauth in all_oauth.keys(): for oauth_key in all_oauth.keys():
# Find this OAuth token in the database, or create it # Find this OAuth token in the database, or create it
query = ub.session.query(ub.OAuth).filter_by( query = ub.session.query(ub.OAuth).filter_by(
provider=oauth, provider=oauth_key,
provider_user_id=session[str(oauth) + "_oauth_user_id"], provider_user_id=session[str(oauth_key) + "_oauth_user_id"],
) )
try: try:
oauth = query.one() oauth_key = query.one()
oauth.user_id = user.id oauth_key.user_id = user.id
except NoResultFound: except NoResultFound:
# no found, return error # no found, return error
return return
@ -93,22 +92,23 @@ def register_user_with_oauth(user=None):
def logout_oauth_user(): def logout_oauth_user():
for oauth in oauth_check.keys(): for oauth_key in oauth_check.keys():
if str(oauth) + '_oauth_user_id' in session: if str(oauth_key) + '_oauth_user_id' in session:
session.pop(str(oauth) + '_oauth_user_id') session.pop(str(oauth_key) + '_oauth_user_id')
if ub.oauth_support: if ub.oauth_support:
oauthblueprints = [] oauthblueprints = []
if not ub.session.query(ub.OAuthProvider).count(): if not ub.session.query(ub.OAuthProvider).count():
oauth = ub.OAuthProvider() oauthProvider = ub.OAuthProvider()
oauth.provider_name = "github" oauthProvider.provider_name = "github"
oauth.active = False oauthProvider.active = False
ub.session.add(oauth) ub.session.add(oauthProvider)
ub.session.commit() ub.session.commit()
oauth = ub.OAuthProvider() oauthProvider = ub.OAuthProvider()
oauth.provider_name = "google" oauthProvider.provider_name = "google"
oauth.active = False oauthProvider.active = False
ub.session.add(oauth) ub.session.add(oauthProvider)
ub.session.commit() ub.session.commit()
oauth_ids = ub.session.query(ub.OAuthProvider).all() oauth_ids = ub.session.query(ub.OAuthProvider).all()
@ -141,9 +141,9 @@ if ub.oauth_support:
scope=element['scope'] scope=element['scope']
) )
element['blueprint'] = blueprint element['blueprint'] = blueprint
app.register_blueprint(blueprint, url_prefix="/login")
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']), element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
user=current_user, user_required=True) user=current_user, user_required=True)
app.register_blueprint(blueprint, url_prefix="/login")
if element['active']: if element['active']:
register_oauth_blueprint(element['id'], element['provider_name']) register_oauth_blueprint(element['id'], element['provider_name'])
@ -190,54 +190,64 @@ if ub.oauth_support:
provider_user_id=provider_user_id, provider_user_id=provider_user_id,
) )
try: try:
oauth = query.one() oauth_entry = query.one()
# update token # update token
oauth.token = token oauth_entry.token = token
except NoResultFound: except NoResultFound:
oauth = ub.OAuth( oauth_entry = ub.OAuth(
provider=provider_id, provider=provider_id,
provider_user_id=provider_user_id, provider_user_id=provider_user_id,
token=token, token=token,
) )
try: try:
ub.session.add(oauth) ub.session.add(oauth_entry)
ub.session.commit() ub.session.commit()
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
ub.session.rollback() ub.session.rollback()
# Disable Flask-Dance's default behavior for saving the OAuth token # Disable Flask-Dance's default behavior for saving the OAuth token
return False # Value differrs depending on flask-dance version
return backend_resultcode
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url): def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
query = ub.session.query(ub.OAuth).filter_by( query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id, provider=provider_id,
provider_user_id=provider_user_id, provider_user_id=provider_user_id,
) )
try: try:
oauth = query.one() oauth_entry = query.first()
# already bind with user, just login # already bind with user, just login
if oauth.user: if oauth_entry.user:
login_user(oauth.user) login_user(oauth_entry.user)
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname),
category="success")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
else: else:
# bind to current user # bind to current user
if current_user and current_user.is_authenticated: if current_user and current_user.is_authenticated:
oauth.user = current_user oauth_entry.user = current_user
try: try:
ub.session.add(oauth) ub.session.add(oauth_entry)
ub.session.commit() ub.session.commit()
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
return redirect(url_for('web.profile'))
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
ub.session.rollback() ub.session.rollback()
else:
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
log.info('Login failed, No User Linked With OAuth Account')
return redirect(url_for('web.login')) return redirect(url_for('web.login'))
# return redirect(url_for('web.login'))
# if config.config_public_reg: # if config.config_public_reg:
# return redirect(url_for('web.register')) # return redirect(url_for('web.register'))
# else: # else:
# flash(_(u"Public registration is not enabled"), category="error") # flash(_(u"Public registration is not enabled"), category="error")
# return redirect(url_for(redirect_url)) # return redirect(url_for(redirect_url))
except NoResultFound: except (NoResultFound, AttributeError):
return redirect(url_for(redirect_url)) return redirect(url_for(redirect_url))
@ -248,8 +258,8 @@ if ub.oauth_support:
) )
try: try:
oauths = query.all() oauths = query.all()
for oauth in oauths: for oauth_entry in oauths:
status.append(int(oauth.provider)) status.append(int(oauth_entry.provider))
return status return status
except NoResultFound: except NoResultFound:
return None return None
@ -263,21 +273,21 @@ if ub.oauth_support:
user_id=current_user.id, user_id=current_user.id,
) )
try: try:
oauth = query.one() oauth_entry = query.one()
if current_user and current_user.is_authenticated: if current_user and current_user.is_authenticated:
oauth.user = current_user oauth_entry.user = current_user
try: try:
ub.session.delete(oauth) ub.session.delete(oauth_entry)
ub.session.commit() ub.session.commit()
logout_oauth_user() logout_oauth_user()
flash(_(u"Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success") flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
ub.session.rollback() ub.session.rollback()
flash(_(u"Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound: except NoResultFound:
log.warning("oauth %s for user %d not fount", provider, current_user.id) log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_(u"Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") flash(_(u"Not Linked to %(oauth)s.", oauth=oauth_check[provider]), category="error")
return redirect(url_for('web.profile')) return redirect(url_for('web.profile'))
@ -296,7 +306,7 @@ if ub.oauth_support:
flash(msg, category="error") flash(msg, category="error")
@oauth.route('/github') @oauth.route('/link/github')
@oauth_required @oauth_required
def github_login(): def github_login():
if not github.authorized: if not github.authorized:
@ -304,7 +314,7 @@ if ub.oauth_support:
account_info = github.get('/user') account_info = github.get('/user')
if account_info.ok: if account_info.ok:
account_info_json = account_info.json() account_info_json = account_info.json()
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login') return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
flash(_(u"GitHub Oauth error, please retry later."), category="error") flash(_(u"GitHub Oauth error, please retry later."), category="error")
return redirect(url_for('web.login')) return redirect(url_for('web.login'))
@ -315,7 +325,7 @@ if ub.oauth_support:
return unlink_oauth(oauthblueprints[0]['id']) return unlink_oauth(oauthblueprints[0]['id'])
@oauth.route('/login/google') @oauth.route('/link/google')
@oauth_required @oauth_required
def google_login(): def google_login():
if not google.authorized: if not google.authorized:
@ -323,7 +333,7 @@ if ub.oauth_support:
resp = google.get("/oauth2/v2/userinfo") resp = google.get("/oauth2/v2/userinfo")
if resp.ok: if resp.ok:
account_info_json = resp.json() account_info_json = resp.json()
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login') return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
flash(_(u"Google Oauth error, please retry later."), category="error") flash(_(u"Google Oauth error, please retry later."), category="error")
return redirect(url_for('web.login')) return redirect(url_for('web.login'))

View File

@ -26,19 +26,22 @@ log = logger.create()
try: from . import goodreads_support try: from . import goodreads_support
except ImportError as err: except ImportError as err:
log.debug("cannot import goodreads, showing authors-metadata will not work: %s", err) log.debug("Cannot import goodreads, showing authors-metadata will not work: %s", err)
goodreads_support = None goodreads_support = None
try: from . import simpleldap as ldap try:
from . import simpleldap as ldap
from .simpleldap import ldapVersion
except ImportError as err: 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
ldapVersion = None
try: try:
from . import SyncToken as SyncToken from . import SyncToken as SyncToken
kobo = True kobo = True
except ImportError as err: except ImportError as err:
log.debug("cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err) log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
kobo = None kobo = None
SyncToken = None SyncToken = None

View File

@ -23,6 +23,10 @@ from flask_simpleldap import LDAP, LDAPException
from .. import constants, logger from .. import constants, logger
try:
from ldap.pkginfo import __version__ as ldapVersion
except ImportError:
pass
log = logger.create() log = logger.create()
_ldap = LDAP() _ldap = LDAP()
@ -34,44 +38,69 @@ def init_app(app, config):
app.config['LDAP_HOST'] = config.config_ldap_provider_url app.config['LDAP_HOST'] = config.config_ldap_provider_url
app.config['LDAP_PORT'] = config.config_ldap_port app.config['LDAP_PORT'] = config.config_ldap_port
app.config['LDAP_SCHEMA'] = config.config_ldap_schema if config.config_ldap_encryption == 2:
app.config['LDAP_USERNAME'] = config.config_ldap_user_object.replace('%s', config.config_ldap_serv_username)\ app.config['LDAP_SCHEMA'] = 'ldaps'
+ ',' + config.config_ldap_dn else:
app.config['LDAP_SCHEMA'] = 'ldap'
# app.config['LDAP_SCHEMA'] = config.config_ldap_schema
app.config['LDAP_USERNAME'] = config.config_ldap_serv_username
if config.config_ldap_serv_password is None:
config.config_ldap_serv_password = ''
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password) app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password)
app.config['LDAP_REQUIRE_CERT'] = bool(config.config_ldap_require_cert) if bool(config.config_ldap_cert_path):
if config.config_ldap_require_cert: app.config['LDAP_REQUIRE_CERT'] = True
app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path
app.config['LDAP_BASE_DN'] = config.config_ldap_dn app.config['LDAP_BASE_DN'] = config.config_ldap_dn
app.config['LDAP_USER_OBJECT_FILTER'] = config.config_ldap_user_object app.config['LDAP_USER_OBJECT_FILTER'] = config.config_ldap_user_object
app.config['LDAP_USE_SSL'] = bool(config.config_ldap_use_ssl)
app.config['LDAP_USE_TLS'] = bool(config.config_ldap_use_tls) app.config['LDAP_USE_TLS'] = bool(config.config_ldap_encryption == 1)
app.config['LDAP_USE_SSL'] = bool(config.config_ldap_encryption == 2)
app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap) app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap)
app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
# app.config['LDAP_CUSTOM_OPTIONS'] = {'OPT_NETWORK_TIMEOUT': 10}
_ldap.init_app(app) _ldap.init_app(app)
def get_object_details(user=None, group=None, query_filter=None, dn_only=False):
return _ldap.get_object_details(user, group, query_filter, dn_only)
def bind():
return _ldap.bind()
def get_group_members(group):
return _ldap.get_group_members(group)
def basic_auth_required(func): def basic_auth_required(func):
return _ldap.basic_auth_required(func) return _ldap.basic_auth_required(func)
def bind_user(username, password): def bind_user(username, password):
# ulf= _ldap.get_object_details('admin')
'''Attempts a LDAP login. '''Attempts a LDAP login.
:returns: True if login succeeded, False if login failed, None if server unavailable. :returns: True if login succeeded, False if login failed, None if server unavailable.
''' '''
try: try:
if _ldap.get_object_details(username):
result = _ldap.bind_user(username, password) result = _ldap.bind_user(username, password)
log.debug("LDAP login '%s': %r", username, result) log.debug("LDAP login '%s': %r", username, result)
return result is not None return result is not None, None
return None, None # User not found
except (TypeError, AttributeError) as ex:
error = ("LDAP bind_user: %s" % ex)
return None, error
except LDAPException as ex: except LDAPException as ex:
if ex.message == 'Invalid credentials': if ex.message == 'Invalid credentials':
log.info("LDAP login '%s' failed: %s", username, ex) error = ("LDAP admin login failed")
return False return None, error
if ex.message == "Can't contact LDAP server": if ex.message == "Can't contact LDAP server":
log.warning('LDAP Server down: %s', ex) # log.warning('LDAP Server down: %s', ex)
return None error = ('LDAP Server down: %s' % ex)
return None, error
else: else:
log.warning('LDAP Server error: %s', ex.message) error = ('LDAP Server error: %s' % ex.message)
return None return None, error

File diff suppressed because one or more lines are too long

View File

@ -29,15 +29,15 @@ $(function () {
var msg = i18nMsg; var msg = i18nMsg;
var douban = "https://api.douban.com"; var douban = "https://api.douban.com";
var dbSearch = "/v2/book/search"; var dbSearch = "/v2/book/search";
var dbDone = true; var dbDone = 0;
var google = "https://www.googleapis.com"; var google = "https://www.googleapis.com";
var ggSearch = "/books/v1/volumes"; var ggSearch = "/books/v1/volumes";
var ggDone = false; var ggDone = 0;
var comicvine = "https://comicvine.gamespot.com"; var comicvine = "https://comicvine.gamespot.com";
var cvSearch = "/api/search/"; var cvSearch = "/api/search/";
var cvDone = false; var cvDone = 0;
var showFlag = 0; var showFlag = 0;
@ -73,7 +73,9 @@ $(function () {
if (showFlag === 1) { if (showFlag === 1) {
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>"); $("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
} }
if (!ggDone && !dbDone) { if ((ggDone == 3 || (ggDone == 1 && ggResults.length === 0)) &&
(dbDone == 3 || (ggDone == 1 && dbResults.length === 0)) &&
(cvDone == 3 || (ggDone == 1 && cvResults.length === 0))) {
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>"); $("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
return; return;
} }
@ -93,7 +95,8 @@ $(function () {
return [year, month, day].join("-"); return [year, month, day].join("-");
} }
if (ggDone && ggResults.length > 0) { if (ggResults.length > 0) {
if (ggDone < 2) {
ggResults.forEach(function(result) { ggResults.forEach(function(result) {
var book = { var book = {
id: result.id, id: result.id,
@ -121,9 +124,14 @@ $(function () {
$("#book-list").append($book); $("#book-list").append($book);
}); });
ggDone = false; ggDone = 2;
} else {
ggDone = 3;
} }
if (dbDone && dbResults.length > 0) { }
if (dbResults.length > 0) {
if (dbDone < 2) {
dbResults.forEach(function(result) { dbResults.forEach(function(result) {
var seriesTitle = ""; var seriesTitle = "";
if (result.series) { if (result.series) {
@ -168,9 +176,13 @@ $(function () {
$("#book-list").append($book); $("#book-list").append($book);
}); });
dbDone = false; dbDone = 2;
} else {
dbDone = 3;
} }
if (cvDone && cvResults.length > 0) { }
if (cvResults.length > 0) {
if (cvDone < 2) {
cvResults.forEach(function(result) { cvResults.forEach(function(result) {
var seriesTitle = ""; var seriesTitle = "";
if (result.volume.name) { if (result.volume.name) {
@ -214,7 +226,10 @@ $(function () {
$("#book-list").append($book); $("#book-list").append($book);
}); });
cvDone = false; cvDone = 2;
} else {
cvDone = 3;
}
} }
} }
@ -227,11 +242,10 @@ $(function () {
success: function success(data) { success: function success(data) {
if ("items" in data) { if ("items" in data) {
ggResults = data.items; ggResults = data.items;
ggDone = true;
} }
}, },
complete: function complete() { complete: function complete() {
ggDone = true; ggDone = 1;
showResult(); showResult();
$("#show-google").trigger("change"); $("#show-google").trigger("change");
} }
@ -252,7 +266,7 @@ $(function () {
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML); $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
}, },
complete: function complete() { complete: function complete() {
dbDone = true; dbDone = 1;
showResult(); showResult();
$("#show-douban").trigger("change"); $("#show-douban").trigger("change");
} }
@ -274,7 +288,7 @@ $(function () {
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML); $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
}, },
complete: function complete() { complete: function complete() {
cvDone = true; cvDone = 1;
showResult(); showResult();
$("#show-comics").trigger("change"); $("#show-comics").trigger("change");
} }
@ -283,6 +297,10 @@ $(function () {
function doSearch (keyword) { function doSearch (keyword) {
showFlag = 0; showFlag = 0;
dbDone = ggDone = cvDone = 0;
dbResults = [];
ggResults = [];
cvResults = [];
$("#meta-info").text(msg.loading); $("#meta-info").text(msg.loading);
if (keyword) { if (keyword) {
dbSearchBook(keyword); dbSearchBook(keyword);

View File

@ -29,7 +29,6 @@ $(document).on("change", "input[type=\"checkbox\"][data-control]", function () {
}); });
}); });
// Generic control/related handler to show/hide fields based on a select' value // Generic control/related handler to show/hide fields based on a select' value
$(document).on("change", "select[data-control]", function() { $(document).on("change", "select[data-control]", function() {
var $this = $(this); var $this = $(this);
@ -39,13 +38,26 @@ $(document).on("change", "select[data-control]", function() {
for (var i = 0; i < $(this)[0].length; i++) { for (var i = 0; i < $(this)[0].length; i++) {
var element = parseInt($(this)[0][i].value); var element = parseInt($(this)[0][i].value);
if (element === showOrHide) { if (element === showOrHide) {
$("[data-related=" + name + "-" + element + "]").show(); $("[data-related^=" + name + "][data-related*=-" + element + "]").show();
} else { } else {
$("[data-related=" + name + "-" + element + "]").hide(); $("[data-related^=" + name + "][data-related*=-" + element + "]").hide();
} }
} }
}); });
// Generic control/related handler to show/hide fields based on a select' value
// this one is made to show all values if select value is not 0
$(document).on("change", "select[data-controlall]", function() {
var $this = $(this);
var name = $this.data("controlall");
var showOrHide = parseInt($this.val());
if (showOrHide) {
$("[data-related=" + name + "]").show();
} else {
$("[data-related=" + name + "]").hide();
}
});
$(function() { $(function() {
var updateTimerID; var updateTimerID;
@ -64,7 +76,7 @@ $(function() {
function cleanUp() { function cleanUp() {
clearInterval(updateTimerID); clearInterval(updateTimerID);
$("#spinner2").hide(); $("#spinner2").hide();
$("#updateFinished").removeClass("hidden"); $("#DialogFinished").removeClass("hidden");
$("#check_for_update").removeClass("hidden"); $("#check_for_update").removeClass("hidden");
$("#perform_update").addClass("hidden"); $("#perform_update").addClass("hidden");
$("#message").alert("close"); $("#message").alert("close");
@ -81,13 +93,13 @@ $(function() {
url: window.location.pathname + "/../../get_updater_status", url: window.location.pathname + "/../../get_updater_status",
success: function success(data) { success: function success(data) {
// console.log(data.status); // console.log(data.status);
$("#Updatecontent").html(updateText[data.status]); $("#DialogContent").html(updateText[data.status]);
if (data.status > 6) { if (data.status > 6) {
cleanUp(); cleanUp();
} }
}, },
error: function error() { error: function error() {
$("#Updatecontent").html(updateText[7]); $("#DialogContent").html(updateText[7]);
cleanUp(); cleanUp();
}, },
timeout: 2000 timeout: 2000
@ -146,8 +158,8 @@ $(function() {
var $this = $(this); var $this = $(this);
var buttonText = $this.html(); var buttonText = $this.html();
$this.html("..."); $this.html("...");
$("#Updatecontent").html(""); $("#DialogContent").html("");
$("#updateFinished").addClass("hidden"); $("#DialogFinished").addClass("hidden");
$("#update_error").addClass("hidden"); $("#update_error").addClass("hidden");
if ($("#message").length) { if ($("#message").length) {
$("#message").alert("close"); $("#message").alert("close");
@ -189,13 +201,24 @@ $(function() {
}); });
}); });
$("#restart_database").click(function() { $("#restart_database").click(function() {
$("#DialogHeader").addClass("hidden");
$("#DialogFinished").addClass("hidden");
$("#DialogContent").html("");
$("#spinner2").show();
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../shutdown", url: window.location.pathname + "/../../shutdown",
data: {"parameter":2} data: {"parameter":2},
success: function success(data) {
$("#spinner2").hide();
ResultText = data.text;
$("#DialogContent").html(ResultText);
$("#DialogFinished").removeClass("hidden");
}
}); });
}); });
$("#perform_update").click(function() { $("#perform_update").click(function() {
$("#DialogHeader").removeClass("hidden");
$("#spinner2").show(); $("#spinner2").show();
$.ajax({ $.ajax({
type: "POST", type: "POST",
@ -204,7 +227,7 @@ $(function() {
url: window.location.pathname + "/../../get_updater_status", url: window.location.pathname + "/../../get_updater_status",
success: function success(data) { success: function success(data) {
updateText = data.text; updateText = data.text;
$("#Updatecontent").html(updateText[data.status]); $("#DialogContent").html(updateText[data.status]);
// console.log(data.status); // console.log(data.status);
updateTimerID = setInterval(updateTimer, 2000); updateTimerID = setInterval(updateTimer, 2000);
} }
@ -214,6 +237,7 @@ $(function() {
// Init all data control handlers to default // Init all data control handlers to default
$("input[data-control]").trigger("change"); $("input[data-control]").trigger("change");
$("select[data-control]").trigger("change"); $("select[data-control]").trigger("change");
$("select[data-controlall]").trigger("change");
$("#bookDetailsModal") $("#bookDetailsModal")
.on("show.bs.modal", function(e) { .on("show.bs.modal", function(e) {
@ -274,6 +298,26 @@ $(function() {
$(".discover .row").isotope("layout"); $(".discover .row").isotope("layout");
}); });
$('#import_ldap_users').click(function() {
$("#DialogHeader").addClass("hidden");
$("#DialogFinished").addClass("hidden");
$("#DialogContent").html("");
$("#spinner2").show();
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
$.ajax({
method:"get",
dataType: "json",
url: path + "/../../import_ldap_users",
success: function success(data) {
$("#spinner2").hide();
ResultText = data.text;
$("#DialogContent").html(ResultText);
$("#DialogFinished").removeClass("hidden");
}
});
});
$(".author-expand").click(function() { $(".author-expand").click(function() {
$(this).parent().find("a.author-name").slice($(this).data("authors-max")).toggle(); $(this).parent().find("a.author-name").slice($(this).data("authors-max")).toggle();
$(this).parent().find("span.author-hidden-divider").toggle(); $(this).parent().find("span.author-hidden-divider").toggle();

View File

@ -36,6 +36,10 @@
{% endfor %} {% endfor %}
</table> </table>
<div class="btn btn-default" id="admin_new_user"><a href="{{url_for('admin.new_user')}}">{{_('Add New User')}}</a></div> <div class="btn btn-default" id="admin_new_user"><a href="{{url_for('admin.new_user')}}">{{_('Add New User')}}</a></div>
{% if (config.config_login_type == 1) %}
<div class="btn btn-default" id="import_ldap_users" data-toggle="modal" data-target="#StatusDialog">{{_('Import LDAP Users')}}</div>
<!--a href="#" id="import_ldap_users" name="import_ldap_users"><button type="submit" class="btn btn-default">{{_('Import LDAP Users')}}</button></a-->
{% endif %}
</div> </div>
</div> </div>
@ -120,7 +124,7 @@
<div class="col"> <div class="col">
<h2>{{_('Administration')}}</h2> <h2>{{_('Administration')}}</h2>
<div class="btn btn-default"><a id="logfile" href="{{url_for('admin.view_logfile')}}">{{_('View Logs')}}</a></div> <div class="btn btn-default"><a id="logfile" href="{{url_for('admin.view_logfile')}}">{{_('View Logs')}}</a></div>
<div class="btn btn-default" id="restart_database">{{_('Reconnect Calibre Database')}}</div> <div class="btn btn-default" id="restart_database" data-toggle="modal" data-target="#StatusDialog">{{_('Reconnect Calibre Database')}}</div>
<div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div> <div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div>
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div> <div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div>
</div> </div>
@ -146,7 +150,7 @@
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div> <div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
<div class="btn btn-default" id="check_for_update">{{_('Check for Update')}}</div> <div class="btn btn-default" id="check_for_update">{{_('Check for Update')}}</div>
<div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#UpdateprogressDialog">{{_('Perform Update')}}</div> <div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div>
</div> </div>
</div> </div>
</div> </div>
@ -183,21 +187,21 @@
</div> </div>
</div> </div>
</div> </div>
<div id="UpdateprogressDialog" class="modal fade" role="dialog"> <div id="StatusDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">
<!-- Modal content--> <!-- Modal content-->
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-info text-center"> <div class="modal-header bg-info text-center">
<span>{{_('Updating, please do not reload this page')}}</span> <span id="DialogHeader">{{_('Updating, please do not reload this page')}}</span>
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">
<div id="spinner2" class="spinner2" style="display:none;"> <div id="spinner2" class="spinner2" style="display:none;">
<img id="img-spinner2" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/> <img id="img-spinner2" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/>
</div> </div>
<p></p> <p></p>
<div id="Updatecontent"></div> <div id="DialogContent"></div>
<p></p> <p></p>
<button type="button" class="btn btn-default hidden" id="updateFinished" data-dismiss="modal">{{_('OK')}}</button> <button type="button" class="btn btn-default hidden" id="DialogFinished" data-dismiss="modal">{{_('OK')}}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -198,6 +198,17 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="form-group">
<input type="checkbox" id="config_allow_reverse_proxy_header_login" name="config_allow_reverse_proxy_header_login" data-control="reverse-proxy-login-settings" {% if config.config_allow_reverse_proxy_header_login %}checked{% endif %}>
<label for="config_allow_reverse_proxy_header_login">{{_('Allow Reverse Proxy Authentication')}}</label>
</div>
<div data-related="reverse-proxy-login-settings">
<div class="form-group">
<label for="config_reverse_proxy_login_header_name">{{_('Reverse Proxy Header Name')}}</label>
<input type="text" class="form-control" id="config_reverse_proxy_login_header_name" name="config_reverse_proxy_login_header_name" value="{% if config.config_reverse_proxy_login_header_name != None %}{{ config.config_reverse_proxy_login_header_name }}{% endif %}" autocomplete="off">
</div>
</div>
{% if not config.config_is_initial %}
{% if feature_support['ldap'] or feature_support['oauth'] %} {% if feature_support['ldap'] or feature_support['oauth'] %}
<div class="form-group"> <div class="form-group">
<label for="config_login_type">{{_('Login type')}}</label> <label for="config_login_type">{{_('Login type')}}</label>
@ -219,37 +230,31 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="config_ldap_port">{{_('LDAP Server Port')}}</label> <label for="config_ldap_port">{{_('LDAP Server Port')}}</label>
<input type="text" class="form-control" id="config_ldap_port" name="config_ldap_port" value="{% if config.config_ldap_port != None %}{{ config.config_ldap_port }}{% endif %}" autocomplete="off"> <input type="number" min="1" max="65535" class="form-control" id="config_ldap_port" name="config_ldap_port" value="{% if config.config_ldap_port != None %}{{ config.config_ldap_port }}{% endif %}" autocomplete="off" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="config_ldap_schema">{{_('LDAP Schema (LDAP or LPAPS)')}}</label> <label for="config_ldap_encryption">{{_('LDAP Encryption')}}</label>
<input type="text" class="form-control" id="config_ldap_schema" name="config_ldap_schema" value="{% if config.config_ldap_schema != None %}{{ config.config_ldap_schema }}{% endif %}" autocomplete="off"> <label for="config_ldap_encryption">{{_('LDAP Encryption')}}</label>
<select name="config_ldap_encryption" id="config_ldap_encryption" class="form-control" data-controlall="ldap-cert-settings">
<option value="0" {% if config.config_ldap_encryption == 0 %}selected{% endif %}>{{ _('None') }}</option>
<option value="1" {% if config.config_ldap_encryption == 1 %}selected{% endif %}>{{ _('TLS') }}</option>
<option value="2" {% if config.config_ldap_encryption == 2 %}selected{% endif %}>{{ _('SSL') }}</option>
</select>
</div> </div>
<div data-related="ldap-cert-settings">
<div class="form-group">
<label for="config_ldap_cert_path">{{_('LDAP Certificate Path')}}</label>
<input type="text" class="form-control" id="config_ldap_cert_path" name="config_ldap_cert_path" value="{% if config.config_ldap_cert_path != None %}{{ config.config_ldap_cert_path }}{% endif %}" autocomplete="off">
</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="config_ldap_serv_username">{{_('LDAP Administrator Username')}}</label> <label for="config_ldap_serv_username">{{_('LDAP Administrator Username')}}</label>
<input type="text" class="form-control" id="config_ldap_serv_username" name="config_ldap_serv_username" value="{% if config.config_ldap_serv_username != None %}{{ config.config_ldap_serv_username }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_ldap_serv_username" name="config_ldap_serv_username" value="{% if config.config_ldap_serv_username != None %}{{ config.config_ldap_serv_username }}{% endif %}" autocomplete="off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="config_ldap_serv_password">{{_('LDAP Administrator Password')}}</label> <label for="config_ldap_serv_password">{{_('LDAP Administrator Password')}}</label>
<input type="password" class="form-control" id="config_ldap_serv_password" name="config_ldap_serv_password" value="{% if config.config_ldap_serv_password != None %}{{ config.config_ldap_serv_password }}{% endif %}" autocomplete="off"> <input type="password" class="form-control" id="config_ldap_serv_password" name="config_ldap_serv_password" value="" autocomplete="off">
</div>
<div class="form-group">
<input type="checkbox" id="config_ldap_use_ssl" name="config_ldap_use_ssl" {% if config.config_ldap_use_ssl %}checked{% endif %}>
<label for="config_ldap_use_ssl">{{_('LDAP Server Enable SSL')}}</label>
</div>
<div class="form-group">
<input type="checkbox" id="config_ldap_use_tls" name="config_ldap_use_tls" {% if config.config_ldap_use_tls %}checked{% endif %}>
<label for="config_ldap_use_tls">{{_('LDAP Server Enable TLS')}}</label>
</div>
<div class="form-group">
<input type="checkbox" id="config_ldap_require_cert" name="config_ldap_require_cert" data-control="ldap-cert-settings" {% if config.config_ldap_require_cert %}checked{% endif %}>
<label for="config_ldap_require_cert">{{_('LDAP Server Certificate')}}</label>
</div>
<div data-related="ldap-cert-settings">
<div class="form-group">
<label for="config_ldap_cert_path">{{_('LDAP SSL Certificate Path')}}</label>
<input type="text" class="form-control" id="config_ldap_cert_path" name="config_ldap_cert_path" value="{% if config.config_ldap_cert_path != None and config.config_ldap_require_cert !=None %}{{ config.config_ldap_cert_path }}{% endif %}" autocomplete="off">
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="config_ldap_dn">{{_('LDAP Distinguished Name (DN)')}}</label> <label for="config_ldap_dn">{{_('LDAP Distinguished Name (DN)')}}</label>
@ -263,6 +268,19 @@
<input type="checkbox" id="config_ldap_openldap" name="config_ldap_openldap" {% if config.config_ldap_openldap %}checked{% endif %}> <input type="checkbox" id="config_ldap_openldap" name="config_ldap_openldap" {% if config.config_ldap_openldap %}checked{% endif %}>
<label for="config_ldap_openldap">{{_('LDAP Server is OpenLDAP?')}}</label> <label for="config_ldap_openldap">{{_('LDAP Server is OpenLDAP?')}}</label>
</div> </div>
<h4 class="text-center">{{_('Following Settings are Needed For User Import')}}</h4>
<div class="form-group">
<label for="config_ldap_group_object_filter">{{_('LDAP Group Object Filter')}}</label>
<input type="text" class="form-control" id="config_ldap_group_object_filter" name="config_ldap_group_object_filter" value="{% if config.config_ldap_group_object_filter != None %}{{ config.config_ldap_group_object_filter }}{% endif %}" autocomplete="off">
</div>
<div class="form-group">
<label for="config_ldap_group_name">{{_('LDAP Group Name')}}</label>
<input type="text" class="form-control" id="config_ldap_group_name" name="config_ldap_group_name" value="{% if config.config_ldap_group_name != None %}{{ config.config_ldap_group_name }}{% endif %}" autocomplete="off">
</div>
<div class="form-group">
<label for="config_ldap_group_members_field">{{_('LDAP Group Members Field')}}</label>
<input type="text" class="form-control" id="config_ldap_group_members_field" name="config_ldap_group_members_field" value="{% if config.config_ldap_group_members_field != None %}{{ config.config_ldap_group_members_field }}{% endif %}" autocomplete="off">
</div>
</div> </div>
{% endif %} {% endif %}
{% if feature_support['oauth'] %} {% if feature_support['oauth'] %}
@ -283,16 +301,7 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="form-group"> {% endif %}
<input type="checkbox" id="config_allow_reverse_proxy_header_login" name="config_allow_reverse_proxy_header_login" data-control="reverse-proxy-login-settings" {% if config.config_allow_reverse_proxy_header_login %}checked{% endif %}>
<label for="config_allow_reverse_proxy_header_login">{{_('Allow Reverse Proxy Authentication')}}</label>
</div>
<div data-related="reverse-proxy-login-settings">
<div class="form-group">
<label for="config_reverse_proxy_login_header_name">{{_('Reverse Proxy Header Name')}}</label>
<input type="text" class="form-control" id="config_reverse_proxy_login_header_name" name="config_reverse_proxy_login_header_name" value="{% if config.config_reverse_proxy_login_header_name != None %}{{ config.config_reverse_proxy_login_header_name }}{% endif %}" autocomplete="off">
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -25,20 +25,22 @@
<a href="{{url_for('web.remote_login')}}" class="pull-right">{{_('Log in with Magic Link')}}</a> <a href="{{url_for('web.remote_login')}}" class="pull-right">{{_('Log in with Magic Link')}}</a>
{% endif %} {% endif %}
{% if config.config_login_type == 2 %} {% if config.config_login_type == 2 %}
<a href="{{url_for('oauth.github_login')}}" class="pull-right"> {% if 1 in oauth_check %}
<a href="{{url_for('oauth.github_login')}}" class="pull-right github">
<svg height="32" class="octicon octicon-mark-github" viewBox="0 0 16 16" version="1.1" width="32" aria-hidden="true"> <svg height="32" class="octicon octicon-mark-github" viewBox="0 0 16 16" version="1.1" width="32" aria-hidden="true">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path> <path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg> </svg>
</a> </a>
{% endif %} {% endif %}
{% if config.config_login_type == 3 %} {% if 2 in oauth_check %}
<a href="{{url_for('oauth.google_login')}}" class="pull-right"> <a href="{{url_for('oauth.google_login')}}" class="pull-right google">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="40" height="40" width="40" height="40"
viewBox="0 3 48 49" viewBox="0 3 48 49"
style="fill:#000000;"><g id="surface1"><path style=" fill:#FFC107;" d="M 43.609375 20.082031 L 42 20.082031 L 42 20 L 24 20 L 24 28 L 35.304688 28 C 33.652344 32.65625 29.222656 36 24 36 C 17.371094 36 12 30.628906 12 24 C 12 17.371094 17.371094 12 24 12 C 27.058594 12 29.84375 13.152344 31.960938 15.039063 L 37.617188 9.382813 C 34.046875 6.054688 29.269531 4 24 4 C 12.953125 4 4 12.953125 4 24 C 4 35.046875 12.953125 44 24 44 C 35.046875 44 44 35.046875 44 24 C 44 22.660156 43.863281 21.351563 43.609375 20.082031 Z "></path><path style=" fill:#FF3D00;" d="M 6.304688 14.691406 L 12.878906 19.511719 C 14.65625 15.109375 18.960938 12 24 12 C 27.058594 12 29.84375 13.152344 31.960938 15.039063 L 37.617188 9.382813 C 34.046875 6.054688 29.269531 4 24 4 C 16.316406 4 9.65625 8.335938 6.304688 14.691406 Z "></path><path style=" fill:#4CAF50;" d="M 24 44 C 29.164063 44 33.859375 42.023438 37.410156 38.808594 L 31.21875 33.570313 C 29.210938 35.089844 26.714844 36 24 36 C 18.796875 36 14.382813 32.683594 12.71875 28.054688 L 6.195313 33.078125 C 9.503906 39.554688 16.226563 44 24 44 Z "></path><path style=" fill:#1976D2;" d="M 43.609375 20.082031 L 42 20.082031 L 42 20 L 24 20 L 24 28 L 35.304688 28 C 34.511719 30.238281 33.070313 32.164063 31.214844 33.570313 C 31.21875 33.570313 31.21875 33.570313 31.21875 33.570313 L 37.410156 38.808594 C 36.972656 39.203125 44 34 44 24 C 44 22.660156 43.863281 21.351563 43.609375 20.082031 Z "></path></g></svg> style="fill:#000000;"><g id="surface1"><path style=" fill:#FFC107;" d="M 43.609375 20.082031 L 42 20.082031 L 42 20 L 24 20 L 24 28 L 35.304688 28 C 33.652344 32.65625 29.222656 36 24 36 C 17.371094 36 12 30.628906 12 24 C 12 17.371094 17.371094 12 24 12 C 27.058594 12 29.84375 13.152344 31.960938 15.039063 L 37.617188 9.382813 C 34.046875 6.054688 29.269531 4 24 4 C 12.953125 4 4 12.953125 4 24 C 4 35.046875 12.953125 44 24 44 C 35.046875 44 44 35.046875 44 24 C 44 22.660156 43.863281 21.351563 43.609375 20.082031 Z "></path><path style=" fill:#FF3D00;" d="M 6.304688 14.691406 L 12.878906 19.511719 C 14.65625 15.109375 18.960938 12 24 12 C 27.058594 12 29.84375 13.152344 31.960938 15.039063 L 37.617188 9.382813 C 34.046875 6.054688 29.269531 4 24 4 C 16.316406 4 9.65625 8.335938 6.304688 14.691406 Z "></path><path style=" fill:#4CAF50;" d="M 24 44 C 29.164063 44 33.859375 42.023438 37.410156 38.808594 L 31.21875 33.570313 C 29.210938 35.089844 26.714844 36 24 36 C 18.796875 36 14.382813 32.683594 12.71875 28.054688 L 6.195313 33.078125 C 9.503906 39.554688 16.226563 44 24 44 Z "></path><path style=" fill:#1976D2;" d="M 43.609375 20.082031 L 42 20.082031 L 42 20 L 24 20 L 24 28 L 35.304688 28 C 34.511719 30.238281 33.070313 32.164063 31.214844 33.570313 C 31.21875 33.570313 31.21875 33.570313 31.21875 33.570313 L 37.410156 38.808594 C 36.972656 39.203125 44 34 44 24 C 44 22.660156 43.863281 21.351563 43.609375 20.082031 Z "></path></g></svg>
</a> </a>
{% endif %} {% endif %}
{% endif %}
</form> </form>
</div> </div>
{% if error %} {% if error %}

View File

@ -35,10 +35,12 @@
</thead> </thead>
<tbody> <tbody>
{% for library,version in versions.items() %} {% for library,version in versions.items() %}
{% if version %}
<tr> <tr>
<th>{{library}}</th> <th>{{library}}</th>
<td>{{_(version)}}</td> <td>{{_(version)}}</td>
</tr> </tr>
{% endif %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@ -46,7 +46,7 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
{% if registered_oauth.keys()| length > 0 %} {% if registered_oauth.keys()| length > 0 and not new_user %}
{% for id, name in registered_oauth.items() %} {% for id, name in registered_oauth.items() %}
<div class="form-group"> <div class="form-group">
<label>{{ name }} {{_('OAuth Settings')}}</label> <label>{{ name }} {{_('OAuth Settings')}}</label>
@ -161,7 +161,7 @@
</div> </div>
<div class="modal-body">...</div> <div class="modal-body">...</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button> <button type="button" id="kobo_close" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -30,6 +30,11 @@ from werkzeug.local import LocalProxy
try: try:
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
oauth_support = True oauth_support = True
except ImportError:
# fails on flask-dance >1.3, due to renaming
try:
from flask_dance.consumer.storage.sqla import OAuthConsumerMixin
oauth_support = True
except ImportError: except ImportError:
oauth_support = False oauth_support = False
from sqlalchemy import create_engine, exc, exists, event from sqlalchemy import create_engine, exc, exists, event
@ -237,7 +242,7 @@ class Anonymous(AnonymousUserMixin, UserBase):
self.sidebar_view = data.sidebar_view self.sidebar_view = data.sidebar_view
self.default_language = data.default_language self.default_language = data.default_language
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 self.denied_tags = data.denied_tags
self.allowed_tags = data.allowed_tags self.allowed_tags = data.allowed_tags
@ -531,14 +536,12 @@ def migrate_Database(session):
"locale VARCHAR(2)," "locale VARCHAR(2),"
"sidebar_view INTEGER," "sidebar_view INTEGER,"
"default_language VARCHAR(3)," "default_language VARCHAR(3),"
"mature_content BOOLEAN,"
"UNIQUE (nickname)," "UNIQUE (nickname),"
"UNIQUE (email)," "UNIQUE (email))")
"CHECK (mature_content IN (0, 1)))")
conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale,"
"sidebar_view, default_language, mature_content) " "sidebar_view, default_language) "
"SELECT id, nickname, email, role, password, kindle_mail, locale," "SELECT id, nickname, email, role, password, kindle_mail, locale,"
"sidebar_view, default_language, mature_content FROM user") "sidebar_view, default_language FROM user")
# delete old user table and rename new user_id table to user: # delete old user table and rename new user_id table to user:
conn.execute("DROP TABLE user") conn.execute("DROP TABLE user")
conn.execute("ALTER TABLE user_id RENAME TO user") conn.execute("ALTER TABLE user_id RENAME TO user")

View File

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

View File

@ -1,16 +1,16 @@
# GDrive Integration # GDrive Integration
google-api-python-client==1.7.11,<1.8.0 google-api-python-client==1.7.11,<1.8.0
gevent>=1.2.1,<1.5.0 gevent>=1.2.1,<1.6.0
greenlet>=0.4.12,<0.5.0 greenlet>=0.4.12,<0.5.0
httplib2>=0.9.2,<0.18.0 httplib2>=0.9.2,<0.18.0
oauth2client>=4.0.0,<4.14.0 oauth2client>=4.0.0,<4.14.0
uritemplate>=3.0.0,<3.1.0 uritemplate>=3.0.0,<3.1.0
pyasn1-modules>=0.0.8,<0.3.0 pyasn1-modules>=0.0.8,<0.3.0
pyasn1>=0.1.9,<0.5.0 pyasn1>=0.1.9,<0.5.0
PyDrive>=1.3.1,<1.14.0 PyDrive>=1.3.1,<1.4.0
PyYAML>=3.12 PyYAML>=3.12
rsa==3.4.2,<4.1.0 rsa==3.4.2,<4.1.0
six>=1.10.0,<1.14.0 six>=1.10.0,<1.15.0
# goodreads # goodreads
goodreads>=0.3.2,<0.4.0 goodreads>=0.3.2,<0.4.0
@ -18,15 +18,15 @@ python-Levenshtein>=0.12.0,<0.13.0
# ldap login # ldap login
python_ldap>=3.0.0,<3.3.0 python_ldap>=3.0.0,<3.3.0
flask-simpleldap>1.3.0,<1.5.0 flask-simpleldap>=1.4.0,<1.5.0
#oauth #oauth
flask-dance>=0.13.0 flask-dance>=1.4.0,<3.1.0
sqlalchemy_utils>=0.33.5,<0.37.0 sqlalchemy_utils>=0.33.5,<0.37.0
# extracting metadata # extracting metadata
lxml>=3.8.0,<4.6.0 lxml>=3.8.0,<4.6.0
Pillow>=4.0.0,<7.1.0 Pillow>=4.0.0,<7.2.0
rarfile>=2.7 rarfile>=2.7
# other # other

View File

@ -1,14 +1,14 @@
Babel>=1.3, <2.9 Babel>=1.3, <2.9
Flask-Babel>=0.11.1,<1.1.0 Flask-Babel>=0.11.1,<1.1.0
Flask-Login>=0.3.2,<0.5.1 Flask-Login>=0.3.2,<0.5.1
Flask-Principal>=0.3.2,<0.5.0 Flask-Principal>=0.3.2,<0.5.1
singledispatch>=3.4.0.0,<3.5.0.0 singledispatch>=3.4.0.0,<3.5.0.0
backports_abc>=0.4 backports_abc>=0.4
Flask>=1.0.2,<1.2.0 Flask>=1.0.2,<1.2.0
iso-639>=0.4.5,<0.5.0 iso-639>=0.4.5,<0.5.0
PyPDF2==1.26.0,<1.27.0 PyPDF2==1.26.0,<1.27.0
pytz>=2016.10 pytz>=2016.10
requests>=2.11.1,<2.23.0 requests>=2.11.1,<2.24.0
SQLAlchemy>=1.1.0,<1.4.0 SQLAlchemy>=1.1.0,<1.4.0
tornado>=4.1,<6.1 tornado>=4.1,<6.1
Wand>=0.4.4,<0.6.0 Wand>=0.4.4,<0.6.0

709
test/Calibre-Web TestSummary.html Normal file → Executable file

File diff suppressed because it is too large Load Diff