Merge branch 'master' into Develop
# Conflicts: # cps/config_sql.py # cps/ub.py # cps/web.py
This commit is contained in:
commit
9e159ed5ab
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
13
cps/about.py
13
cps/about.py
|
@ -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())
|
||||||
|
|
||||||
|
|
117
cps/admin.py
117
cps/admin.py
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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])
|
||||||
|
|
15
cps/oauth.py
15
cps/oauth.py
|
@ -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
|
||||||
|
|
108
cps/oauth_bb.py
108
cps/oauth_bb.py
|
@ -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'))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
cps/static/css/caliBlur.min.css
vendored
2
cps/static/css/caliBlur.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
15
cps/ub.py
15
cps/ub.py
|
@ -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")
|
||||||
|
|
161
cps/web.py
161
cps/web.py
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
709
test/Calibre-Web TestSummary.html
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user