mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-30 23:03:02 +00:00 
			
		
		
		
	refactoring to prevent web.py being the middle of the universe
This commit is contained in:
		| @@ -31,7 +31,7 @@ import werkzeug, flask, flask_login, flask_principal, jinja2 | ||||
| from flask_babel import gettext as _ | ||||
|  | ||||
| from . import db, calibre_db, converter, uploader, server, isoLanguages, constants | ||||
| from .web import render_title_template | ||||
| from .render_template import render_title_template | ||||
| try: | ||||
|     from flask_login import __version__ as flask_loginVersion | ||||
| except ImportError: | ||||
|   | ||||
							
								
								
									
										163
									
								
								cps/admin.py
									
									
									
									
									
								
							
							
						
						
									
										163
									
								
								cps/admin.py
									
									
									
									
									
								
							| @@ -31,20 +31,25 @@ from datetime import datetime, timedelta | ||||
|  | ||||
| from babel import Locale as LC | ||||
| from babel.dates import format_datetime | ||||
| from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory | ||||
| from flask_login import login_required, current_user, logout_user | ||||
| from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g | ||||
| from flask_login import login_required, current_user, logout_user, confirm_login | ||||
| from flask_babel import gettext as _ | ||||
| from sqlalchemy import and_ | ||||
| from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError | ||||
| from sqlalchemy.sql.expression import func | ||||
| from sqlalchemy.sql.expression import func, or_ | ||||
|  | ||||
| from . import constants, logger, helper, services | ||||
| from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils | ||||
| from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash | ||||
| from .gdriveutils import is_gdrive_ready, gdrive_support | ||||
| from .web import admin_required, render_title_template, before_request, unconfigured | ||||
| from .render_template import render_title_template | ||||
| from . import debug_info | ||||
|  | ||||
| try: | ||||
|     from functools import wraps | ||||
| except ImportError: | ||||
|     pass  # We're not using Python 3 | ||||
|  | ||||
| log = logger.create() | ||||
|  | ||||
| feature_support = { | ||||
| @@ -73,6 +78,49 @@ feature_support['gdrive'] = gdrive_support | ||||
| admi = Blueprint('admin', __name__) | ||||
|  | ||||
|  | ||||
| def admin_required(f): | ||||
|     """ | ||||
|     Checks if current_user.role == 1 | ||||
|     """ | ||||
|  | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if current_user.role_admin(): | ||||
|             return f(*args, **kwargs) | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
|  | ||||
| def unconfigured(f): | ||||
|     """ | ||||
|     Checks if calibre-web instance is not configured | ||||
|     """ | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if not config.db_configured: | ||||
|             return f(*args, **kwargs) | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
| @admi.before_app_request | ||||
| def before_request(): | ||||
|     if current_user.is_authenticated: | ||||
|         confirm_login() | ||||
|     g.constants = constants | ||||
|     g.user = current_user | ||||
|     g.allow_registration = config.config_public_reg | ||||
|     g.allow_anonymous = config.config_anonbrowse | ||||
|     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: | ||||
|         return redirect(url_for('admin.basic_configuration')) | ||||
|  | ||||
|  | ||||
| @admi.route("/admin") | ||||
| @login_required | ||||
| @@ -1269,3 +1317,110 @@ def get_updater_status(): | ||||
|         except Exception: | ||||
|             status['status'] = 11 | ||||
|     return json.dumps(status) | ||||
|  | ||||
|  | ||||
| @admi.route('/import_ldap_users') | ||||
| @login_required | ||||
| @admin_required | ||||
| def import_ldap_users(): | ||||
|     showtext = {} | ||||
|     try: | ||||
|         new_users = services.ldap.get_group_members(config.config_ldap_group_name) | ||||
|     except (services.ldap.LDAPException, TypeError, AttributeError, KeyError) as e: | ||||
|         log.exception(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) | ||||
|  | ||||
|     imported = 0 | ||||
|     for username in new_users: | ||||
|         user = username.decode('utf-8') | ||||
|         if '=' in user: | ||||
|             # if member object field is empty take user object as filter | ||||
|             if config.config_ldap_member_user_object: | ||||
|                 query_filter = config.config_ldap_member_user_object | ||||
|             else: | ||||
|                 query_filter = config.config_ldap_user_object | ||||
|             try: | ||||
|                 user_identifier = extract_user_identifier(user, query_filter) | ||||
|             except Exception as e: | ||||
|                 log.warning(e) | ||||
|                 continue | ||||
|         else: | ||||
|             user_identifier = user | ||||
|             query_filter = None | ||||
|         try: | ||||
|             user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter) | ||||
|         except AttributeError as e: | ||||
|             log.exception(e) | ||||
|             continue | ||||
|         if user_data: | ||||
|             user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object) | ||||
|  | ||||
|             username = user_data[user_login_field][0].decode('utf-8') | ||||
|             # check for duplicate username | ||||
|             if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first(): | ||||
|                 # if ub.session.query(ub.User).filter(ub.User.nickname == username).first(): | ||||
|                 log.warning("LDAP User  %s Already in Database", user_data) | ||||
|                 continue | ||||
|  | ||||
|             kindlemail = '' | ||||
|             if 'mail' in user_data: | ||||
|                 useremail = user_data['mail'][0].decode('utf-8') | ||||
|                 if (len(user_data['mail']) > 1): | ||||
|                     kindlemail = user_data['mail'][1].decode('utf-8') | ||||
|  | ||||
|             else: | ||||
|                 log.debug('No Mail Field Found in LDAP Response') | ||||
|                 useremail = username + '@email.com' | ||||
|             # check for duplicate email | ||||
|             if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first(): | ||||
|                 log.warning("LDAP Email %s Already in Database", user_data) | ||||
|                 continue | ||||
|             content = ub.User() | ||||
|             content.nickname = username | ||||
|             content.password = ''  # dummy password which will be replaced by ldap one | ||||
|             content.email = useremail | ||||
|             content.kindle_mail = kindlemail | ||||
|             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() | ||||
|                 imported +=1 | ||||
|             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'.format(imported)) | ||||
|     return json.dumps(showtext) | ||||
|  | ||||
|  | ||||
| def extract_user_data_from_field(user, field): | ||||
|     match = re.search(field + "=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE) | ||||
|     if match: | ||||
|         return match.group(1) | ||||
|     else: | ||||
|         raise Exception("Could Not Parse LDAP User: {}".format(user)) | ||||
|  | ||||
| def extract_dynamic_field_from_filter(user, filter): | ||||
|     match = re.search("([a-zA-Z0-9-]+)=%s", filter, re.IGNORECASE | re.UNICODE) | ||||
|     if match: | ||||
|         return match.group(1) | ||||
|     else: | ||||
|         raise Exception("Could Not Parse LDAP Userfield: {}", user) | ||||
|  | ||||
| def extract_user_identifier(user, filter): | ||||
|     dynamic_field = extract_dynamic_field_from_filter(user, filter) | ||||
|     return extract_user_data_from_field(user, dynamic_field) | ||||
|   | ||||
| @@ -37,13 +37,38 @@ from . import config, get_locale, ub, db | ||||
| from . import calibre_db | ||||
| from .services.worker import WorkerThread | ||||
| from .tasks.upload import TaskUpload | ||||
| from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required | ||||
| from .render_template import render_title_template | ||||
| from .usermanagement import login_required_if_no_ano | ||||
|  | ||||
| try: | ||||
|     from functools import wraps | ||||
| except ImportError: | ||||
|     pass  # We're not using Python 3 | ||||
|  | ||||
|  | ||||
| editbook = Blueprint('editbook', __name__) | ||||
| log = logger.create() | ||||
|  | ||||
|  | ||||
| def upload_required(f): | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if current_user.role_upload() or current_user.role_admin(): | ||||
|             return f(*args, **kwargs) | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
| def edit_required(f): | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if current_user.role_edit() or current_user.role_admin(): | ||||
|             return f(*args, **kwargs) | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
|  | ||||
| # Modifies different Database objects, first check if elements have to be added to database, than check | ||||
| # if elements have to be deleted, because they are no longer used | ||||
| def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type): | ||||
|   | ||||
							
								
								
									
										72
									
								
								cps/error_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								cps/error_handler.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||
