Merge branch 'master' into Develop

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

View File

@ -17,6 +17,9 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
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**
A clear and concise description of what you expected to happen.

View File

@ -23,12 +23,12 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- Send eBooks to Kindle devices with the click of a button
- 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)
- Upload new books in many formats
- Support for Calibre custom columns
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
- Support for Calibre Custom Columns
- Ability to hide content based on categories and Custom Column content per user
- Self-update capability
- "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

View File

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

View File

@ -42,8 +42,10 @@ from .helper import speaking_language, check_valid_domain, send_test_mail, reset
from .gdriveutils import is_gdrive_ready, gdrive_support
from .web import admin_required, render_title_template, before_request, unconfigured, login_required_if_no_ano
log = logger.create()
feature_support = {
'ldap': False, # bool(services.ldap),
'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo)
}
@ -57,7 +59,8 @@ feature_support = {
try:
from .oauth_bb import oauth_check, oauthblueprints
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
oauthblueprints = []
oauth_check = {}
@ -65,7 +68,7 @@ except ImportError:
feature_support['gdrive'] = gdrive_support
admi = Blueprint('admin', __name__)
log = logger.create()
@admi.route("/admin")
@ -79,12 +82,12 @@ def admin_forbidden():
@admin_required
def shutdown():
task = int(request.args.get("parameter").strip())
showtext = {}
if task in (0, 1): # valid commandos received
# close all database connections
db.dispose()
ub.dispose()
showtext = {}
if task == 0:
showtext['text'] = _(u'Server restarted, please reload page')
else:
@ -96,9 +99,11 @@ def shutdown():
if task == 2:
log.warning("reconnecting to calibre database")
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")
@ -502,7 +507,7 @@ def _configuration_update_helper():
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
gdrive_secrets = json.load(settings)['web']
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(
gdrive_secrets['client_id'],
gdrive_secrets['client_secret'],
@ -518,11 +523,11 @@ def _configuration_update_helper():
reboot_required |= _config_string("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")
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_anonbrowse")
@ -540,26 +545,57 @@ def _configuration_update_helper():
#LDAP configurator,
if config.config_login_type == constants.LOGIN_LDAP:
_config_string("config_ldap_provider_url")
_config_int("config_ldap_port")
_config_string("config_ldap_schema")
_config_string("config_ldap_dn")
_config_string("config_ldap_user_object")
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:
return _configuration_result('Please enter a LDAP provider, port, DN and user object identifier', gdriveError)
reboot_required |= _config_string("config_ldap_provider_url")
reboot_required |= _config_int("config_ldap_port")
# _config_string("config_ldap_schema")
reboot_required |= _config_string("config_ldap_dn")
reboot_required |= _config_string("config_ldap_serv_username")
reboot_required |= _config_string("config_ldap_user_object")
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_serv_username or "config_ldap_serv_password" not in to_save:
return _configuration_result('Please enter a LDAP service account and password', gdriveError)
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode)
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:
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")
_config_checkbox("config_ldap_openldap")
_config_checkbox("config_ldap_require_cert")
_config_string("config_ldap_cert_path")
if config.config_ldap_cert_path and not os.path.isfile(config.config_ldap_cert_path):
return _configuration_result('LDAP Certfile location is not valid, please enter correct path', gdriveError)
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password):
return _configuration_result('Please Enter a LDAP Service Account and Password', gdriveError)
#_config_checkbox("config_ldap_use_ssl")
#_config_checkbox("config_ldap_use_tls")
# 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
_config_checkbox("config_remote_login")
@ -586,33 +622,32 @@ def _configuration_update_helper():
active_oauths = 0
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'] \
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
reboot_required = True
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"]
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:
ub.session.query(ub.OAuthProvider).filter(ub.OAuthProvider.id == element['id']).update(
{"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_string("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_string("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
_config_string("config_rarfile_location")
@ -631,7 +666,7 @@ def _configuration_update_helper():
if db_change:
# reload(db)
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()
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
if error_flash:
config.load()
flash(_(error_flash), category="error")
flash(error_flash, category="error")
show_login_button = False
return render_title_template("config_edit.html", config=config, provider=oauthblueprints,

View File

@ -71,7 +71,7 @@ class _Settings(_Base):
config_kobo_sync = Column(Boolean, default=False)
config_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_denied_tags = Column(String, default="")
@ -93,18 +93,21 @@ class _Settings(_Base):
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_schema = Column(String, default='ldap')
config_ldap_serv_username = Column(String)
config_ldap_serv_password = Column(String)
config_ldap_use_ssl = Column(Boolean, default=False)
config_ldap_use_tls = Column(Boolean, default=False)
config_ldap_require_cert = Column(Boolean, default=False)
config_ldap_cert_path = Column(String)
config_ldap_dn = Column(String)
config_ldap_user_object = Column(String)
config_ldap_openldap = Column(Boolean, default=False)
# config_ldap_schema = Column(String, default='ldap')
config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
config_ldap_serv_password = Column(String, default="")
config_ldap_encryption = Column(SmallInteger, default=0)
# config_ldap_use_tls = Column(Boolean, default=False)
# config_ldap_require_cert = Column(Boolean, default=False)
config_ldap_cert_path = Column(String, default="")
config_ldap_dn = Column(String, default='dc=example,dc=org')
config_ldap_user_object = Column(String, default='uid=%s')
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_converterpath = Column(String)
@ -212,7 +215,7 @@ class _ConfigSQL(object):
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.
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
if convertor is not None:
if encode:
new_value = convertor(new_value.encode(encode))
else:
new_value = convertor(new_value)
current_value = self.__dict__.get(field)
@ -277,7 +283,9 @@ class _ConfigSQL(object):
self._session.commit()
self.load()
def invalidate(self):
def invalidate(self, error=None):
if error:
log.error(error)
log.warning("invalidating configuration")
self.db_configured = False
self.config_calibre_dir = None

View File

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

View File

@ -796,6 +796,7 @@ def fill_indexpage_with_archived_books(page, database, db_filter, order, allow_s
def get_typeahead(database, query, replace=('',''), tag_filter=true()):
query = query or ''
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()
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])

View File

@ -23,7 +23,18 @@ from flask import session
try:
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
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):
"""
Stores and retrieves OAuth tokens using a relational database through
@ -39,7 +50,7 @@ try:
def get(self, blueprint, user=None, user_id=None):
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
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
token = self.cache.get(cache_key)
@ -152,5 +163,5 @@ try:
blueprint=blueprint, user=user, user_id=user_id,
))
except ImportError:
except Exception:
pass

View File

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

View File

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

View File

@ -23,6 +23,10 @@ from flask_simpleldap import LDAP, LDAPException
from .. import constants, logger
try:
from ldap.pkginfo import __version__ as ldapVersion
except ImportError:
pass
log = logger.create()
_ldap = LDAP()
@ -34,44 +38,69 @@ def init_app(app, config):
app.config['LDAP_HOST'] = config.config_ldap_provider_url
app.config['LDAP_PORT'] = config.config_ldap_port
app.config['LDAP_SCHEMA'] = config.config_ldap_schema
app.config['LDAP_USERNAME'] = config.config_ldap_user_object.replace('%s', config.config_ldap_serv_username)\
+ ',' + config.config_ldap_dn
if config.config_ldap_encryption == 2:
app.config['LDAP_SCHEMA'] = 'ldaps'
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_REQUIRE_CERT'] = bool(config.config_ldap_require_cert)
if config.config_ldap_require_cert:
if bool(config.config_ldap_cert_path):
app.config['LDAP_REQUIRE_CERT'] = True
app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path
app.config['LDAP_BASE_DN'] = config.config_ldap_dn
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_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)
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):
return _ldap.basic_auth_required(func)
def bind_user(username, password):
# ulf= _ldap.get_object_details('admin')
'''Attempts a LDAP login.
:returns: True if login succeeded, False if login failed, None if server unavailable.
'''
try:
if _ldap.get_object_details(username):
result = _ldap.bind_user(username, password)
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:
if ex.message == 'Invalid credentials':
log.info("LDAP login '%s' failed: %s", username, ex)
return False
error = ("LDAP admin login failed")
return None, error
if ex.message == "Can't contact LDAP server":
log.warning('LDAP Server down: %s', ex)
return None
# log.warning('LDAP Server down: %s', ex)
error = ('LDAP Server down: %s' % ex)
return None, error
else:
log.warning('LDAP Server error: %s', ex.message)
return None
error = ('LDAP Server error: %s' % ex.message)
return None, error

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -36,6 +36,10 @@
{% endfor %}
</table>
<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>
@ -120,7 +124,7 @@
<div class="col">
<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" 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_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div>
</div>
@ -146,7 +150,7 @@
<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 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>
@ -183,21 +187,21 @@
</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">
<!-- Modal content-->
<div class="modal-content">
<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 class="modal-body text-center">
<div id="spinner2" class="spinner2" style="display:none;">
<img id="img-spinner2" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/>
</div>
<p></p>
<div id="Updatecontent"></div>
<div id="DialogContent"></div>
<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>

View File

@ -198,6 +198,17 @@
</div>
</div>
{% 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'] %}
<div class="form-group">
<label for="config_login_type">{{_('Login type')}}</label>
@ -219,37 +230,31 @@
</div>
<div class="form-group">
<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 class="form-group">
<label for="config_ldap_schema">{{_('LDAP Schema (LDAP or LPAPS)')}}</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>
<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 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">
<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">
</div>
<div class="form-group">
<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">
</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>
<input type="password" class="form-control" id="config_ldap_serv_password" name="config_ldap_serv_password" value="" autocomplete="off">
</div>
<div class="form-group">
<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 %}>
<label for="config_ldap_openldap">{{_('LDAP Server is OpenLDAP?')}}</label>
</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>
{% endif %}
{% if feature_support['oauth'] %}
@ -283,16 +301,7 @@
</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>
{% endif %}
</div>
</div>
</div>

View File

@ -25,20 +25,22 @@
<a href="{{url_for('web.remote_login')}}" class="pull-right">{{_('Log in with Magic Link')}}</a>
{% endif %}
{% 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">
<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>
</a>
{% endif %}
{% if config.config_login_type == 3 %}
<a href="{{url_for('oauth.google_login')}}" class="pull-right">
{% if 2 in oauth_check %}
<a href="{{url_for('oauth.google_login')}}" class="pull-right google">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="40" height="40"
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>
</a>
{% endif %}
{% endif %}
</form>
</div>
{% if error %}

View File

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

View File

@ -46,7 +46,7 @@
{% endfor %}
</select>
</div>
{% if registered_oauth.keys()| length > 0 %}
{% if registered_oauth.keys()| length > 0 and not new_user %}
{% for id, name in registered_oauth.items() %}
<div class="form-group">
<label>{{ name }} {{_('OAuth Settings')}}</label>
@ -161,7 +161,7 @@
</div>
<div class="modal-body">...</div>
<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>

View File

@ -30,6 +30,11 @@ from werkzeug.local import LocalProxy
try:
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
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:
oauth_support = False
from sqlalchemy import create_engine, exc, exists, event
@ -237,7 +242,7 @@ class Anonymous(AnonymousUserMixin, UserBase):
self.sidebar_view = data.sidebar_view
self.default_language = data.default_language
self.locale = data.locale
self.mature_content = data.mature_content
# self.mature_content = data.mature_content
self.kindle_mail = data.kindle_mail
self.denied_tags = data.denied_tags
self.allowed_tags = data.allowed_tags
@ -531,14 +536,12 @@ def migrate_Database(session):
"locale VARCHAR(2),"
"sidebar_view INTEGER,"
"default_language VARCHAR(3),"
"mature_content BOOLEAN,"
"UNIQUE (nickname),"
"UNIQUE (email),"
"CHECK (mature_content IN (0, 1)))")
"UNIQUE (email))")
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,"
"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:
conn.execute("DROP TABLE user")
conn.execute("ALTER TABLE user_id RENAME TO user")

View File

@ -28,6 +28,7 @@ import json
import mimetypes
import traceback
import binascii
import re
from babel import Locale as LC
from babel.dates import format_date
@ -53,13 +54,14 @@ from .pagination import Pagination
from .redirect import redirect_back
feature_support = {
'ldap': False, # bool(services.ldap),
'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo)
}
try:
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
feature_support['oauth'] = True
except ImportError:
feature_support['oauth'] = False
@ -109,14 +111,15 @@ for ex in default_exceptions:
elif ex == 500:
app.register_error_handler(ex, internal_error)
web = Blueprint('web', __name__)
log = logger.create()
# ################################### Login logic and rights management ###############################################
def _fetch_user_by_name(username):
return ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first()
@lm.user_loader
def load_user(user_id):
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:
return func(*args, **kwargs)
return login_required(func)(*args, **kwargs)
return decorated_view
@ -256,7 +260,8 @@ def edit_required(f):
# Returns the template for rendering and includes the instance name
def render_title_template(*args, **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)
@ -268,11 +273,81 @@ def before_request():
g.allow_upload = config.config_uploading
g.current_theme = config.config_theme
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()
if not config.db_configured and request.endpoint not in ('admin.basic_configuration', 'login') and '/static/' not in request.path:
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()
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'))
@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 #########################################################
@ -418,39 +493,34 @@ def get_comic_book(book_id, book_format, page):
# ################################### Typeahead ##################################################################
@web.route("/get_authors_json")
@web.route("/get_authors_json", methods=['GET'])
@login_required_if_no_ano
def get_authors_json():
if request.method == "GET":
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
def get_publishers_json():
if request.method == "GET":
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
def get_tags_json():
if request.method == "GET":
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
def get_series_json():
if request.method == "GET":
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
def get_languages_json():
if request.method == "GET":
query = request.args.get('q').lower()
query = (request.args.get('q') or '').lower()
language_names = isoLanguages.get_language_names(get_locale())
entries_start = [s for key, s in language_names.items() if s.lower().startswith(query.lower())]
if len(entries_start) < 5:
@ -461,19 +531,18 @@ def get_languages_json():
return json_dumps
@web.route("/get_matching_tags", methods=['GET', 'POST'])
@web.route("/get_matching_tags", methods=['GET'])
@login_required_if_no_ano
def get_matching_tags():
tag_dict = {'tags': []}
if request.method == "GET":
q = db.session.query(db.Books)
db.session.connection().connection.connection.create_function("lower", 1, lcase)
author_input = request.args.get('author_name')
title_input = request.args.get('book_title')
include_tag_inputs = request.args.getlist('include_tag')
exclude_tag_inputs = request.args.getlist('exclude_tag')
include_extension_inputs = request.args.getlist('include_extension')
exclude_extension_inputs = request.args.getlist('exclude_extension')
author_input = request.args.get('author_name') or ''
title_input = request.args.get('book_title') or ''
include_tag_inputs = request.args.getlist('include_tag') or ''
exclude_tag_inputs = request.args.getlist('exclude_tag') or ''
# include_extension_inputs = request.args.getlist('include_extension') or ''
# exclude_extension_inputs = request.args.getlist('exclude_extension') or ''
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")),
func.lower(db.Books.title).ilike("%" + title_input + "%"))
if len(include_tag_inputs) > 0:
@ -595,13 +664,13 @@ def render_hot_books(page):
abort(404)
def render_author_books(page, author_id, order):
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],
db.books_series_link, db.Series)
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"))
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):
name = db.session.query(db.Data).filter(db.Data.format == book_id.upper()).first()
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]])
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")
@ -832,11 +902,13 @@ def reconnect():
db.reconnect_db(config)
return json.dumps({})
@web.route("/search", methods=["GET"])
@login_required_if_no_ano
def search():
term = request.args.get("query").strip().lower()
term = request.args.get("query")
if term:
term.strip().lower()
entries = get_search_results(term)
ids = list()
for element in entries:
@ -1083,6 +1155,7 @@ def serve_book(book_id, book_format, anyname):
else:
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>/<anyname>")
@login_required_if_no_ano
@ -1186,17 +1259,25 @@ def login():
form = request.form.to_dict()
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == form['username'].strip().lower()) \
.first()
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user:
login_result = services.ldap.bind_user(form['username'], form['password'])
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "":
login_result, error = services.ldap.bind_user(form['username'], form['password'])
if login_result:
login_user(user, remember=True)
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")
return redirect_back(url_for("web.index"))
if login_result is None:
log.error('Could not login. LDAP server down, please contact your administrator')
flash(_(u"Could not login. LDAP server down, please contact your administrator"), category="error")
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
and user.nickname != "Guest":
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:
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
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")
log.info('Password reset for user "%s" IP-adress: %s', form['username'], ipAdress)
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")
else:
flash(_(u"Please enter valid username to reset password"), category="error")
@ -1220,13 +1301,23 @@ def login():
login_user(user, remember=True)
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")
config.config_is_initial = False
return redirect_back(url_for("web.index"))
else:
log.info('Login failed for user "%s" IP-adress: %s', form['username'], ipAdress)
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')
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")

View File

@ -1,16 +1,16 @@
# GDrive Integration
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
httplib2>=0.9.2,<0.18.0
oauth2client>=4.0.0,<4.14.0
uritemplate>=3.0.0,<3.1.0
pyasn1-modules>=0.0.8,<0.3.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
rsa==3.4.2,<4.1.0
six>=1.10.0,<1.14.0
six>=1.10.0,<1.15.0
# goodreads
goodreads>=0.3.2,<0.4.0
@ -18,15 +18,15 @@ python-Levenshtein>=0.12.0,<0.13.0
# ldap login
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
flask-dance>=0.13.0
flask-dance>=1.4.0,<3.1.0
sqlalchemy_utils>=0.33.5,<0.37.0
# extracting metadata
lxml>=3.8.0,<4.6.0
Pillow>=4.0.0,<7.1.0
Pillow>=4.0.0,<7.2.0
rarfile>=2.7
# other

View File

@ -1,14 +1,14 @@
Babel>=1.3, <2.9
Flask-Babel>=0.11.1,<1.1.0
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
backports_abc>=0.4
Flask>=1.0.2,<1.2.0
iso-639>=0.4.5,<0.5.0
PyPDF2==1.26.0,<1.27.0
pytz>=2016.10
requests>=2.11.1,<2.23.0
requests>=2.11.1,<2.24.0
SQLAlchemy>=1.1.0,<1.4.0
tornado>=4.1,<6.1
Wand>=0.4.4,<0.6.0

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

File diff suppressed because it is too large Load Diff