| #    Copyright (C) 2018-2020 OzzieIsaacs | ||||
| # | ||||
| #  This program is free software: you can redistribute it and/or modify | ||||
| #  it under the terms of the GNU General Public License as published by | ||||
| #  the Free Software Foundation, either version 3 of the License, or | ||||
| #  (at your option) any later version. | ||||
| # | ||||
| #  This program is distributed in the hope that it will be useful, | ||||
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| #  GNU General Public License for more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License | ||||
| #  along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| import traceback | ||||
| from flask import render_template | ||||
| from werkzeug.exceptions import default_exceptions | ||||
| try: | ||||
|     from werkzeug.exceptions import FailedDependency | ||||
| except ImportError: | ||||
|     from werkzeug.exceptions import UnprocessableEntity as FailedDependency | ||||
|  | ||||
| from . import config, app, logger, services | ||||
|  | ||||
|  | ||||
| log = logger.create() | ||||
|  | ||||
| # custom error page | ||||
| def error_http(error): | ||||
|     return render_template('http_error.html', | ||||
|                            error_code="Error {0}".format(error.code), | ||||
|                            error_name=error.name, | ||||
|                            issue=False, | ||||
|                            instance=config.config_calibre_web_title | ||||
|                            ), error.code | ||||
|  | ||||
|  | ||||
| def internal_error(error): | ||||
|     return render_template('http_error.html', | ||||
|                            error_code="Internal Server Error", | ||||
|                            error_name=str(error), | ||||
|                            issue=True, | ||||
|                            error_stack=traceback.format_exc().split("\n"), | ||||
|                            instance=config.config_calibre_web_title | ||||
|                            ), 500 | ||||
|  | ||||
| # http error handling | ||||
| for ex in default_exceptions: | ||||
|     if ex < 500: | ||||
|         app.register_error_handler(ex, error_http) | ||||
|     elif ex == 500: | ||||
|         app.register_error_handler(ex, internal_error) | ||||
|  | ||||
|  | ||||
| if services.ldap: | ||||
|     # Only way of catching the LDAPException upon logging in with LDAP server down | ||||
|     @app.errorhandler(services.ldap.LDAPException) | ||||
|     def handle_exception(e): | ||||
|         log.debug('LDAP server not accessible while trying to login to opds feed') | ||||
|         return error_http(FailedDependency()) | ||||
|  | ||||
|  | ||||
| # @app.errorhandler(InvalidRequestError) | ||||
| #@app.errorhandler(OperationalError) | ||||
| #def handle_db_exception(e): | ||||
| #    db.session.rollback() | ||||
| #    log.error('Database request error: %s',e) | ||||
| #    return internal_error(InternalServerError(e)) | ||||
| @@ -35,7 +35,7 @@ from flask_babel import gettext as _ | ||||
| from flask_login import login_required | ||||
|  | ||||
| from . import logger, gdriveutils, config, ub, calibre_db | ||||
| from .web import admin_required | ||||
| from .admin import admin_required | ||||
|  | ||||
| gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive') | ||||
| log = logger.create() | ||||
|   | ||||
| @@ -69,7 +69,7 @@ from flask_babel import gettext as _ | ||||
| from sqlalchemy.exc import OperationalError | ||||
|  | ||||
| from . import logger, ub, lm | ||||
| from .web import render_title_template | ||||
| from .render_template import render_title_template | ||||
|  | ||||
| try: | ||||
|     from functools import wraps | ||||
|   | ||||
| @@ -30,12 +30,12 @@ from flask_babel import gettext as _ | ||||
| from flask_dance.consumer import oauth_authorized, oauth_error | ||||
| from flask_dance.contrib.github import make_github_blueprint, github | ||||
| from flask_dance.contrib.google import make_google_blueprint, google | ||||
| from flask_login import login_user, current_user | ||||
| from flask_login import login_user, current_user, login_required | ||||
| from sqlalchemy.orm.exc import NoResultFound | ||||
| from sqlalchemy.exc import OperationalError | ||||
|  | ||||
| from . import constants, logger, config, app, ub | ||||
| from .web import login_required | ||||
|  | ||||
| from .oauth import OAuthBackend, backend_resultcode | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -28,12 +28,13 @@ from functools import wraps | ||||
| from flask import Blueprint, request, render_template, Response, g, make_response, abort | ||||
| from flask_login import current_user | ||||
| from sqlalchemy.sql.expression import func, text, or_, and_ | ||||
| from werkzeug.security import check_password_hash | ||||
|  | ||||
|  | ||||
| from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages | ||||
| from .helper import get_download_link, get_book_cover | ||||
| from .pagination import Pagination | ||||
| from .web import render_read_books, load_user_from_request | ||||
| from .web import render_read_books | ||||
| from .usermanagement import load_user_from_request | ||||
| from flask_babel import gettext as _ | ||||
| from babel import Locale as LC | ||||
| from babel.core import UnknownLocaleError | ||||
|   | ||||
							
								
								
									
										138
									
								
								cps/remotelogin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								cps/remotelogin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||
| #    Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, | ||||
| #                            andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, | ||||
| #                            falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, | ||||
| #                            ruben-herold, marblepebble, JackED42, SiphonSquirrel, | ||||
| #                            apetresc, nanu-c, mutschler | ||||
| # | ||||
| #  This program is free software: you can redistribute it and/or modify | ||||
| #  it under the terms of the GNU General Public License as published by | ||||
| #  the Free Software Foundation, either version 3 of the License, or | ||||
| #  (at your option) any later version. | ||||
| # | ||||
| #  This program is distributed in the hope that it will be useful, | ||||
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| #  GNU General Public License for more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License | ||||
| #  along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| import json | ||||
| from datetime import datetime | ||||
|  | ||||
| from flask import Blueprint, request, make_response, abort, url_for, flash, redirect | ||||
| from flask_login import login_required, current_user, login_user | ||||
| from flask_babel import gettext as _ | ||||
|  | ||||
| from . import config, logger, ub | ||||
| from .render_template import render_title_template | ||||
|  | ||||
| try: | ||||
|     from functools import wraps | ||||
| except ImportError: | ||||
|     pass  # We're not using Python 3 | ||||
|  | ||||
| remotelogin = Blueprint('remotelogin', __name__) | ||||
| log = logger.create() | ||||
|  | ||||
|  | ||||
| def remote_login_required(f): | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if config.config_remote_login: | ||||
|             return f(*args, **kwargs) | ||||
|         if request.headers.get('X-Requested-With') == 'XMLHttpRequest': | ||||
|             data = {'status': 'error', 'message': 'Forbidden'} | ||||
|             response = make_response(json.dumps(data, ensure_ascii=False)) | ||||
|             response.headers["Content-Type"] = "application/json; charset=utf-8" | ||||
|             return response, 403 | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
| @remotelogin.route('/remote/login') | ||||
| @remote_login_required | ||||
| def remote_login(): | ||||
|     auth_token = ub.RemoteAuthToken() | ||||
|     ub.session.add(auth_token) | ||||
|     ub.session.commit() | ||||
|  | ||||
|     verify_url = url_for('web.verify_token', token=auth_token.auth_token, _external=true) | ||||
|     log.debug(u"Remot Login request with token: %s", auth_token.auth_token) | ||||
|     return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token, | ||||
|                                  verify_url=verify_url, page="remotelogin") | ||||
|  | ||||
|  | ||||
| @remotelogin.route('/verify/<token>') | ||||
| @remote_login_required | ||||
| @login_required | ||||
| def verify_token(token): | ||||
|     auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first() | ||||
|  | ||||
|     # Token not found | ||||
|     if auth_token is None: | ||||
|         flash(_(u"Token not found"), category="error") | ||||
|         log.error(u"Remote Login token not found") | ||||
|         return redirect(url_for('web.index')) | ||||
|  | ||||
|     # Token expired | ||||
|     if datetime.now() > auth_token.expiration: | ||||
|         ub.session.delete(auth_token) | ||||
|         ub.session.commit() | ||||
|  | ||||
|         flash(_(u"Token has expired"), category="error") | ||||
|         log.error(u"Remote Login token expired") | ||||
|         return redirect(url_for('web.index')) | ||||
|  | ||||
|     # Update token with user information | ||||
|     auth_token.user_id = current_user.id | ||||
|     auth_token.verified = True | ||||
|     ub.session.commit() | ||||
|  | ||||
|     flash(_(u"Success! Please return to your device"), category="success") | ||||
|     log.debug(u"Remote Login token for userid %s verified", auth_token.user_id) | ||||
|     return redirect(url_for('web.index')) | ||||
|  | ||||
|  | ||||
| @remotelogin.route('/ajax/verify_token', methods=['POST']) | ||||
| @remote_login_required | ||||
| def token_verified(): | ||||
|     token = request.form['token'] | ||||
|     auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first() | ||||
|  | ||||
|     data = {} | ||||
|  | ||||
|     # Token not found | ||||
|     if auth_token is None: | ||||
|         data['status'] = 'error' | ||||
|         data['message'] = _(u"Token not found") | ||||
|  | ||||
|     # Token expired | ||||
|     elif datetime.now() > auth_token.expiration: | ||||
|         ub.session.delete(auth_token) | ||||
|         ub.session.commit() | ||||
|  | ||||
|         data['status'] = 'error' | ||||
|         data['message'] = _(u"Token has expired") | ||||
|  | ||||
|     elif not auth_token.verified: | ||||
|         data['status'] = 'not_verified' | ||||
|  | ||||
|     else: | ||||
|         user = ub.session.query(ub.User).filter(ub.User.id == auth_token.user_id).first() | ||||
|         login_user(user) | ||||
|  | ||||
|         ub.session.delete(auth_token) | ||||
|         ub.session.commit() | ||||
|  | ||||
|         data['status'] = 'success' | ||||
|         log.debug(u"Remote Login for userid %s succeded", user.id) | ||||
|         flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") | ||||
|  | ||||
|     response = make_response(json.dumps(data, ensure_ascii=False)) | ||||
|     response.headers["Content-Type"] = "application/json; charset=utf-8" | ||||
|  | ||||
|     return response | ||||
							
								
								
									
										99
									
								
								cps/render_template.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								cps/render_template.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||
| #    Copyright (C) 2018-2020 OzzieIsaacs | ||||
| # | ||||
| #  This program is free software: you can redistribute it and/or modify | ||||
| #  it under the terms of the GNU General Public License as published by | ||||
| #  the Free Software Foundation, either version 3 of the License, or | ||||
| #  (at your option) any later version. | ||||
| # | ||||
| #  This program is distributed in the hope that it will be useful, | ||||
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| #  GNU General Public License for more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License | ||||
| #  along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| from flask import render_template | ||||
| from flask_babel import gettext as _ | ||||
| from flask import g | ||||
| from werkzeug.local import LocalProxy | ||||
|  | ||||
| from . import config, constants | ||||
| from .ub import User | ||||
|  | ||||
|  | ||||
| def _get_sidebar_config(kwargs=None): | ||||
|     kwargs = kwargs or [] | ||||
|     if 'content' in kwargs: | ||||
|         content = kwargs['content'] | ||||
|         content = isinstance(content, (User, LocalProxy)) and not content.role_anonymous() | ||||
|     else: | ||||
|         content = 'conf' in kwargs | ||||
|     sidebar = list() | ||||
|     sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new", | ||||
|                     "visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root", | ||||
|                     "show_text": _('Show recent books'), "config_show":False}) | ||||
|     sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", | ||||
|                     "visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot", | ||||
|                     "show_text": _('Show Hot Books'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', | ||||
|                     "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), | ||||
|                     "page": "download", "show_text": _('Show Downloaded Books'), | ||||
|                     "config_show": content}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated", | ||||
|          "visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated", | ||||
|          "show_text": _('Show Top Rated Books'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read", | ||||
|                     "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), | ||||
|                     "page": "read", "show_text": _('Show read and unread'), "config_show": content}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread", | ||||
|          "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread", | ||||
|          "show_text": _('Show unread'), "config_show": False}) | ||||
|     sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand", | ||||
|                     "visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover", | ||||
|                     "show_text": _('Show random books'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat", | ||||
|                     "visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", | ||||
|                     "show_text": _('Show category selection'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie", | ||||
|                     "visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", | ||||
|                     "show_text": _('Show series selection'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author", | ||||
|                     "visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", | ||||
|                     "show_text": _('Show author selection'), "config_show": True}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher", | ||||
|          "visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", | ||||
|          "show_text": _('Show publisher selection'), "config_show":True}) | ||||
|     sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang", | ||||
|                     "visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'), | ||||
|                     "page": "language", | ||||
|                     "show_text": _('Show language selection'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate", | ||||
|                     "visibility": constants.SIDEBAR_RATING, 'public': True, | ||||
|                     "page": "rating", "show_text": _('Show ratings selection'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format", | ||||
|                     "visibility": constants.SIDEBAR_FORMAT, 'public': True, | ||||
|                     "page": "format", "show_text": _('Show file formats selection'), "config_show": True}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived", | ||||
|          "visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived", | ||||
|          "show_text": _('Show archived books'), "config_show": content}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list", | ||||
|          "visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list", | ||||
|          "show_text": _('Show Books List'), "config_show": content}) | ||||
|  | ||||
|     return sidebar | ||||
|  | ||||
| # Returns the template for rendering and includes the instance name | ||||
| def render_title_template(*args, **kwargs): | ||||
|     sidebar = _get_sidebar_config(kwargs) | ||||
|     return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, | ||||
|                            accept=constants.EXTENSIONS_UPLOAD, | ||||
|                            *args, **kwargs) | ||||
| @@ -30,7 +30,8 @@ from sqlalchemy.sql.expression import func | ||||
| from sqlalchemy.exc import OperationalError, InvalidRequestError | ||||
|  | ||||
| from . import logger, ub, calibre_db | ||||
| from .web import login_required_if_no_ano, render_title_template | ||||
| from .render_template import render_title_template | ||||
| from .usermanagement import login_required_if_no_ano | ||||
|  | ||||
|  | ||||
| shelf = Blueprint('shelf', __name__) | ||||
|   | ||||
| @@ -2,18 +2,33 @@ | ||||
| {% block body %} | ||||
| <div class="discover"> | ||||
|   <h2>{{title}}</h2> | ||||
|   {% if g.user.role_download() %} | ||||
|  <a id="shelf_down" href="{{ url_for('shelf.show_shelf', shelf_type=2, shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a> | ||||
|   {% if g.user.role_download() %} | ||||
|   <a id="shelf_down" href="{{ url_for('shelf.show_shelf', shelf_type=2, shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a> | ||||
|       {% endif %} | ||||
|   {% if g.user.is_authenticated %} | ||||
|     {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public  %} | ||||
|       <div id="delete_shelf" data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div> | ||||
|       <a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a> | ||||
|       <a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a> | ||||
|       {% if entries.__len__() %} | ||||
|         <a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a> | ||||
|       {% endif %} | ||||
|     {% endif %} | ||||
|   {% endif %} | ||||
|     <div class="filterheader hidden-xs hidden-sm"> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" id="auth_az" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='authaz')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" id="auth_za" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='authza')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> | ||||
|       {% if page == 'series' %} | ||||
|       <a data-toggle="tooltip" title="{{_('Sort ascending according to series index')}}" id="series_asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='seriesasc')}}"><span class="glyphicon glyphicon-sort-by-order"></span></a> | ||||
|       <a data-toggle="tooltip" title="{{_('Sort descending according to series index')}}" id="series_desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='seriesdesc')}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> | ||||
|       {% endif %} | ||||
|     </div> | ||||
|   <div class="row display-flex"> | ||||
|  | ||||
|     {% for entry in entries %} | ||||
|     <div class="col-sm-3 col-lg-2 col-xs-6 book"> | ||||
|       <div class="cover"> | ||||
|   | ||||
							
								
								
									
										71
									
								
								cps/ub.py
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								cps/ub.py
									
									
									
									
									
								
							| @@ -26,10 +26,8 @@ import uuid | ||||
| from flask import session as flask_session | ||||
| from binascii import hexlify | ||||
|  | ||||
| from flask import g | ||||
| from flask_babel import gettext as _ | ||||
| from flask_login import AnonymousUserMixin, current_user | ||||
| from werkzeug.local import LocalProxy | ||||
|  | ||||
| try: | ||||
|     from flask_dance.consumer.backend.sqla import OAuthConsumerMixin | ||||
|     oauth_support = True | ||||
| @@ -57,73 +55,6 @@ Base = declarative_base() | ||||
| searched_ids = {} | ||||
|  | ||||
|  | ||||
| def get_sidebar_config(kwargs=None): | ||||
|     kwargs = kwargs or [] | ||||
|     if 'content' in kwargs: | ||||
|         content = kwargs['content'] | ||||
|         content = isinstance(content, (User, LocalProxy)) and not content.role_anonymous() | ||||
|     else: | ||||
|         content = 'conf' in kwargs | ||||
|     sidebar = list() | ||||
|     sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new", | ||||
|                     "visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root", | ||||
|                     "show_text": _('Show recent books'), "config_show":False}) | ||||
|     sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", | ||||
|                     "visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot", | ||||
|                     "show_text": _('Show Hot Books'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', | ||||
|                     "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), | ||||
|                     "page": "download", "show_text": _('Show Downloaded Books'), | ||||
|                     "config_show": content}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated", | ||||
|          "visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated", | ||||
|          "show_text": _('Show Top Rated Books'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read", | ||||
|                     "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), | ||||
|                     "page": "read", "show_text": _('Show read and unread'), "config_show": content}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread", | ||||
|          "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread", | ||||
|          "show_text": _('Show unread'), "config_show": False}) | ||||
|     sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand", | ||||
|                     "visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover", | ||||
|                     "show_text": _('Show random books'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat", | ||||
|                     "visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", | ||||
|                     "show_text": _('Show category selection'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie", | ||||
|                     "visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", | ||||
|                     "show_text": _('Show series selection'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author", | ||||
|                     "visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", | ||||
|                     "show_text": _('Show author selection'), "config_show": True}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher", | ||||
|          "visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", | ||||
|          "show_text": _('Show publisher selection'), "config_show":True}) | ||||
|     sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang", | ||||
|                     "visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'), | ||||
|                     "page": "language", | ||||
|                     "show_text": _('Show language selection'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate", | ||||
|                     "visibility": constants.SIDEBAR_RATING, 'public': True, | ||||
|                     "page": "rating", "show_text": _('Show ratings selection'), "config_show": True}) | ||||
|     sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format", | ||||
|                     "visibility": constants.SIDEBAR_FORMAT, 'public': True, | ||||
|                     "page": "format", "show_text": _('Show file formats selection'), "config_show": True}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived", | ||||
|          "visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived", | ||||
|          "show_text": _('Show archived books'), "config_show": content}) | ||||
|     sidebar.append( | ||||
|         {"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list", | ||||
|          "visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list", | ||||
|          "show_text": _('Show Books List'), "config_show": content}) | ||||
|  | ||||
|     return sidebar | ||||
|  | ||||
|  | ||||
| def store_ids(result): | ||||
|     ids = list() | ||||
|     for element in result: | ||||
|   | ||||
							
								
								
									
										88
									
								
								cps/usermanagement.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								cps/usermanagement.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||
| #    Copyright (C) 2018-2020 OzzieIsaacs | ||||
| # | ||||
| #  This program is free software: you can redistribute it and/or modify | ||||
| #  it under the terms of the GNU General Public License as published by | ||||
| #  the Free Software Foundation, either version 3 of the License, or | ||||
| #  (at your option) any later version. | ||||
| # | ||||
| #  This program is distributed in the hope that it will be useful, | ||||
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| #  GNU General Public License for more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License | ||||
| #  along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| import base64 | ||||
| import binascii | ||||
|  | ||||
| from sqlalchemy.sql.expression import func | ||||
| from werkzeug.security import check_password_hash | ||||
| from flask_login import login_required | ||||
|  | ||||
| from . import lm, ub, config, constants, services | ||||
|  | ||||
| try: | ||||
|     from functools import wraps | ||||
| except ImportError: | ||||
|     pass  # We're not using Python 3 | ||||
|  | ||||
| def login_required_if_no_ano(func): | ||||
|     @wraps(func) | ||||
|     def decorated_view(*args, **kwargs): | ||||
|         if config.config_anonbrowse == 1: | ||||
|             return func(*args, **kwargs) | ||||
|         return login_required(func)(*args, **kwargs) | ||||
|  | ||||
|     return decorated_view | ||||
|  | ||||
|  | ||||
| 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() | ||||
|  | ||||
|  | ||||
| @lm.request_loader | ||||
| def load_user_from_request(request): | ||||
|     if config.config_allow_reverse_proxy_header_login: | ||||
|         rp_header_name = config.config_reverse_proxy_login_header_name | ||||
|         if rp_header_name: | ||||
|             rp_header_username = request.headers.get(rp_header_name) | ||||
|             if rp_header_username: | ||||
|                 user = _fetch_user_by_name(rp_header_username) | ||||
|                 if user: | ||||
|                     return user | ||||
|  | ||||
|     auth_header = request.headers.get("Authorization") | ||||
|     if auth_header: | ||||
|         user = load_user_from_auth_header(auth_header) | ||||
|         if user: | ||||
|             return user | ||||
|  | ||||
|     return | ||||
|  | ||||
|  | ||||
| def load_user_from_auth_header(header_val): | ||||
|     if header_val.startswith('Basic '): | ||||
|         header_val = header_val.replace('Basic ', '', 1) | ||||
|     basic_username = basic_password = '' | ||||
|     try: | ||||
|         header_val = base64.b64decode(header_val).decode('utf-8') | ||||
|         basic_username = header_val.split(':')[0] | ||||
|         basic_password = header_val.split(':')[1] | ||||
|     except (TypeError, UnicodeDecodeError, binascii.Error): | ||||
|         pass | ||||
|     user = _fetch_user_by_name(basic_username) | ||||
|     if user and config.config_login_type == constants.LOGIN_LDAP and services.ldap: | ||||
|         if services.ldap.bind_user(str(user.password), basic_password): | ||||
|             return user | ||||
|     if user and check_password_hash(str(user.password), basic_password): | ||||
|         return user | ||||
|     return | ||||
							
								
								
									
										425
									
								
								cps/web.py
									
									
									
									
									
								
							
							
						
						
									
										425
									
								
								cps/web.py
									
									
									
									
									
								
							| @@ -22,47 +22,40 @@ | ||||
|  | ||||
| from __future__ import division, print_function, unicode_literals | ||||
| import os | ||||
| import base64 | ||||
| from datetime import datetime | ||||
| import json | ||||
| import mimetypes | ||||
| import traceback | ||||
| import binascii | ||||
| import re | ||||
| import chardet  # dependency of requests | ||||
|  | ||||
| from babel.dates import format_date | ||||
| from babel import Locale as LC | ||||
| from babel.core import UnknownLocaleError | ||||
| from flask import Blueprint, jsonify | ||||
| from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for | ||||
| from flask import session as flask_session, send_file | ||||
| from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for | ||||
| from flask import session as flask_session | ||||
| from flask_babel import gettext as _ | ||||
| from flask_login import login_user, logout_user, login_required, current_user, confirm_login | ||||
| from flask_login import login_user, logout_user, login_required, current_user | ||||
| from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError | ||||
| from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_ | ||||
| from sqlalchemy.sql.expression import text, func, false, not_, and_ | ||||
| from sqlalchemy.orm.attributes import flag_modified | ||||
| from werkzeug.exceptions import default_exceptions | ||||
| from sqlalchemy.sql.functions import coalesce | ||||
|  | ||||
| from .services.worker import WorkerThread | ||||
|  | ||||
| try: | ||||
|     from werkzeug.exceptions import FailedDependency | ||||
| except ImportError: | ||||
|     from werkzeug.exceptions import UnprocessableEntity as FailedDependency | ||||
| from werkzeug.datastructures import Headers | ||||
| from werkzeug.security import generate_password_hash, check_password_hash | ||||
|  | ||||
| from . import constants, logger, isoLanguages, services | ||||
| from . import lm, babel, db, ub, config, get_locale, app | ||||
| from . import calibre_db | ||||
| from . import babel, db, ub, config, get_locale, app | ||||
| from . import calibre_db, shelf | ||||
| from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download | ||||
| from .helper import check_valid_domain, render_task_status, \ | ||||
|     get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \ | ||||
|     send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password | ||||
| from .pagination import Pagination | ||||
| from .redirect import redirect_back | ||||
| from .usermanagement import login_required_if_no_ano | ||||
| from .render_template import render_title_template | ||||
|  | ||||
| feature_support = { | ||||
|     'ldap': bool(services.ldap), | ||||
| @@ -72,7 +65,6 @@ feature_support = { | ||||
|  | ||||
| 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 | ||||
| @@ -83,55 +75,12 @@ try: | ||||
| except ImportError: | ||||
|     pass  # We're not using Python 3 | ||||
|  | ||||
|  | ||||
| try: | ||||
|     from natsort import natsorted as sort | ||||
| except ImportError: | ||||
|     sort = sorted  # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files | ||||
|  | ||||
|  | ||||
| # custom error page | ||||
| def error_http(error): | ||||
|     return render_template('http_error.html', | ||||
|                            error_code="Error {0}".format(error.code), | ||||
|                            error_name=error.name, | ||||
|                            issue=False, | ||||
|                            instance=config.config_calibre_web_title | ||||
|                            ), error.code | ||||
|  | ||||
|  | ||||
| def internal_error(error): | ||||
|     return render_template('http_error.html', | ||||
|                            error_code="Internal Server Error", | ||||
|                            error_name=str(error), | ||||
|                            issue=True, | ||||
|                            error_stack=traceback.format_exc().split("\n"), | ||||
|                            instance=config.config_calibre_web_title | ||||
|                            ), 500 | ||||
|  | ||||
|  | ||||
| # http error handling | ||||
| for ex in default_exceptions: | ||||
|     if ex < 500: | ||||
|         app.register_error_handler(ex, error_http) | ||||
|     elif ex == 500: | ||||
|         app.register_error_handler(ex, internal_error) | ||||
|  | ||||
|  | ||||
| if feature_support['ldap']: | ||||
|     # Only way of catching the LDAPException upon logging in with LDAP server down | ||||
|     @app.errorhandler(services.ldap.LDAPException) | ||||
|     def handle_exception(e): | ||||
|         log.debug('LDAP server not accessible while trying to login to opds feed') | ||||
|         return error_http(FailedDependency()) | ||||
|  | ||||
| # @app.errorhandler(InvalidRequestError) | ||||
| #@app.errorhandler(OperationalError) | ||||
| #def handle_db_exception(e): | ||||
| #    db.session.rollback() | ||||
| #    log.error('Database request error: %s',e) | ||||
| #    return internal_error(InternalServerError(e)) | ||||
|  | ||||
| @app.after_request | ||||
| def add_security_headers(resp): | ||||
|     # resp.headers['Content-Security-Policy']= "script-src 'self' https://www.googleapis.com https://api.douban.com https://comicvine.gamespot.com;" | ||||
| @@ -147,104 +96,6 @@ 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() | ||||
|  | ||||
|  | ||||
| @lm.request_loader | ||||
| def load_user_from_request(request): | ||||
|     if config.config_allow_reverse_proxy_header_login: | ||||
|         rp_header_name = config.config_reverse_proxy_login_header_name | ||||
|         if rp_header_name: | ||||
|             rp_header_username = request.headers.get(rp_header_name) | ||||
|             if rp_header_username: | ||||
|                 user = _fetch_user_by_name(rp_header_username) | ||||
|                 if user: | ||||
|                     return user | ||||
|  | ||||
|     auth_header = request.headers.get("Authorization") | ||||
|     if auth_header: | ||||
|         user = load_user_from_auth_header(auth_header) | ||||
|         if user: | ||||
|             return user | ||||
|  | ||||
|     return | ||||
|  | ||||
|  | ||||
| def load_user_from_auth_header(header_val): | ||||
|     if header_val.startswith('Basic '): | ||||
|         header_val = header_val.replace('Basic ', '', 1) | ||||
|     basic_username = basic_password = '' | ||||
|     try: | ||||
|         header_val = base64.b64decode(header_val).decode('utf-8') | ||||
|         basic_username = header_val.split(':')[0] | ||||
|         basic_password = header_val.split(':')[1] | ||||
|     except (TypeError, UnicodeDecodeError, binascii.Error): | ||||
|         pass | ||||
|     user = _fetch_user_by_name(basic_username) | ||||
|     if user and config.config_login_type == constants.LOGIN_LDAP and services.ldap: | ||||
|         if services.ldap.bind_user(str(user.password), basic_password): | ||||
|             return user | ||||
|     if user and check_password_hash(str(user.password), basic_password): | ||||
|         return user | ||||
|     return | ||||
|  | ||||
|  | ||||
| def login_required_if_no_ano(func): | ||||
|     @wraps(func) | ||||
|     def decorated_view(*args, **kwargs): | ||||
|         if config.config_anonbrowse == 1: | ||||
|             return func(*args, **kwargs) | ||||
|         return login_required(func)(*args, **kwargs) | ||||
|  | ||||
|     return decorated_view | ||||
|  | ||||
|  | ||||
| def remote_login_required(f): | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if config.config_remote_login: | ||||
|             return f(*args, **kwargs) | ||||
|         if request.headers.get('X-Requested-With') == 'XMLHttpRequest': | ||||
|             data = {'status': 'error', 'message': 'Forbidden'} | ||||
|             response = make_response(json.dumps(data, ensure_ascii=False)) | ||||
|             response.headers["Content-Type"] = "application/json; charset=utf-8" | ||||
|             return response, 403 | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
|  | ||||
| def admin_required(f): | ||||
|     """ | ||||
|     Checks if current_user.role == 1 | ||||
|     """ | ||||
|  | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if current_user.role_admin(): | ||||
|             return f(*args, **kwargs) | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
|  | ||||
| def unconfigured(f): | ||||
|     """ | ||||
|     Checks if calibre-web instance is not configured | ||||
|     """ | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if not config.db_configured: | ||||
|             return f(*args, **kwargs) | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
|  | ||||
| def download_required(f): | ||||
| @@ -266,154 +117,6 @@ def viewer_required(f): | ||||
|  | ||||
|     return inner | ||||
|  | ||||
|  | ||||
| def upload_required(f): | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if current_user.role_upload() or current_user.role_admin(): | ||||
|             return f(*args, **kwargs) | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
|  | ||||
| def edit_required(f): | ||||
|     @wraps(f) | ||||
|     def inner(*args, **kwargs): | ||||
|         if current_user.role_edit() or current_user.role_admin(): | ||||
|             return f(*args, **kwargs) | ||||
|         abort(403) | ||||
|  | ||||
|     return inner | ||||
|  | ||||
|  | ||||
| # ################################### Helper functions ################################################################ | ||||
|  | ||||
|  | ||||
| @web.before_app_request | ||||
| def before_request(): | ||||
|     if current_user.is_authenticated: | ||||
|         confirm_login() | ||||
|     g.constants = constants | ||||
|     g.user = current_user | ||||
|     g.allow_registration = config.config_public_reg | ||||
|     g.allow_anonymous = config.config_anonbrowse | ||||
|     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', "admin.config_pathchooser") and '/static/' not in request.path: | ||||
|         return redirect(url_for('admin.basic_configuration')) | ||||
|  | ||||
|  | ||||
| @app.route('/import_ldap_users') | ||||
| @login_required | ||||
| @admin_required | ||||
| def import_ldap_users(): | ||||
|     showtext = {} | ||||
|     try: | ||||
|         new_users = services.ldap.get_group_members(config.config_ldap_group_name) | ||||
|     except (services.ldap.LDAPException, TypeError, AttributeError, KeyError) as e: | ||||
|         log.debug_or_exception(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) | ||||
|  | ||||
|     imported = 0 | ||||
|     for username in new_users: | ||||
|         user = username.decode('utf-8') | ||||
|         if '=' in user: | ||||
|             # if member object field is empty take user object as filter | ||||
|             if config.config_ldap_member_user_object: | ||||
|                 query_filter = config.config_ldap_member_user_object | ||||
|             else: | ||||
|                 query_filter = config.config_ldap_user_object | ||||
|             try: | ||||
|                 user_identifier = extract_user_identifier(user, query_filter) | ||||
|             except Exception as e: | ||||
|                 log.warning(e) | ||||
|                 continue | ||||
|         else: | ||||
|             user_identifier = user | ||||
|             query_filter = None | ||||
|         try: | ||||
|             user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter) | ||||
|         except AttributeError as e: | ||||
|             log.debug_or_exception(e) | ||||
|             continue | ||||
|         if user_data: | ||||
|             user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object) | ||||
|  | ||||
|             username = user_data[user_login_field][0].decode('utf-8') | ||||
|             # check for duplicate username | ||||
|             if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first(): | ||||
|                 log.warning("LDAP User  %s Already in Database", user_data) | ||||
|                 continue | ||||
|  | ||||
|             kindlemail = '' | ||||
|             if 'mail' in user_data: | ||||
|                 useremail = user_data['mail'][0].decode('utf-8') | ||||
|                 if (len(user_data['mail']) > 1): | ||||
|                     kindlemail = user_data['mail'][1].decode('utf-8') | ||||
|  | ||||
|             else: | ||||
|                 log.debug('No Mail Field Found in LDAP Response') | ||||
|                 useremail = username + '@email.com' | ||||
|             # check for duplicate email | ||||
|             if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first(): | ||||
|                 log.warning("LDAP Email %s Already in Database", user_data) | ||||
|                 continue | ||||
|             content = ub.User() | ||||
|             content.nickname = username | ||||
|             content.password = ''  # dummy password which will be replaced by ldap one | ||||
|             content.email = useremail | ||||
|             content.kindle_mail = kindlemail | ||||
|             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() | ||||
|                 imported +=1 | ||||
|             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'.format(imported)) | ||||
|     return json.dumps(showtext) | ||||
|  | ||||
|  | ||||
| def extract_user_data_from_field(user, field): | ||||
|     match = re.search(field + "=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE) | ||||
|     if match: | ||||
|         return match.group(1) | ||||
|     else: | ||||
|         raise Exception("Could Not Parse LDAP User: {}".format(user)) | ||||
|  | ||||
| def extract_dynamic_field_from_filter(user, filter): | ||||
|     match = re.search("([a-zA-Z0-9-]+)=%s", filter, re.IGNORECASE | re.UNICODE) | ||||
|     if match: | ||||
|         return match.group(1) | ||||
|     else: | ||||
|         raise Exception("Could Not Parse LDAP Userfield: {}", user) | ||||
|  | ||||
| def extract_user_identifier(user, filter): | ||||
|     dynamic_field = extract_dynamic_field_from_filter(user, filter) | ||||
|     return extract_user_data_from_field(user, dynamic_field) | ||||
|  | ||||
|  | ||||
| # ################################### data provider functions ######################################################### | ||||
|  | ||||
|  | ||||
| @@ -650,14 +353,6 @@ def get_matching_tags(): | ||||
|     return json_dumps | ||||
|  | ||||
|  | ||||
| # 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, | ||||
|                            *args, **kwargs) | ||||
|  | ||||
|  | ||||
| def render_books_list(data, sort, book_id, page): | ||||
|     order = [db.Books.timestamp.desc()] | ||||
|     if sort == 'stored': | ||||
| @@ -735,6 +430,8 @@ def render_books_list(data, sort, book_id, page): | ||||
|         term = json.loads(flask_session['query']) | ||||
|         offset = int(int(config.config_books_per_page) * (page - 1)) | ||||
|         return render_adv_search_results(term, offset, order, config.config_books_per_page) | ||||
|     elif data == "shelf": | ||||
|         return shelf.show_shelf(1, book_id) | ||||
|     else: | ||||
|         website = data or "newest" | ||||
|         entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order) | ||||
| @@ -1691,101 +1388,7 @@ def logout(): | ||||
|     return redirect(url_for('web.login')) | ||||
|  | ||||
|  | ||||
| @web.route('/remote/login') | ||||
| @remote_login_required | ||||
| def remote_login(): | ||||
|     auth_token = ub.RemoteAuthToken() | ||||
|     ub.session.add(auth_token) | ||||
|     try: | ||||
|         ub.session.commit() | ||||
|     except OperationalError: | ||||
|         ub.session.rollback() | ||||
|  | ||||
|     verify_url = url_for('web.verify_token', token=auth_token.auth_token, _external=true) | ||||
|     log.debug(u"Remot Login request with token: %s", auth_token.auth_token) | ||||
|     return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token, | ||||
|                                  verify_url=verify_url, page="remotelogin") | ||||
|  | ||||
|  | ||||
| @web.route('/verify/<token>') | ||||
| @remote_login_required | ||||
| @login_required | ||||
| def verify_token(token): | ||||
|     auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first() | ||||
|  | ||||
|     # Token not found | ||||
|     if auth_token is None: | ||||
|         flash(_(u"Token not found"), category="error") | ||||
|         log.error(u"Remote Login token not found") | ||||
|         return redirect(url_for('web.index')) | ||||
|  | ||||
|     # Token expired | ||||
|     if datetime.now() > auth_token.expiration: | ||||
|         ub.session.delete(auth_token) | ||||
|         ub.session.commit() | ||||
|  | ||||
|         flash(_(u"Token has expired"), category="error") | ||||
|         log.error(u"Remote Login token expired") | ||||
|         return redirect(url_for('web.index')) | ||||
|  | ||||
|     # Update token with user information | ||||
|     auth_token.user_id = current_user.id | ||||
|     auth_token.verified = True | ||||
|     try: | ||||
|         ub.session.commit() | ||||
|     except OperationalError: | ||||
|         ub.session.rollback() | ||||
|  | ||||
|     flash(_(u"Success! Please return to your device"), category="success") | ||||
|     log.debug(u"Remote Login token for userid %s verified", auth_token.user_id) | ||||
|     return redirect(url_for('web.index')) | ||||
|  | ||||
|  | ||||
| @web.route('/ajax/verify_token', methods=['POST']) | ||||
| @remote_login_required | ||||
| def token_verified(): | ||||
|     token = request.form['token'] | ||||
|     auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first() | ||||
|  | ||||
|     data = {} | ||||
|  | ||||
|     # Token not found | ||||
|     if auth_token is None: | ||||
|         data['status'] = 'error' | ||||
|         data['message'] = _(u"Token not found") | ||||
|  | ||||
|     # Token expired | ||||
|     elif datetime.now() > auth_token.expiration: | ||||
|         ub.session.delete(auth_token) | ||||
|         try: | ||||
|             ub.session.commit() | ||||
|         except OperationalError: | ||||
|             ub.session.rollback() | ||||
|  | ||||
|         data['status'] = 'error' | ||||
|         data['message'] = _(u"Token has expired") | ||||
|  | ||||
|     elif not auth_token.verified: | ||||
|         data['status'] = 'not_verified' | ||||
|  | ||||
|     else: | ||||
|         user = ub.session.query(ub.User).filter(ub.User.id == auth_token.user_id).first() | ||||
|         login_user(user) | ||||
|  | ||||
|         ub.session.delete(auth_token) | ||||
|         try: | ||||
|             ub.session.commit() | ||||
|         except OperationalError: | ||||
|             ub.session.rollback() | ||||
|  | ||||
|         data['status'] = 'success' | ||||
|         log.debug(u"Remote Login for userid %s succeded", user.id) | ||||
|         flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") | ||||
|  | ||||
|     response = make_response(json.dumps(data, ensure_ascii=False)) | ||||
|     response.headers["Content-Type"] = "application/json; charset=utf-8" | ||||
|  | ||||
|     return response | ||||
|  | ||||
|  | ||||
| # ################################### Users own configuration ######################################################### | ||||
| @@ -1926,14 +1529,6 @@ def read_book(book_id, book_format): | ||||
|                 log.debug(u"Start comic reader for %d", book_id) | ||||
|                 return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), | ||||
|                                              extension=fileExt) | ||||
|         # if feature_support['rar']: | ||||
|         #    extensionList = ["cbr","cbt","cbz"] | ||||
|         # else: | ||||
|         #     extensionList = ["cbt","cbz"] | ||||
|         # for fileext in extensionList: | ||||
|         #     if book_format.lower() == fileext: | ||||
|         #         return render_title_template('readcbr.html', comicfile=book_id, | ||||
|         #         extension=fileext, title=_(u"Read a Book"), book=book) | ||||
|         log.debug(u"Error opening eBook. File does not exist or file is not accessible") | ||||
|         flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") | ||||
|         return redirect(url_for("web.index")) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Ozzieisaacs
					Ozzieisaacs