mirror of
https://github.com/janeczku/calibre-web
synced 2024-12-20 23:20:32 +00:00
Attention flask-httpAuth is additionally needed to run calibre-web
Updated pdf.js viewer Refactored login routine (#2623)
This commit is contained in:
commit
e3be7595e2
@ -19,12 +19,12 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import ast
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from .cw_login import LoginManager
|
||||||
|
from flask import session
|
||||||
|
|
||||||
from flask_login import LoginManager, confirm_login
|
|
||||||
from flask import session, current_app
|
|
||||||
from flask_login.utils import decode_cookie
|
|
||||||
from flask_login.signals import user_loaded_from_cookie
|
|
||||||
|
|
||||||
|
|
||||||
class MyLoginManager(LoginManager):
|
class MyLoginManager(LoginManager):
|
||||||
@ -36,18 +36,5 @@ class MyLoginManager(LoginManager):
|
|||||||
return super(). _session_protection_failed()
|
return super(). _session_protection_failed()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _load_user_from_remember_cookie(self, cookie):
|
|
||||||
user_id = decode_cookie(cookie)
|
|
||||||
if user_id is not None:
|
|
||||||
session["_user_id"] = user_id
|
|
||||||
session["_fresh"] = False
|
|
||||||
user = None
|
|
||||||
if self._user_callback:
|
|
||||||
user = self._user_callback(user_id)
|
|
||||||
if user is not None:
|
|
||||||
app = current_app._get_current_object()
|
|
||||||
user_loaded_from_cookie.send(app, user=user)
|
|
||||||
# if session was restored from remember me cookie make login valid
|
|
||||||
confirm_login()
|
|
||||||
return user
|
|
||||||
return None
|
|
||||||
|
@ -83,8 +83,8 @@ log = logger.create()
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE='Lax',
|
SESSION_COOKIE_SAMESITE='Strict',
|
||||||
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
REMEMBER_COOKIE_SAMESITE='Strict', # will be available in flask-login 0.5.1 earliest
|
||||||
WTF_CSRF_SSL_STRICT=False
|
WTF_CSRF_SSL_STRICT=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,12 +26,12 @@ import sqlite3
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import flask_login
|
|
||||||
import jinja2
|
import jinja2
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from . import db, calibre_db, converter, uploader, constants, dep_check
|
from . import db, calibre_db, converter, uploader, constants, dep_check
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
|
|
||||||
about = flask.Blueprint('about', __name__)
|
about = flask.Blueprint('about', __name__)
|
||||||
@ -74,7 +74,7 @@ def collect_stats():
|
|||||||
|
|
||||||
|
|
||||||
@about.route("/stats")
|
@about.route("/stats")
|
||||||
@flask_login.login_required
|
@user_login_required
|
||||||
def stats():
|
def stats():
|
||||||
counter = calibre_db.session.query(db.Books).count()
|
counter = calibre_db.session.query(db.Books).count()
|
||||||
authors = calibre_db.session.query(db.Authors).count()
|
authors = calibre_db.session.query(db.Authors).count()
|
||||||
|
122
cps/admin.py
122
cps/admin.py
@ -34,10 +34,9 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from flask_login import login_required, current_user, logout_user
|
from .cw_login import current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
||||||
from flask import session as flask_session
|
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
||||||
@ -52,6 +51,7 @@ from .embed_helper import get_calibre_binarypath
|
|||||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||||
from .render_template import render_title_template, get_sidebar_config
|
from .render_template import render_title_template, get_sidebar_config
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
|
from .usermanagement import user_login_required
|
||||||
from .babel import get_available_translations, get_available_locale, get_user_locale_language
|
from .babel import get_available_translations, get_available_locale, get_user_locale_language
|
||||||
from . import debug_info
|
from . import debug_info
|
||||||
|
|
||||||
@ -103,13 +103,13 @@ def admin_required(f):
|
|||||||
|
|
||||||
@admi.before_app_request
|
@admi.before_app_request
|
||||||
def before_request():
|
def before_request():
|
||||||
try:
|
#try:
|
||||||
if not ub.check_user_session(current_user.id,
|
#if not ub.check_user_session(current_user.id,
|
||||||
flask_session.get('_id')) and 'opds' not in request.path \
|
# flask_session.get('_id')) and 'opds' not in request.path \
|
||||||
and config.config_session == 1:
|
# and config.config_session == 1:
|
||||||
logout_user()
|
# logout_user()
|
||||||
except AttributeError:
|
#except AttributeError:
|
||||||
pass # ? fails on requesting /ajax/emailstat during restart ?
|
# pass # ? fails on requesting /ajax/emailstat during restart ?
|
||||||
g.constants = constants
|
g.constants = constants
|
||||||
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION', '')
|
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION', '')
|
||||||
g.allow_registration = config.config_public_reg
|
g.allow_registration = config.config_public_reg
|
||||||
@ -129,14 +129,14 @@ def before_request():
|
|||||||
return redirect(url_for('admin.db_configuration'))
|
return redirect(url_for('admin.db_configuration'))
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin")
|
#@admi.route("/admin")
|
||||||
@login_required
|
#@user_login_required
|
||||||
def admin_forbidden():
|
#def admin_forbidden():
|
||||||
abort(403)
|
# abort(403)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/shutdown", methods=["POST"])
|
@admi.route("/shutdown", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def shutdown():
|
def shutdown():
|
||||||
task = request.get_json().get('parameter', -1)
|
task = request.get_json().get('parameter', -1)
|
||||||
@ -165,7 +165,7 @@ def shutdown():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/metadata_backup", methods=["POST"])
|
@admi.route("/metadata_backup", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def queue_metadata_backup():
|
def queue_metadata_backup():
|
||||||
show_text = {}
|
show_text = {}
|
||||||
@ -189,7 +189,7 @@ def reconnect():
|
|||||||
|
|
||||||
@admi.route("/ajax/updateThumbnails", methods=['POST'])
|
@admi.route("/ajax/updateThumbnails", methods=['POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
@login_required
|
@user_login_required
|
||||||
def update_thumbnails():
|
def update_thumbnails():
|
||||||
content = config.get_scheduled_task_settings()
|
content = config.get_scheduled_task_settings()
|
||||||
if content['schedule_generate_book_covers']:
|
if content['schedule_generate_book_covers']:
|
||||||
@ -199,7 +199,7 @@ def update_thumbnails():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/view")
|
@admi.route("/admin/view")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin():
|
def admin():
|
||||||
version = updater_thread.get_current_version_info()
|
version = updater_thread.get_current_version_info()
|
||||||
@ -233,7 +233,7 @@ def admin():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def db_configuration():
|
def db_configuration():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@ -242,7 +242,7 @@ def db_configuration():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/config", methods=["GET"])
|
@admi.route("/admin/config", methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def configuration():
|
def configuration():
|
||||||
return render_title_template("config_edit.html",
|
return render_title_template("config_edit.html",
|
||||||
@ -253,28 +253,28 @@ def configuration():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def ajax_config():
|
def ajax_config():
|
||||||
return _configuration_update_helper()
|
return _configuration_update_helper()
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/ajaxdbconfig", methods=["POST"])
|
@admi.route("/admin/ajaxdbconfig", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def ajax_db_config():
|
def ajax_db_config():
|
||||||
return _db_configuration_update_helper()
|
return _db_configuration_update_helper()
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/alive", methods=["GET"])
|
@admi.route("/admin/alive", methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def calibreweb_alive():
|
def calibreweb_alive():
|
||||||
return "", 200
|
return "", 200
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/viewconfig")
|
@admi.route("/admin/viewconfig")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def view_configuration():
|
def view_configuration():
|
||||||
read_column = calibre_db.session.query(db.CustomColumns) \
|
read_column = calibre_db.session.query(db.CustomColumns) \
|
||||||
@ -291,7 +291,7 @@ def view_configuration():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/usertable")
|
@admi.route("/admin/usertable")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_user_table():
|
def edit_user_table():
|
||||||
visibility = current_user.view_settings.get('useredit', {})
|
visibility = current_user.view_settings.get('useredit', {})
|
||||||
@ -326,7 +326,7 @@ def edit_user_table():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/listusers")
|
@admi.route("/ajax/listusers")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_users():
|
def list_users():
|
||||||
off = int(request.args.get("offset") or 0)
|
off = int(request.args.get("offset") or 0)
|
||||||
@ -377,7 +377,7 @@ def list_users():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/deleteuser", methods=['POST'])
|
@admi.route("/ajax/deleteuser", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_user():
|
def delete_user():
|
||||||
user_ids = request.form.to_dict(flat=False)
|
user_ids = request.form.to_dict(flat=False)
|
||||||
@ -412,7 +412,7 @@ def delete_user():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/getlocale")
|
@admi.route("/ajax/getlocale")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def table_get_locale():
|
def table_get_locale():
|
||||||
locale = get_available_locale()
|
locale = get_available_locale()
|
||||||
@ -424,7 +424,7 @@ def table_get_locale():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/getdefaultlanguage")
|
@admi.route("/ajax/getdefaultlanguage")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def table_get_default_lang():
|
def table_get_default_lang():
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
@ -436,7 +436,7 @@ def table_get_default_lang():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/editlistusers/<param>", methods=['POST'])
|
@admi.route("/ajax/editlistusers/<param>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_list_user(param):
|
def edit_list_user(param):
|
||||||
vals = request.form.to_dict(flat=False)
|
vals = request.form.to_dict(flat=False)
|
||||||
@ -541,7 +541,7 @@ def edit_list_user(param):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/user_table_settings", methods=['POST'])
|
@admi.route("/ajax/user_table_settings", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_table_settings():
|
def update_table_settings():
|
||||||
current_user.view_settings['useredit'] = json.loads(request.data)
|
current_user.view_settings['useredit'] = json.loads(request.data)
|
||||||
@ -558,7 +558,7 @@ def update_table_settings():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/viewconfig", methods=["POST"])
|
@admi.route("/admin/viewconfig", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_view_configuration():
|
def update_view_configuration():
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
@ -603,7 +603,7 @@ def update_view_configuration():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/loaddialogtexts/<element_id>", methods=['POST'])
|
@admi.route("/ajax/loaddialogtexts/<element_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def load_dialogtexts(element_id):
|
def load_dialogtexts(element_id):
|
||||||
texts = {"header": "", "main": "", "valid": 1}
|
texts = {"header": "", "main": "", "valid": 1}
|
||||||
if element_id == "config_delete_kobo_token":
|
if element_id == "config_delete_kobo_token":
|
||||||
@ -639,7 +639,7 @@ def load_dialogtexts(element_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
|
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_domain(allow):
|
def edit_domain(allow):
|
||||||
# POST /post
|
# POST /post
|
||||||
@ -653,7 +653,7 @@ def edit_domain(allow):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/adddomain/<int:allow>", methods=['POST'])
|
@admi.route("/ajax/adddomain/<int:allow>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_domain(allow):
|
def add_domain(allow):
|
||||||
domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower()
|
domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower()
|
||||||
@ -667,7 +667,7 @@ def add_domain(allow):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/deletedomain", methods=['POST'])
|
@admi.route("/ajax/deletedomain", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_domain():
|
def delete_domain():
|
||||||
try:
|
try:
|
||||||
@ -685,7 +685,7 @@ def delete_domain():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/domainlist/<int:allow>")
|
@admi.route("/ajax/domainlist/<int:allow>")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_domain(allow):
|
def list_domain(allow):
|
||||||
answer = ub.session.query(ub.Registration).filter(ub.Registration.allow == allow).all()
|
answer = ub.session.query(ub.Registration).filter(ub.Registration.allow == allow).all()
|
||||||
@ -698,7 +698,7 @@ def list_domain(allow):
|
|||||||
|
|
||||||
@admi.route("/ajax/editrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
@admi.route("/ajax/editrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
||||||
@admi.route("/ajax/editrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
@admi.route("/ajax/editrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_restriction(res_type, user_id):
|
def edit_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
@ -764,14 +764,14 @@ def edit_restriction(res_type, user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_user_0_restriction(res_type):
|
def add_user_0_restriction(res_type):
|
||||||
return add_restriction(res_type, 0)
|
return add_restriction(res_type, 0)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_restriction(res_type, user_id):
|
def add_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
@ -817,14 +817,14 @@ def add_restriction(res_type, user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
|
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_user_0_restriction(res_type):
|
def delete_user_0_restriction(res_type):
|
||||||
return delete_restriction(res_type, 0)
|
return delete_restriction(res_type, 0)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_restriction(res_type, user_id):
|
def delete_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
@ -872,7 +872,7 @@ def delete_restriction(res_type, user_id):
|
|||||||
|
|
||||||
@admi.route("/ajax/listrestriction/<int:res_type>", defaults={"user_id": 0})
|
@admi.route("/ajax/listrestriction/<int:res_type>", defaults={"user_id": 0})
|
||||||
@admi.route("/ajax/listrestriction/<int:res_type>/<int:user_id>")
|
@admi.route("/ajax/listrestriction/<int:res_type>/<int:user_id>")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_restriction(res_type, user_id):
|
def list_restriction(res_type, user_id):
|
||||||
if res_type == 0: # Tags as template
|
if res_type == 0: # Tags as template
|
||||||
@ -916,20 +916,20 @@ def list_restriction(res_type, user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/fullsync", methods=["POST"])
|
@admi.route("/ajax/fullsync", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def ajax_self_fullsync():
|
def ajax_self_fullsync():
|
||||||
return do_full_kobo_sync(current_user.id)
|
return do_full_kobo_sync(current_user.id)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/fullsync/<int:userid>", methods=["POST"])
|
@admi.route("/ajax/fullsync/<int:userid>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def ajax_fullsync(userid):
|
def ajax_fullsync(userid):
|
||||||
return do_full_kobo_sync(userid)
|
return do_full_kobo_sync(userid)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/pathchooser/")
|
@admi.route("/ajax/pathchooser/")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def ajax_pathchooser():
|
def ajax_pathchooser():
|
||||||
return pathchooser()
|
return pathchooser()
|
||||||
@ -1246,7 +1246,7 @@ def _configuration_ldap_helper(to_save):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/simulatedbchange", methods=['POST'])
|
@admi.route("/ajax/simulatedbchange", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def simulatedbchange():
|
def simulatedbchange():
|
||||||
db_change, db_valid = _db_simulate_change()
|
db_change, db_valid = _db_simulate_change()
|
||||||
@ -1254,7 +1254,7 @@ def simulatedbchange():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/user/new", methods=["GET", "POST"])
|
@admi.route("/admin/user/new", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def new_user():
|
def new_user():
|
||||||
content = ub.User()
|
content = ub.User()
|
||||||
@ -1276,7 +1276,7 @@ def new_user():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/mailsettings", methods=["GET"])
|
@admi.route("/admin/mailsettings", methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_mailsettings():
|
def edit_mailsettings():
|
||||||
content = config.get_mail_settings()
|
content = config.get_mail_settings()
|
||||||
@ -1285,7 +1285,7 @@ def edit_mailsettings():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/mailsettings", methods=["POST"])
|
@admi.route("/admin/mailsettings", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_mailsettings():
|
def update_mailsettings():
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
@ -1342,7 +1342,7 @@ def update_mailsettings():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/scheduledtasks")
|
@admi.route("/admin/scheduledtasks")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_scheduledtasks():
|
def edit_scheduledtasks():
|
||||||
content = config.get_scheduled_task_settings()
|
content = config.get_scheduled_task_settings()
|
||||||
@ -1363,7 +1363,7 @@ def edit_scheduledtasks():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_scheduledtasks():
|
def update_scheduledtasks():
|
||||||
error = False
|
error = False
|
||||||
@ -1406,7 +1406,7 @@ def update_scheduledtasks():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/user/<int:user_id>", methods=["GET", "POST"])
|
@admi.route("/admin/user/<int:user_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_user(user_id):
|
def edit_user(user_id):
|
||||||
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
||||||
@ -1435,7 +1435,7 @@ def edit_user(user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
|
@admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def reset_user_password(user_id):
|
def reset_user_password(user_id):
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
@ -1453,7 +1453,7 @@ def reset_user_password(user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/logfile")
|
@admi.route("/admin/logfile")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def view_logfile():
|
def view_logfile():
|
||||||
logfiles = {0: logger.get_logfile(config.config_logfile),
|
logfiles = {0: logger.get_logfile(config.config_logfile),
|
||||||
@ -1467,7 +1467,7 @@ def view_logfile():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/log/<int:logtype>")
|
@admi.route("/ajax/log/<int:logtype>")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def send_logfile(logtype):
|
def send_logfile(logtype):
|
||||||
if logtype == 1:
|
if logtype == 1:
|
||||||
@ -1483,7 +1483,7 @@ def send_logfile(logtype):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/logdownload/<int:logtype>")
|
@admi.route("/admin/logdownload/<int:logtype>")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def download_log(logtype):
|
def download_log(logtype):
|
||||||
if logtype == 0:
|
if logtype == 0:
|
||||||
@ -1498,14 +1498,14 @@ def download_log(logtype):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/debug")
|
@admi.route("/admin/debug")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def download_debug():
|
def download_debug():
|
||||||
return debug_info.send_debug()
|
return debug_info.send_debug()
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/get_update_status", methods=['GET'])
|
@admi.route("/get_update_status", methods=['GET'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def get_update_status():
|
def get_update_status():
|
||||||
if feature_support['updater']:
|
if feature_support['updater']:
|
||||||
@ -1516,7 +1516,7 @@ def get_update_status():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/get_updater_status", methods=['GET', 'POST'])
|
@admi.route("/get_updater_status", methods=['GET', 'POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def get_updater_status():
|
def get_updater_status():
|
||||||
status = {}
|
status = {}
|
||||||
@ -1611,7 +1611,7 @@ def ldap_import_create_user(user, user_data):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route('/import_ldap_users', methods=["POST"])
|
@admi.route('/import_ldap_users', methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def import_ldap_users():
|
def import_ldap_users():
|
||||||
showtext = {}
|
showtext = {}
|
||||||
@ -1666,7 +1666,7 @@ def import_ldap_users():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/canceltask", methods=['POST'])
|
@admi.route("/ajax/canceltask", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def cancel_task():
|
def cancel_task():
|
||||||
task_id = request.get_json().get('task_id', None)
|
task_id = request.get_json().get('task_id', None)
|
||||||
|
@ -2,7 +2,7 @@ from babel import negotiate_locale
|
|||||||
from flask_babel import Babel, Locale
|
from flask_babel import Babel, Locale
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
|
98
cps/cw_login/__init__.py
Normal file
98
cps/cw_login/__init__.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# from .__about__ import __version__
|
||||||
|
from .config import AUTH_HEADER_NAME
|
||||||
|
from .config import COOKIE_DURATION
|
||||||
|
from .config import COOKIE_HTTPONLY
|
||||||
|
from .config import COOKIE_NAME
|
||||||
|
from .config import COOKIE_SECURE
|
||||||
|
from .config import ID_ATTRIBUTE
|
||||||
|
from .config import LOGIN_MESSAGE
|
||||||
|
from .config import LOGIN_MESSAGE_CATEGORY
|
||||||
|
from .config import REFRESH_MESSAGE
|
||||||
|
from .config import REFRESH_MESSAGE_CATEGORY
|
||||||
|
from .login_manager import LoginManager
|
||||||
|
from .mixins import AnonymousUserMixin
|
||||||
|
from .mixins import UserMixin
|
||||||
|
from .signals import session_protected
|
||||||
|
from .signals import user_accessed
|
||||||
|
from .signals import user_loaded_from_cookie
|
||||||
|
from .signals import user_loaded_from_request
|
||||||
|
from .signals import user_logged_in
|
||||||
|
from .signals import user_logged_out
|
||||||
|
from .signals import user_login_confirmed
|
||||||
|
from .signals import user_needs_refresh
|
||||||
|
from .signals import user_unauthorized
|
||||||
|
# from .test_client import FlaskLoginClient
|
||||||
|
from .utils import confirm_login
|
||||||
|
from .utils import current_user
|
||||||
|
from .utils import decode_cookie
|
||||||
|
from .utils import encode_cookie
|
||||||
|
from .utils import fresh_login_required
|
||||||
|
from .utils import login_fresh
|
||||||
|
from .utils import login_remembered
|
||||||
|
from .utils import login_required
|
||||||
|
from .utils import login_url
|
||||||
|
from .utils import login_user
|
||||||
|
from .utils import logout_user
|
||||||
|
from .utils import make_next_param
|
||||||
|
from .utils import set_login_view
|
||||||
|
|
||||||
|
__version_info__ = ("0", "6", "3")
|
||||||
|
__version__ = ".".join(__version_info__)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
|
"AUTH_HEADER_NAME",
|
||||||
|
"COOKIE_DURATION",
|
||||||
|
"COOKIE_HTTPONLY",
|
||||||
|
"COOKIE_NAME",
|
||||||
|
"COOKIE_SECURE",
|
||||||
|
"ID_ATTRIBUTE",
|
||||||
|
"LOGIN_MESSAGE",
|
||||||
|
"LOGIN_MESSAGE_CATEGORY",
|
||||||
|
"REFRESH_MESSAGE",
|
||||||
|
"REFRESH_MESSAGE_CATEGORY",
|
||||||
|
"LoginManager",
|
||||||
|
"AnonymousUserMixin",
|
||||||
|
"UserMixin",
|
||||||
|
"session_protected",
|
||||||
|
"user_accessed",
|
||||||
|
"user_loaded_from_cookie",
|
||||||
|
"user_loaded_from_request",
|
||||||
|
"user_logged_in",
|
||||||
|
"user_logged_out",
|
||||||
|
"user_login_confirmed",
|
||||||
|
"user_needs_refresh",
|
||||||
|
"user_unauthorized",
|
||||||
|
# "FlaskLoginClient",
|
||||||
|
"confirm_login",
|
||||||
|
"current_user",
|
||||||
|
"decode_cookie",
|
||||||
|
"encode_cookie",
|
||||||
|
"fresh_login_required",
|
||||||
|
"login_fresh",
|
||||||
|
"login_remembered",
|
||||||
|
"login_required",
|
||||||
|
"login_url",
|
||||||
|
"login_user",
|
||||||
|
"logout_user",
|
||||||
|
"make_next_param",
|
||||||
|
"set_login_view",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
if name == "user_loaded_from_header":
|
||||||
|
import warnings
|
||||||
|
from .signals import _user_loaded_from_header
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'user_loaded_from_header' is deprecated and will be"
|
||||||
|
" removed in Flask-Login 0.7. Use"
|
||||||
|
" 'user_loaded_from_request' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return _user_loaded_from_header
|
||||||
|
|
||||||
|
raise AttributeError(name)
|
55
cps/cw_login/config.py
Normal file
55
cps/cw_login/config.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
#: The default name of the "remember me" cookie (``remember_token``)
|
||||||
|
COOKIE_NAME = "remember_token"
|
||||||
|
|
||||||
|
#: The default time before the "remember me" cookie expires (365 days).
|
||||||
|
COOKIE_DURATION = timedelta(days=365)
|
||||||
|
|
||||||
|
#: Whether the "remember me" cookie requires Secure; defaults to ``False``
|
||||||
|
COOKIE_SECURE = False
|
||||||
|
|
||||||
|
#: Whether the "remember me" cookie uses HttpOnly or not; defaults to ``True``
|
||||||
|
COOKIE_HTTPONLY = True
|
||||||
|
|
||||||
|
#: Whether the "remember me" cookie requires same origin; defaults to ``None``
|
||||||
|
COOKIE_SAMESITE = None
|
||||||
|
|
||||||
|
#: The default flash message to display when users need to log in.
|
||||||
|
LOGIN_MESSAGE = "Please log in to access this page."
|
||||||
|
|
||||||
|
#: The default flash message category to display when users need to log in.
|
||||||
|
LOGIN_MESSAGE_CATEGORY = "message"
|
||||||
|
|
||||||
|
#: The default flash message to display when users need to reauthenticate.
|
||||||
|
REFRESH_MESSAGE = "Please reauthenticate to access this page."
|
||||||
|
|
||||||
|
#: The default flash message category to display when users need to
|
||||||
|
#: reauthenticate.
|
||||||
|
REFRESH_MESSAGE_CATEGORY = "message"
|
||||||
|
|
||||||
|
#: The default attribute to retreive the str id of the user
|
||||||
|
ID_ATTRIBUTE = "get_id"
|
||||||
|
|
||||||
|
#: Default name of the auth header (``Authorization``)
|
||||||
|
AUTH_HEADER_NAME = "Authorization"
|
||||||
|
|
||||||
|
#: A set of session keys that are populated by Flask-Login. Use this set to
|
||||||
|
#: purge keys safely and accurately.
|
||||||
|
SESSION_KEYS = {
|
||||||
|
"_user_id",
|
||||||
|
"_remember",
|
||||||
|
"_remember_seconds",
|
||||||
|
"_id",
|
||||||
|
"_fresh",
|
||||||
|
"next",
|
||||||
|
}
|
||||||
|
|
||||||
|
#: A set of HTTP methods which are exempt from `login_required` and
|
||||||
|
#: `fresh_login_required`. By default, this is just ``OPTIONS``.
|
||||||
|
EXEMPT_METHODS = {"OPTIONS"}
|
||||||
|
|
||||||
|
#: If true, the page the user is attempting to access is stored in the session
|
||||||
|
#: rather than a url parameter when redirecting to the login view; defaults to
|
||||||
|
#: ``False``.
|
||||||
|
USE_SESSION_FOR_NEXT = False
|
554
cps/cw_login/login_manager.py
Normal file
554
cps/cw_login/login_manager.py
Normal file
@ -0,0 +1,554 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from flask import abort
|
||||||
|
from flask import current_app
|
||||||
|
from flask import flash
|
||||||
|
from flask import g
|
||||||
|
from flask import has_app_context
|
||||||
|
from flask import redirect
|
||||||
|
from flask import request
|
||||||
|
from flask import session
|
||||||
|
from itsdangerous import URLSafeSerializer
|
||||||
|
from flask.json.tag import TaggedJSONSerializer
|
||||||
|
|
||||||
|
from .config import AUTH_HEADER_NAME
|
||||||
|
from .config import COOKIE_DURATION
|
||||||
|
from .config import COOKIE_HTTPONLY
|
||||||
|
from .config import COOKIE_NAME
|
||||||
|
from .config import COOKIE_SAMESITE
|
||||||
|
from .config import COOKIE_SECURE
|
||||||
|
from .config import ID_ATTRIBUTE
|
||||||
|
from .config import LOGIN_MESSAGE
|
||||||
|
from .config import LOGIN_MESSAGE_CATEGORY
|
||||||
|
from .config import REFRESH_MESSAGE
|
||||||
|
from .config import REFRESH_MESSAGE_CATEGORY
|
||||||
|
from .config import SESSION_KEYS
|
||||||
|
from .config import USE_SESSION_FOR_NEXT
|
||||||
|
from .mixins import AnonymousUserMixin
|
||||||
|
from .signals import session_protected
|
||||||
|
from .signals import user_accessed
|
||||||
|
from .signals import user_loaded_from_cookie
|
||||||
|
from .signals import user_loaded_from_request
|
||||||
|
from .signals import user_needs_refresh
|
||||||
|
from .signals import user_unauthorized
|
||||||
|
from .utils import _create_identifier
|
||||||
|
from .utils import _user_context_processor
|
||||||
|
from .utils import confirm_login
|
||||||
|
from .utils import expand_login_view
|
||||||
|
from .utils import login_url as make_login_url
|
||||||
|
from .utils import make_next_param
|
||||||
|
|
||||||
|
|
||||||
|
class LoginManager:
|
||||||
|
"""This object is used to hold the settings used for logging in. Instances
|
||||||
|
of :class:`LoginManager` are *not* bound to specific apps, so you can
|
||||||
|
create one in the main body of your code and then bind it to your
|
||||||
|
app in a factory function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app=None, add_context_processor=True):
|
||||||
|
#: A class or factory function that produces an anonymous user, which
|
||||||
|
#: is used when no one is logged in.
|
||||||
|
self.anonymous_user = AnonymousUserMixin
|
||||||
|
|
||||||
|
#: The name of the view to redirect to when the user needs to log in.
|
||||||
|
#: (This can be an absolute URL as well, if your authentication
|
||||||
|
#: machinery is external to your application.)
|
||||||
|
self.login_view = None
|
||||||
|
|
||||||
|
#: Names of views to redirect to when the user needs to log in,
|
||||||
|
#: per blueprint. If the key value is set to None the value of
|
||||||
|
#: :attr:`login_view` will be used instead.
|
||||||
|
self.blueprint_login_views = {}
|
||||||
|
|
||||||
|
#: The message to flash when a user is redirected to the login page.
|
||||||
|
self.login_message = LOGIN_MESSAGE
|
||||||
|
|
||||||
|
#: The message category to flash when a user is redirected to the login
|
||||||
|
#: page.
|
||||||
|
self.login_message_category = LOGIN_MESSAGE_CATEGORY
|
||||||
|
|
||||||
|
#: The name of the view to redirect to when the user needs to
|
||||||
|
#: reauthenticate.
|
||||||
|
self.refresh_view = None
|
||||||
|
|
||||||
|
#: The message to flash when a user is redirected to the 'needs
|
||||||
|
#: refresh' page.
|
||||||
|
self.needs_refresh_message = REFRESH_MESSAGE
|
||||||
|
|
||||||
|
#: The message category to flash when a user is redirected to the
|
||||||
|
#: 'needs refresh' page.
|
||||||
|
self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY
|
||||||
|
|
||||||
|
#: The mode to use session protection in. This can be either
|
||||||
|
#: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable
|
||||||
|
#: it.
|
||||||
|
self.session_protection = "basic"
|
||||||
|
|
||||||
|
#: If present, used to translate flash messages ``self.login_message``
|
||||||
|
#: and ``self.needs_refresh_message``
|
||||||
|
self.localize_callback = None
|
||||||
|
|
||||||
|
self.unauthorized_callback = None
|
||||||
|
|
||||||
|
self.needs_refresh_callback = None
|
||||||
|
|
||||||
|
self.id_attribute = ID_ATTRIBUTE
|
||||||
|
|
||||||
|
self._user_callback = None
|
||||||
|
|
||||||
|
self._header_callback = None
|
||||||
|
|
||||||
|
self._request_callback = None
|
||||||
|
|
||||||
|
self._session_identifier_generator = _create_identifier
|
||||||
|
|
||||||
|
if app is not None:
|
||||||
|
self.init_app(app, add_context_processor)
|
||||||
|
|
||||||
|
def setup_app(self, app, add_context_processor=True): # pragma: no cover
|
||||||
|
"""
|
||||||
|
This method has been deprecated. Please use
|
||||||
|
:meth:`LoginManager.init_app` instead.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'setup_app' is deprecated and will be removed in"
|
||||||
|
" Flask-Login 0.7. Use 'init_app' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
self.init_app(app, add_context_processor)
|
||||||
|
|
||||||
|
def init_app(self, app, add_context_processor=True):
|
||||||
|
"""
|
||||||
|
Configures an application. This registers an `after_request` call, and
|
||||||
|
attaches this `LoginManager` to it as `app.login_manager`.
|
||||||
|
|
||||||
|
:param app: The :class:`flask.Flask` object to configure.
|
||||||
|
:type app: :class:`flask.Flask`
|
||||||
|
:param add_context_processor: Whether to add a context processor to
|
||||||
|
the app that adds a `current_user` variable to the template.
|
||||||
|
Defaults to ``True``.
|
||||||
|
:type add_context_processor: bool
|
||||||
|
"""
|
||||||
|
app.login_manager = self
|
||||||
|
app.after_request(self._update_remember_cookie)
|
||||||
|
|
||||||
|
if add_context_processor:
|
||||||
|
app.context_processor(_user_context_processor)
|
||||||
|
|
||||||
|
def unauthorized(self):
|
||||||
|
"""
|
||||||
|
This is called when the user is required to log in. If you register a
|
||||||
|
callback with :meth:`LoginManager.unauthorized_handler`, then it will
|
||||||
|
be called. Otherwise, it will take the following actions:
|
||||||
|
|
||||||
|
- Flash :attr:`LoginManager.login_message` to the user.
|
||||||
|
|
||||||
|
- If the app is using blueprints find the login view for
|
||||||
|
the current blueprint using `blueprint_login_views`. If the app
|
||||||
|
is not using blueprints or the login view for the current
|
||||||
|
blueprint is not specified use the value of `login_view`.
|
||||||
|
|
||||||
|
- Redirect the user to the login view. (The page they were
|
||||||
|
attempting to access will be passed in the ``next`` query
|
||||||
|
string variable, so you can redirect there if present instead
|
||||||
|
of the homepage. Alternatively, it will be added to the session
|
||||||
|
as ``next`` if USE_SESSION_FOR_NEXT is set.)
|
||||||
|
|
||||||
|
If :attr:`LoginManager.login_view` is not defined, then it will simply
|
||||||
|
raise a HTTP 401 (Unauthorized) error instead.
|
||||||
|
|
||||||
|
This should be returned from a view or before/after_request function,
|
||||||
|
otherwise the redirect will have no effect.
|
||||||
|
"""
|
||||||
|
user_unauthorized.send(current_app._get_current_object())
|
||||||
|
|
||||||
|
if self.unauthorized_callback:
|
||||||
|
return self.unauthorized_callback()
|
||||||
|
|
||||||
|
if request.blueprint in self.blueprint_login_views:
|
||||||
|
login_view = self.blueprint_login_views[request.blueprint]
|
||||||
|
else:
|
||||||
|
login_view = self.login_view
|
||||||
|
|
||||||
|
if not login_view:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
if self.login_message:
|
||||||
|
if self.localize_callback is not None:
|
||||||
|
flash(
|
||||||
|
self.localize_callback(self.login_message),
|
||||||
|
category=self.login_message_category,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
flash(self.login_message, category=self.login_message_category)
|
||||||
|
|
||||||
|
config = current_app.config
|
||||||
|
if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
|
||||||
|
login_url = expand_login_view(login_view)
|
||||||
|
session["_id"] = self._session_identifier_generator()
|
||||||
|
session["next"] = make_next_param(login_url, request.url)
|
||||||
|
redirect_url = make_login_url(login_view)
|
||||||
|
else:
|
||||||
|
redirect_url = make_login_url(login_view, next_url=request.url)
|
||||||
|
|
||||||
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
def user_loader(self, callback):
|
||||||
|
"""
|
||||||
|
This sets the callback for reloading a user from the session. The
|
||||||
|
function you set should take a user ID (a ``str``) and return a
|
||||||
|
user object, or ``None`` if the user does not exist.
|
||||||
|
|
||||||
|
:param callback: The callback for retrieving a user object.
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
self._user_callback = callback
|
||||||
|
return self.user_callback
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_callback(self):
|
||||||
|
"""Gets the user_loader callback set by user_loader decorator."""
|
||||||
|
return self._user_callback
|
||||||
|
|
||||||
|
def request_loader(self, callback):
|
||||||
|
"""
|
||||||
|
This sets the callback for loading a user from a Flask request.
|
||||||
|
The function you set should take Flask request object and
|
||||||
|
return a user object, or `None` if the user does not exist.
|
||||||
|
|
||||||
|
:param callback: The callback for retrieving a user object.
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
self._request_callback = callback
|
||||||
|
return self.request_callback
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request_callback(self):
|
||||||
|
"""Gets the request_loader callback set by request_loader decorator."""
|
||||||
|
return self._request_callback
|
||||||
|
|
||||||
|
def unauthorized_handler(self, callback):
|
||||||
|
"""
|
||||||
|
This will set the callback for the `unauthorized` method, which among
|
||||||
|
other things is used by `login_required`. It takes no arguments, and
|
||||||
|
should return a response to be sent to the user instead of their
|
||||||
|
normal view.
|
||||||
|
|
||||||
|
:param callback: The callback for unauthorized users.
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
self.unauthorized_callback = callback
|
||||||
|
return callback
|
||||||
|
|
||||||
|
def needs_refresh_handler(self, callback):
|
||||||
|
"""
|
||||||
|
This will set the callback for the `needs_refresh` method, which among
|
||||||
|
other things is used by `fresh_login_required`. It takes no arguments,
|
||||||
|
and should return a response to be sent to the user instead of their
|
||||||
|
normal view.
|
||||||
|
|
||||||
|
:param callback: The callback for unauthorized users.
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
self.needs_refresh_callback = callback
|
||||||
|
return callback
|
||||||
|
|
||||||
|
def needs_refresh(self):
|
||||||
|
"""
|
||||||
|
This is called when the user is logged in, but they need to be
|
||||||
|
reauthenticated because their session is stale. If you register a
|
||||||
|
callback with `needs_refresh_handler`, then it will be called.
|
||||||
|
Otherwise, it will take the following actions:
|
||||||
|
|
||||||
|
- Flash :attr:`LoginManager.needs_refresh_message` to the user.
|
||||||
|
|
||||||
|
- Redirect the user to :attr:`LoginManager.refresh_view`. (The page
|
||||||
|
they were attempting to access will be passed in the ``next``
|
||||||
|
query string variable, so you can redirect there if present
|
||||||
|
instead of the homepage.)
|
||||||
|
|
||||||
|
If :attr:`LoginManager.refresh_view` is not defined, then it will
|
||||||
|
simply raise a HTTP 401 (Unauthorized) error instead.
|
||||||
|
|
||||||
|
This should be returned from a view or before/after_request function,
|
||||||
|
otherwise the redirect will have no effect.
|
||||||
|
"""
|
||||||
|
user_needs_refresh.send(current_app._get_current_object())
|
||||||
|
|
||||||
|
if self.needs_refresh_callback:
|
||||||
|
return self.needs_refresh_callback()
|
||||||
|
|
||||||
|
if not self.refresh_view:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
if self.needs_refresh_message:
|
||||||
|
if self.localize_callback is not None:
|
||||||
|
flash(
|
||||||
|
self.localize_callback(self.needs_refresh_message),
|
||||||
|
category=self.needs_refresh_message_category,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
flash(
|
||||||
|
self.needs_refresh_message,
|
||||||
|
category=self.needs_refresh_message_category,
|
||||||
|
)
|
||||||
|
|
||||||
|
config = current_app.config
|
||||||
|
if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
|
||||||
|
login_url = expand_login_view(self.refresh_view)
|
||||||
|
session["_id"] = self._session_identifier_generator()
|
||||||
|
session["next"] = make_next_param(login_url, request.url)
|
||||||
|
redirect_url = make_login_url(self.refresh_view)
|
||||||
|
else:
|
||||||
|
login_url = self.refresh_view
|
||||||
|
redirect_url = make_login_url(login_url, next_url=request.url)
|
||||||
|
|
||||||
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
def _update_request_context_with_user(self, user=None):
|
||||||
|
"""Store the given user as ctx.user."""
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
user = self.anonymous_user()
|
||||||
|
|
||||||
|
g._login_user = user
|
||||||
|
|
||||||
|
def _load_user(self):
|
||||||
|
"""Loads user from session or remember_me cookie as applicable"""
|
||||||
|
|
||||||
|
if self._user_callback is None and self._request_callback is None:
|
||||||
|
raise Exception(
|
||||||
|
"Missing user_loader or request_loader. Refer to "
|
||||||
|
"https://flask-login.readthedocs.io/#how-it-works "
|
||||||
|
"for more info."
|
||||||
|
)
|
||||||
|
|
||||||
|
user_accessed.send(current_app._get_current_object())
|
||||||
|
|
||||||
|
# Check SESSION_PROTECTION
|
||||||
|
if self._session_protection_failed():
|
||||||
|
return self._update_request_context_with_user()
|
||||||
|
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# Load user from Flask Session
|
||||||
|
user_id = session.get("_user_id")
|
||||||
|
user_random = session.get("_random")
|
||||||
|
user_session_key = session.get("_id")
|
||||||
|
if (user_id is not None
|
||||||
|
and user_random is not None
|
||||||
|
and user_session_key is not None
|
||||||
|
and self._user_callback is not None):
|
||||||
|
user = self._user_callback(user_id, user_random, user_session_key)
|
||||||
|
|
||||||
|
# Load user from Remember Me Cookie or Request Loader
|
||||||
|
if user is None:
|
||||||
|
config = current_app.config
|
||||||
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
header_name = config.get("AUTH_HEADER_NAME", AUTH_HEADER_NAME)
|
||||||
|
has_cookie = (
|
||||||
|
cookie_name in request.cookies and session.get("_remember") != "clear"
|
||||||
|
)
|
||||||
|
if has_cookie:
|
||||||
|
cookie = request.cookies[cookie_name]
|
||||||
|
user = self._load_user_from_remember_cookie(cookie)
|
||||||
|
elif self._request_callback:
|
||||||
|
user = self._load_user_from_request(request)
|
||||||
|
elif header_name in request.headers:
|
||||||
|
header = request.headers[header_name]
|
||||||
|
user = self._load_user_from_header(header)
|
||||||
|
if not user:
|
||||||
|
self._update_request_context_with_user()
|
||||||
|
return self._update_request_context_with_user(user)
|
||||||
|
|
||||||
|
def _session_protection_failed(self):
|
||||||
|
sess = session._get_current_object()
|
||||||
|
ident = self._session_identifier_generator()
|
||||||
|
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
mode = app.config.get("SESSION_PROTECTION", self.session_protection)
|
||||||
|
|
||||||
|
if not mode or mode not in ["basic", "strong"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# if the sess is empty, it's an anonymous user or just logged out
|
||||||
|
# so we can skip this
|
||||||
|
if sess and ident != sess.get("_id", None):
|
||||||
|
if mode == "basic" or sess.permanent:
|
||||||
|
if sess.get("_fresh") is not False:
|
||||||
|
sess["_fresh"] = False
|
||||||
|
session_protected.send(app)
|
||||||
|
return False
|
||||||
|
elif mode == "strong":
|
||||||
|
for k in SESSION_KEYS:
|
||||||
|
sess.pop(k, None)
|
||||||
|
|
||||||
|
sess["_remember"] = "clear"
|
||||||
|
session_protected.send(app)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _load_user_from_remember_cookie(self, cookie):
|
||||||
|
signer_kwargs = dict(
|
||||||
|
key_derivation="hmac", digest_method=staticmethod(hashlib.sha1)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
remember_dict = URLSafeSerializer(
|
||||||
|
current_app.secret_key,
|
||||||
|
salt="remember",
|
||||||
|
serializer=TaggedJSONSerializer(),
|
||||||
|
signer_kwargs=signer_kwargs,
|
||||||
|
).loads(cookie)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if remember_dict['user'] is not None:
|
||||||
|
session["_user_id"] = remember_dict['user']
|
||||||
|
if "_random" not in session:
|
||||||
|
session["_random"] = remember_dict['random']
|
||||||
|
session["_fresh"] = False
|
||||||
|
user = None
|
||||||
|
if self._user_callback:
|
||||||
|
user = self._user_callback(remember_dict['user'], session["_random"], None)
|
||||||
|
if user is not None:
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
user_loaded_from_cookie.send(app, user=user)
|
||||||
|
# if session was restored from remember me cookie make login valid
|
||||||
|
confirm_login()
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_user_from_header(self, header):
|
||||||
|
if self._header_callback:
|
||||||
|
user = self._header_callback(header)
|
||||||
|
if user is not None:
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
|
||||||
|
from .signals import _user_loaded_from_header
|
||||||
|
|
||||||
|
_user_loaded_from_header.send(app, user=user)
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_user_from_request(self, request):
|
||||||
|
if self._request_callback:
|
||||||
|
user = self._request_callback(request)
|
||||||
|
if user is not None:
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
user_loaded_from_request.send(app, user=user)
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _update_remember_cookie(self, response):
|
||||||
|
# Don't modify the session unless there's something to do.
|
||||||
|
if "_remember" not in session and current_app.config.get(
|
||||||
|
"REMEMBER_COOKIE_REFRESH_EACH_REQUEST"
|
||||||
|
):
|
||||||
|
session["_remember"] = "set"
|
||||||
|
|
||||||
|
if "_remember" in session:
|
||||||
|
operation = session.pop("_remember", None)
|
||||||
|
|
||||||
|
if operation == "set" and "_user_id" in session:
|
||||||
|
self._set_cookie(response)
|
||||||
|
elif operation == "clear":
|
||||||
|
self._clear_cookie(response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _set_cookie(self, response):
|
||||||
|
# cookie settings
|
||||||
|
config = current_app.config
|
||||||
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
||||||
|
path = config.get("REMEMBER_COOKIE_PATH", "/")
|
||||||
|
|
||||||
|
secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)
|
||||||
|
httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)
|
||||||
|
samesite = config.get("REMEMBER_COOKIE_SAMESITE", COOKIE_SAMESITE)
|
||||||
|
|
||||||
|
if "_remember_seconds" in session:
|
||||||
|
duration = timedelta(seconds=session["_remember_seconds"])
|
||||||
|
else:
|
||||||
|
duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION)
|
||||||
|
|
||||||
|
# prepare data
|
||||||
|
max_age = int(current_app.permanent_session_lifetime.total_seconds())
|
||||||
|
signer_kwargs = dict(
|
||||||
|
key_derivation="hmac", digest_method=staticmethod(hashlib.sha1)
|
||||||
|
)
|
||||||
|
# save
|
||||||
|
data = URLSafeSerializer(
|
||||||
|
current_app.secret_key,
|
||||||
|
salt="remember",
|
||||||
|
serializer=TaggedJSONSerializer(),
|
||||||
|
signer_kwargs=signer_kwargs,
|
||||||
|
).dumps({"user":session["_user_id"], "random":session["_random"]})
|
||||||
|
|
||||||
|
if isinstance(duration, int):
|
||||||
|
duration = timedelta(seconds=duration)
|
||||||
|
|
||||||
|
try:
|
||||||
|
expires = datetime.utcnow() + duration
|
||||||
|
except TypeError as e:
|
||||||
|
raise Exception(
|
||||||
|
"REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"
|
||||||
|
f" instead got: {duration}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
# actually set it
|
||||||
|
response.set_cookie(
|
||||||
|
cookie_name,
|
||||||
|
value=data,
|
||||||
|
expires=expires,
|
||||||
|
domain=domain,
|
||||||
|
path=path,
|
||||||
|
secure=secure,
|
||||||
|
httponly=httponly,
|
||||||
|
samesite=samesite,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clear_cookie(self, response):
|
||||||
|
config = current_app.config
|
||||||
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
||||||
|
path = config.get("REMEMBER_COOKIE_PATH", "/")
|
||||||
|
response.delete_cookie(cookie_name, domain=domain, path=path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _login_disabled(self):
|
||||||
|
"""Legacy property, use app.config['LOGIN_DISABLED'] instead."""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'_login_disabled' is deprecated and will be removed in"
|
||||||
|
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_app_context():
|
||||||
|
return current_app.config.get("LOGIN_DISABLED", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@_login_disabled.setter
|
||||||
|
def _login_disabled(self, newvalue):
|
||||||
|
"""Legacy property setter, use app.config['LOGIN_DISABLED'] instead."""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'_login_disabled' is deprecated and will be removed in"
|
||||||
|
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
current_app.config["LOGIN_DISABLED"] = newvalue
|
65
cps/cw_login/mixins.py
Normal file
65
cps/cw_login/mixins.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
class UserMixin:
|
||||||
|
"""
|
||||||
|
This provides default implementations for the methods that Flask-Login
|
||||||
|
expects user objects to have.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Python 3 implicitly set __hash__ to None if we override __eq__
|
||||||
|
# We set it back to its default implementation
|
||||||
|
__hash__ = object.__hash__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return self.is_active
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
try:
|
||||||
|
return str(self.id)
|
||||||
|
except AttributeError:
|
||||||
|
raise NotImplementedError("No `id` attribute - override `get_id`") from None
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""
|
||||||
|
Checks the equality of two `UserMixin` objects using `get_id`.
|
||||||
|
"""
|
||||||
|
if isinstance(other, UserMixin):
|
||||||
|
return self.get_id() == other.get_id()
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
"""
|
||||||
|
Checks the inequality of two `UserMixin` objects using `get_id`.
|
||||||
|
"""
|
||||||
|
equal = self.__eq__(other)
|
||||||
|
if equal is NotImplemented:
|
||||||
|
return NotImplemented
|
||||||
|
return not equal
|
||||||
|
|
||||||
|
|
||||||
|
class AnonymousUserMixin:
|
||||||
|
"""
|
||||||
|
This is the default object for representing an anonymous user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return
|
61
cps/cw_login/signals.py
Normal file
61
cps/cw_login/signals.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from flask.signals import Namespace
|
||||||
|
|
||||||
|
_signals = Namespace()
|
||||||
|
|
||||||
|
#: Sent when a user is logged in. In addition to the app (which is the
|
||||||
|
#: sender), it is passed `user`, which is the user being logged in.
|
||||||
|
user_logged_in = _signals.signal("logged-in")
|
||||||
|
|
||||||
|
#: Sent when a user is logged out. In addition to the app (which is the
|
||||||
|
#: sender), it is passed `user`, which is the user being logged out.
|
||||||
|
user_logged_out = _signals.signal("logged-out")
|
||||||
|
|
||||||
|
#: Sent when the user is loaded from the cookie. In addition to the app (which
|
||||||
|
#: is the sender), it is passed `user`, which is the user being reloaded.
|
||||||
|
user_loaded_from_cookie = _signals.signal("loaded-from-cookie")
|
||||||
|
|
||||||
|
#: Sent when the user is loaded from the header. In addition to the app (which
|
||||||
|
#: is the #: sender), it is passed `user`, which is the user being reloaded.
|
||||||
|
_user_loaded_from_header = _signals.signal("loaded-from-header")
|
||||||
|
|
||||||
|
#: Sent when the user is loaded from the request. In addition to the app (which
|
||||||
|
#: is the #: sender), it is passed `user`, which is the user being reloaded.
|
||||||
|
user_loaded_from_request = _signals.signal("loaded-from-request")
|
||||||
|
|
||||||
|
#: Sent when a user's login is confirmed, marking it as fresh. (It is not
|
||||||
|
#: called for a normal login.)
|
||||||
|
#: It receives no additional arguments besides the app.
|
||||||
|
user_login_confirmed = _signals.signal("login-confirmed")
|
||||||
|
|
||||||
|
#: Sent when the `unauthorized` method is called on a `LoginManager`. It
|
||||||
|
#: receives no additional arguments besides the app.
|
||||||
|
user_unauthorized = _signals.signal("unauthorized")
|
||||||
|
|
||||||
|
#: Sent when the `needs_refresh` method is called on a `LoginManager`. It
|
||||||
|
#: receives no additional arguments besides the app.
|
||||||
|
user_needs_refresh = _signals.signal("needs-refresh")
|
||||||
|
|
||||||
|
#: Sent whenever the user is accessed/loaded
|
||||||
|
#: receives no additional arguments besides the app.
|
||||||
|
user_accessed = _signals.signal("accessed")
|
||||||
|
|
||||||
|
#: Sent whenever session protection takes effect, and a session is either
|
||||||
|
#: marked non-fresh or deleted. It receives no additional arguments besides
|
||||||
|
#: the app.
|
||||||
|
session_protected = _signals.signal("session-protected")
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
if name == "user_loaded_from_header":
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'user_loaded_from_header' is deprecated and will be"
|
||||||
|
" removed in Flask-Login 0.7. Use"
|
||||||
|
" 'user_loaded_from_request' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return _user_loaded_from_header
|
||||||
|
|
||||||
|
raise AttributeError(name)
|
424
cps/cw_login/utils.py
Normal file
424
cps/cw_login/utils.py
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
import hmac
|
||||||
|
import os
|
||||||
|
from functools import wraps
|
||||||
|
from hashlib import sha512
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
from urllib.parse import urlunsplit
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
|
from flask import has_request_context
|
||||||
|
from flask import request
|
||||||
|
from flask import session
|
||||||
|
from flask import url_for
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .config import COOKIE_NAME
|
||||||
|
from .config import EXEMPT_METHODS
|
||||||
|
from .signals import user_logged_in
|
||||||
|
from .signals import user_logged_out
|
||||||
|
from .signals import user_login_confirmed
|
||||||
|
|
||||||
|
#: A proxy for the current user. If no user is logged in, this will be an
|
||||||
|
#: anonymous user
|
||||||
|
current_user = LocalProxy(lambda: _get_user())
|
||||||
|
|
||||||
|
|
||||||
|
def encode_cookie(payload, key=None):
|
||||||
|
"""
|
||||||
|
This will encode a ``str`` value into a cookie, and sign that cookie
|
||||||
|
with the app's secret key.
|
||||||
|
|
||||||
|
:param payload: The value to encode, as `str`.
|
||||||
|
:type payload: str
|
||||||
|
|
||||||
|
:param key: The key to use when creating the cookie digest. If not
|
||||||
|
specified, the SECRET_KEY value from app config will be used.
|
||||||
|
:type key: str
|
||||||
|
"""
|
||||||
|
return f"{payload}|{_cookie_digest(payload, key=key)}"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_cookie(cookie, key=None):
|
||||||
|
"""
|
||||||
|
This decodes a cookie given by `encode_cookie`. If verification of the
|
||||||
|
cookie fails, ``None`` will be implicitly returned.
|
||||||
|
|
||||||
|
:param cookie: An encoded cookie.
|
||||||
|
:type cookie: str
|
||||||
|
|
||||||
|
:param key: The key to use when creating the cookie digest. If not
|
||||||
|
specified, the SECRET_KEY value from app config will be used.
|
||||||
|
:type key: str
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload, digest = cookie.rsplit("|", 1)
|
||||||
|
if hasattr(digest, "decode"):
|
||||||
|
digest = digest.decode("ascii") # pragma: no cover
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if hmac.compare_digest(_cookie_digest(payload, key=key), digest):
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def make_next_param(login_url, current_url):
|
||||||
|
"""
|
||||||
|
Reduces the scheme and host from a given URL so it can be passed to
|
||||||
|
the given `login` URL more efficiently.
|
||||||
|
|
||||||
|
:param login_url: The login URL being redirected to.
|
||||||
|
:type login_url: str
|
||||||
|
:param current_url: The URL to reduce.
|
||||||
|
:type current_url: str
|
||||||
|
"""
|
||||||
|
l_url = urlsplit(login_url)
|
||||||
|
c_url = urlsplit(current_url)
|
||||||
|
|
||||||
|
if (not l_url.scheme or l_url.scheme == c_url.scheme) and (
|
||||||
|
not l_url.netloc or l_url.netloc == c_url.netloc
|
||||||
|
):
|
||||||
|
return urlunsplit(("", "", c_url.path, c_url.query, ""))
|
||||||
|
return current_url
|
||||||
|
|
||||||
|
|
||||||
|
def expand_login_view(login_view):
|
||||||
|
"""
|
||||||
|
Returns the url for the login view, expanding the view name to a url if
|
||||||
|
needed.
|
||||||
|
|
||||||
|
:param login_view: The name of the login view or a URL for the login view.
|
||||||
|
:type login_view: str
|
||||||
|
"""
|
||||||
|
if login_view.startswith(("https://", "http://", "/")):
|
||||||
|
return login_view
|
||||||
|
|
||||||
|
return url_for(login_view)
|
||||||
|
|
||||||
|
|
||||||
|
def login_url(login_view, next_url=None, next_field="next"):
|
||||||
|
"""
|
||||||
|
Creates a URL for redirecting to a login page. If only `login_view` is
|
||||||
|
provided, this will just return the URL for it. If `next_url` is provided,
|
||||||
|
however, this will append a ``next=URL`` parameter to the query string
|
||||||
|
so that the login view can redirect back to that URL. Flask-Login's default
|
||||||
|
unauthorized handler uses this function when redirecting to your login url.
|
||||||
|
To force the host name used, set `FORCE_HOST_FOR_REDIRECTS` to a host. This
|
||||||
|
prevents from redirecting to external sites if request headers Host or
|
||||||
|
X-Forwarded-For are present.
|
||||||
|
|
||||||
|
:param login_view: The name of the login view. (Alternately, the actual
|
||||||
|
URL to the login view.)
|
||||||
|
:type login_view: str
|
||||||
|
:param next_url: The URL to give the login view for redirection.
|
||||||
|
:type next_url: str
|
||||||
|
:param next_field: What field to store the next URL in. (It defaults to
|
||||||
|
``next``.)
|
||||||
|
:type next_field: str
|
||||||
|
"""
|
||||||
|
base = expand_login_view(login_view)
|
||||||
|
|
||||||
|
if next_url is None:
|
||||||
|
return base
|
||||||
|
|
||||||
|
parsed_result = urlsplit(base)
|
||||||
|
md = parse_qs(parsed_result.query, keep_blank_values=True)
|
||||||
|
md[next_field] = make_next_param(base, next_url)
|
||||||
|
netloc = current_app.config.get("FORCE_HOST_FOR_REDIRECTS") or parsed_result.netloc
|
||||||
|
parsed_result = parsed_result._replace(
|
||||||
|
netloc=netloc, query=urlencode(md, doseq=True)
|
||||||
|
)
|
||||||
|
return urlunsplit(parsed_result)
|
||||||
|
|
||||||
|
|
||||||
|
def login_fresh():
|
||||||
|
"""
|
||||||
|
This returns ``True`` if the current login is fresh.
|
||||||
|
"""
|
||||||
|
return session.get("_fresh", False)
|
||||||
|
|
||||||
|
|
||||||
|
def login_remembered():
|
||||||
|
"""
|
||||||
|
This returns ``True`` if the current login is remembered across sessions.
|
||||||
|
"""
|
||||||
|
config = current_app.config
|
||||||
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
has_cookie = cookie_name in request.cookies and session.get("_remember") != "clear"
|
||||||
|
if has_cookie:
|
||||||
|
cookie = request.cookies[cookie_name]
|
||||||
|
user_id = decode_cookie(cookie)
|
||||||
|
return user_id is not None
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def login_user(user, remember=False, duration=None, force=False, fresh=True):
|
||||||
|
"""
|
||||||
|
Logs a user in. You should pass the actual user object to this. If the
|
||||||
|
user's `is_active` property is ``False``, they will not be logged in
|
||||||
|
unless `force` is ``True``.
|
||||||
|
|
||||||
|
This will return ``True`` if the log in attempt succeeds, and ``False`` if
|
||||||
|
it fails (i.e. because the user is inactive).
|
||||||
|
|
||||||
|
:param user: The user object to log in.
|
||||||
|
:type user: object
|
||||||
|
:param remember: Whether to remember the user after their session expires.
|
||||||
|
Defaults to ``False``.
|
||||||
|
:type remember: bool
|
||||||
|
:param duration: The amount of time before the remember cookie expires. If
|
||||||
|
``None`` the value set in the settings is used. Defaults to ``None``.
|
||||||
|
:type duration: :class:`datetime.timedelta`
|
||||||
|
:param force: If the user is inactive, setting this to ``True`` will log
|
||||||
|
them in regardless. Defaults to ``False``.
|
||||||
|
:type force: bool
|
||||||
|
:param fresh: setting this to ``False`` will log in the user with a session
|
||||||
|
marked as not "fresh". Defaults to ``True``.
|
||||||
|
:type fresh: bool
|
||||||
|
"""
|
||||||
|
if not force and not user.is_active:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_id = getattr(user, current_app.login_manager.id_attribute)()
|
||||||
|
session["_user_id"] = user_id
|
||||||
|
session["_fresh"] = fresh
|
||||||
|
session["_id"] = current_app.login_manager._session_identifier_generator()
|
||||||
|
session["_random"] = os.urandom(10).hex()
|
||||||
|
|
||||||
|
if remember:
|
||||||
|
session["_remember"] = "set"
|
||||||
|
if duration is not None:
|
||||||
|
try:
|
||||||
|
# equal to timedelta.total_seconds() but works with Python 2.6
|
||||||
|
session["_remember_seconds"] = (
|
||||||
|
duration.microseconds
|
||||||
|
+ (duration.seconds + duration.days * 24 * 3600) * 10**6
|
||||||
|
) / 10.0**6
|
||||||
|
except AttributeError as e:
|
||||||
|
raise Exception(
|
||||||
|
f"duration must be a datetime.timedelta, instead got: {duration}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
current_app.login_manager._update_request_context_with_user(user)
|
||||||
|
user_logged_in.send(current_app._get_current_object(), user=_get_user())
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def logout_user():
|
||||||
|
"""
|
||||||
|
Logs a user out. (You do not need to pass the actual user.) This will
|
||||||
|
also clean up the remember me cookie if it exists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = _get_user()
|
||||||
|
|
||||||
|
if "_user_id" in session:
|
||||||
|
session.pop("_user_id")
|
||||||
|
|
||||||
|
if "_fresh" in session:
|
||||||
|
session.pop("_fresh")
|
||||||
|
|
||||||
|
if "_id" in session:
|
||||||
|
session.pop("_id")
|
||||||
|
|
||||||
|
if "_random" in session:
|
||||||
|
session.pop("_random")
|
||||||
|
|
||||||
|
|
||||||
|
cookie_name = current_app.config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
if cookie_name in request.cookies:
|
||||||
|
session["_remember"] = "clear"
|
||||||
|
if "_remember_seconds" in session:
|
||||||
|
session.pop("_remember_seconds")
|
||||||
|
|
||||||
|
user_logged_out.send(current_app._get_current_object(), user=user)
|
||||||
|
|
||||||
|
current_app.login_manager._update_request_context_with_user()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_login():
|
||||||
|
"""
|
||||||
|
This sets the current session as fresh. Sessions become stale when they
|
||||||
|
are reloaded from a cookie.
|
||||||
|
"""
|
||||||
|
session["_fresh"] = True
|
||||||
|
session["_id"] = current_app.login_manager._session_identifier_generator()
|
||||||
|
user_login_confirmed.send(current_app._get_current_object())
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(func):
|
||||||
|
"""
|
||||||
|
If you decorate a view with this, it will ensure that the current user is
|
||||||
|
logged in and authenticated before calling the actual view. (If they are
|
||||||
|
not, it calls the :attr:`LoginManager.unauthorized` callback.) For
|
||||||
|
example::
|
||||||
|
|
||||||
|
@app.route('/post')
|
||||||
|
@user_login_required
|
||||||
|
def post():
|
||||||
|
pass
|
||||||
|
|
||||||
|
If there are only certain times you need to require that your user is
|
||||||
|
logged in, you can do so with::
|
||||||
|
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
return current_app.login_manager.unauthorized()
|
||||||
|
|
||||||
|
...which is essentially the code that this function adds to your views.
|
||||||
|
|
||||||
|
It can be convenient to globally turn off authentication when unit testing.
|
||||||
|
To enable this, if the application configuration variable `LOGIN_DISABLED`
|
||||||
|
is set to `True`, this decorator will be ignored.
|
||||||
|
|
||||||
|
.. Note ::
|
||||||
|
|
||||||
|
Per `W3 guidelines for CORS preflight requests
|
||||||
|
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
|
||||||
|
HTTP ``OPTIONS`` requests are exempt from login checks.
|
||||||
|
|
||||||
|
:param func: The view function to decorate.
|
||||||
|
:type func: function
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"):
|
||||||
|
pass
|
||||||
|
elif not current_user.is_authenticated:
|
||||||
|
return current_app.login_manager.unauthorized()
|
||||||
|
|
||||||
|
# flask 1.x compatibility
|
||||||
|
# current_app.ensure_sync is only available in Flask >= 2.0
|
||||||
|
if callable(getattr(current_app, "ensure_sync", None)):
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
|
def fresh_login_required(func):
|
||||||
|
"""
|
||||||
|
If you decorate a view with this, it will ensure that the current user's
|
||||||
|
login is fresh - i.e. their session was not restored from a 'remember me'
|
||||||
|
cookie. Sensitive operations, like changing a password or e-mail, should
|
||||||
|
be protected with this, to impede the efforts of cookie thieves.
|
||||||
|
|
||||||
|
If the user is not authenticated, :meth:`LoginManager.unauthorized` is
|
||||||
|
called as normal. If they are authenticated, but their session is not
|
||||||
|
fresh, it will call :meth:`LoginManager.needs_refresh` instead. (In that
|
||||||
|
case, you will need to provide a :attr:`LoginManager.refresh_view`.)
|
||||||
|
|
||||||
|
Behaves identically to the :func:`login_required` decorator with respect
|
||||||
|
to configuration variables.
|
||||||
|
|
||||||
|
.. Note ::
|
||||||
|
|
||||||
|
Per `W3 guidelines for CORS preflight requests
|
||||||
|
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
|
||||||
|
HTTP ``OPTIONS`` requests are exempt from login checks.
|
||||||
|
|
||||||
|
:param func: The view function to decorate.
|
||||||
|
:type func: function
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"):
|
||||||
|
pass
|
||||||
|
elif not current_user.is_authenticated:
|
||||||
|
return current_app.login_manager.unauthorized()
|
||||||
|
elif not login_fresh():
|
||||||
|
return current_app.login_manager.needs_refresh()
|
||||||
|
try:
|
||||||
|
# current_app.ensure_sync available in Flask >= 2.0
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
except AttributeError: # pragma: no cover
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
|
def set_login_view(login_view, blueprint=None):
|
||||||
|
"""
|
||||||
|
Sets the login view for the app or blueprint. If a blueprint is passed,
|
||||||
|
the login view is set for this blueprint on ``blueprint_login_views``.
|
||||||
|
|
||||||
|
:param login_view: The user object to log in.
|
||||||
|
:type login_view: str
|
||||||
|
:param blueprint: The blueprint which this login view should be set on.
|
||||||
|
Defaults to ``None``.
|
||||||
|
:type blueprint: object
|
||||||
|
"""
|
||||||
|
|
||||||
|
num_login_views = len(current_app.login_manager.blueprint_login_views)
|
||||||
|
if blueprint is not None or num_login_views != 0:
|
||||||
|
(current_app.login_manager.blueprint_login_views[blueprint.name]) = login_view
|
||||||
|
|
||||||
|
if (
|
||||||
|
current_app.login_manager.login_view is not None
|
||||||
|
and None not in current_app.login_manager.blueprint_login_views
|
||||||
|
):
|
||||||
|
(
|
||||||
|
current_app.login_manager.blueprint_login_views[None]
|
||||||
|
) = current_app.login_manager.login_view
|
||||||
|
|
||||||
|
current_app.login_manager.login_view = None
|
||||||
|
else:
|
||||||
|
current_app.login_manager.login_view = login_view
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user():
|
||||||
|
if has_request_context():
|
||||||
|
if "flask_httpauth_user" in g:
|
||||||
|
if g.flask_httpauth_user is not None:
|
||||||
|
return g.flask_httpauth_user
|
||||||
|
if "_login_user" not in g:
|
||||||
|
current_app.login_manager._load_user()
|
||||||
|
|
||||||
|
return g._login_user
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _cookie_digest(payload, key=None):
|
||||||
|
key = _secret_key(key)
|
||||||
|
|
||||||
|
return hmac.new(key, payload.encode("utf-8"), sha512).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_remote_addr():
|
||||||
|
address = request.headers.get("X-Forwarded-For", request.remote_addr)
|
||||||
|
if address is not None:
|
||||||
|
# An 'X-Forwarded-For' header includes a comma separated list of the
|
||||||
|
# addresses, the first address being the actual remote address.
|
||||||
|
address = address.encode("utf-8").split(b",")[0].strip()
|
||||||
|
return address
|
||||||
|
|
||||||
|
|
||||||
|
def _create_identifier():
|
||||||
|
user_agent = request.headers.get("User-Agent")
|
||||||
|
if user_agent is not None:
|
||||||
|
user_agent = user_agent.encode("utf-8")
|
||||||
|
base = f"{_get_remote_addr()}|{user_agent}"
|
||||||
|
if str is bytes:
|
||||||
|
base = str(base, "utf-8", errors="replace") # pragma: no cover
|
||||||
|
h = sha512()
|
||||||
|
h.update(base.encode("utf8"))
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _user_context_processor():
|
||||||
|
return dict(current_user=_get_user())
|
||||||
|
|
||||||
|
|
||||||
|
def _secret_key(key=None):
|
||||||
|
if key is None:
|
||||||
|
key = current_app.config["SECRET_KEY"]
|
||||||
|
|
||||||
|
if isinstance(key, str): # pragma: no cover
|
||||||
|
key = key.encode("latin1") # ensure bytes
|
||||||
|
|
||||||
|
return key
|
@ -23,6 +23,7 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
import unidecode
|
import unidecode
|
||||||
|
from weakref import WeakSet
|
||||||
|
|
||||||
from sqlite3 import OperationalError as sqliteOperationalError
|
from sqlite3 import OperationalError as sqliteOperationalError
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@ -40,7 +41,7 @@ except ImportError:
|
|||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask import flash
|
from flask import flash
|
||||||
@ -48,8 +49,6 @@ from flask import flash
|
|||||||
from . import logger, ub, isoLanguages
|
from . import logger, ub, isoLanguages
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
|
|
||||||
from weakref import WeakSet
|
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ from flask import Blueprint, request, flash, redirect, url_for, abort, Response
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import current_user, login_required
|
from .cw_login import current_user, login_required
|
||||||
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
|
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
|
||||||
from sqlalchemy.orm.exc import StaleDataError
|
from sqlalchemy.orm.exc import StaleDataError
|
||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func
|
||||||
@ -43,10 +43,11 @@ from . import config, ub, db, calibre_db
|
|||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks.upload import TaskUpload
|
from .tasks.upload import TaskUpload
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .usermanagement import login_required_if_no_ano
|
|
||||||
from .kobo_sync_status import change_archived_books
|
from .kobo_sync_status import change_archived_books
|
||||||
from .redirect import get_redirect_location
|
from .redirect import get_redirect_location
|
||||||
from .file_helper import validate_mime_type
|
from .file_helper import validate_mime_type
|
||||||
|
from .usermanagement import user_login_required, login_required_if_no_ano
|
||||||
|
|
||||||
|
|
||||||
editbook = Blueprint('edit-book', __name__)
|
editbook = Blueprint('edit-book', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@ -73,14 +74,14 @@ def edit_required(f):
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def delete_book_from_details(book_id):
|
def delete_book_from_details(book_id):
|
||||||
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
|
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
||||||
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def delete_book_ajax(book_id, book_format):
|
def delete_book_ajax(book_id, book_format):
|
||||||
return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', ""))
|
return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', ""))
|
||||||
|
|
||||||
@ -331,7 +332,7 @@ def convert_bookformat(book_id):
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/getcustomenum/<int:c_id>")
|
@editbook.route("/ajax/getcustomenum/<int:c_id>")
|
||||||
@login_required
|
@user_login_required
|
||||||
def table_get_custom_enum(c_id):
|
def table_get_custom_enum(c_id):
|
||||||
ret = list()
|
ret = list()
|
||||||
cc = (calibre_db.session.query(db.CustomColumns)
|
cc = (calibre_db.session.query(db.CustomColumns)
|
||||||
@ -455,7 +456,7 @@ def edit_list_book(param):
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_sorted_entry(field, bookid):
|
def get_sorted_entry(field, bookid):
|
||||||
if field in ['title', 'authors', 'sort', 'author_sort']:
|
if field in ['title', 'authors', 'sort', 'author_sort']:
|
||||||
book = calibre_db.get_filtered_book(bookid)
|
book = calibre_db.get_filtered_book(bookid)
|
||||||
@ -472,7 +473,7 @@ def get_sorted_entry(field, bookid):
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/simulatemerge", methods=['POST'])
|
@editbook.route("/ajax/simulatemerge", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@edit_required
|
@edit_required
|
||||||
def simulate_merge_list_book():
|
def simulate_merge_list_book():
|
||||||
vals = request.get_json().get('Merge_books')
|
vals = request.get_json().get('Merge_books')
|
||||||
@ -488,7 +489,7 @@ def simulate_merge_list_book():
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/mergebooks", methods=['POST'])
|
@editbook.route("/ajax/mergebooks", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@edit_required
|
@edit_required
|
||||||
def merge_list_book():
|
def merge_list_book():
|
||||||
vals = request.get_json().get('Merge_books')
|
vals = request.get_json().get('Merge_books')
|
||||||
@ -526,7 +527,7 @@ def merge_list_book():
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/xchange", methods=['POST'])
|
@editbook.route("/ajax/xchange", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@edit_required
|
@edit_required
|
||||||
def table_xchange_author_title():
|
def table_xchange_author_title():
|
||||||
vals = request.get_json().get('xchange')
|
vals = request.get_json().get('xchange')
|
||||||
|
@ -29,11 +29,11 @@ from shutil import move, copyfile
|
|||||||
|
|
||||||
from flask import Blueprint, flash, request, redirect, url_for, abort
|
from flask import Blueprint, flash, request, redirect, url_for, abort
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
from . import logger, gdriveutils, config, ub, calibre_db, csrf
|
from . import logger, gdriveutils, config, ub, calibre_db, csrf
|
||||||
from .admin import admin_required
|
from .admin import admin_required
|
||||||
from .file_helper import get_temp_dir
|
from .file_helper import get_temp_dir
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@ -49,7 +49,7 @@ gdrive_watch_callback_token = 'target=calibreweb-watch_files' # nosec
|
|||||||
|
|
||||||
|
|
||||||
@gdrive.route("/authenticate")
|
@gdrive.route("/authenticate")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def authenticate_google_drive():
|
def authenticate_google_drive():
|
||||||
try:
|
try:
|
||||||
@ -76,7 +76,7 @@ def google_drive_callback():
|
|||||||
|
|
||||||
|
|
||||||
@gdrive.route("/watch/subscribe")
|
@gdrive.route("/watch/subscribe")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def watch_gdrive():
|
def watch_gdrive():
|
||||||
if not config.config_google_drive_watch_changes_response:
|
if not config.config_google_drive_watch_changes_response:
|
||||||
@ -102,7 +102,7 @@ def watch_gdrive():
|
|||||||
|
|
||||||
|
|
||||||
@gdrive.route("/watch/revoke")
|
@gdrive.route("/watch/revoke")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def revoke_watch_gdrive():
|
def revoke_watch_gdrive():
|
||||||
last_watch_response = config.config_google_drive_watch_changes_response
|
last_watch_response = config.config_google_drive_watch_changes_response
|
||||||
|
@ -34,7 +34,7 @@ from flask import send_from_directory, make_response, abort, url_for, Response
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
|
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
|
@ -27,10 +27,9 @@ import datetime
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
# from babel.dates import format_date
|
|
||||||
from flask import Blueprint, request, url_for
|
from flask import Blueprint, request, url_for
|
||||||
from flask_babel import format_date
|
from flask_babel import format_date
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
|
|
||||||
from . import constants, logger
|
from . import constants, logger
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ from flask import (
|
|||||||
redirect,
|
redirect,
|
||||||
abort
|
abort
|
||||||
)
|
)
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
@ -44,7 +44,6 @@ from sqlalchemy.exc import StatementError
|
|||||||
from sqlalchemy.sql import select
|
from sqlalchemy.sql import select
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||||
from . import isoLanguages
|
from . import isoLanguages
|
||||||
from .epub import get_epub_layout
|
from .epub import get_epub_layout
|
||||||
|
@ -65,12 +65,14 @@ from os import urandom
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import g, Blueprint, abort, request
|
from flask import g, Blueprint, abort, request
|
||||||
from flask_login import login_user, current_user, login_required
|
from .cw_login import login_user, current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_limiter import RateLimitExceeded
|
from flask_limiter import RateLimitExceeded
|
||||||
|
|
||||||
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@ -78,7 +80,7 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
|||||||
|
|
||||||
|
|
||||||
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||||
@login_required
|
@user_login_required
|
||||||
def generate_auth_token(user_id):
|
def generate_auth_token(user_id):
|
||||||
warning = False
|
warning = False
|
||||||
host_list = request.host.rsplit(':')
|
host_list = request.host.rsplit(':')
|
||||||
@ -120,7 +122,7 @@ def generate_auth_token(user_id):
|
|||||||
|
|
||||||
|
|
||||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def delete_auth_token(user_id):
|
def delete_auth_token(user_id):
|
||||||
# Invalidate any previously generated Kobo Auth token for this user
|
# Invalidate any previously generated Kobo Auth token for this user
|
||||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||||
|
@ -17,11 +17,11 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from . import ub
|
from . import ub
|
||||||
import datetime
|
import datetime
|
||||||
from sqlalchemy.sql.expression import or_, and_, true
|
from sqlalchemy.sql.expression import or_, and_, true
|
||||||
from sqlalchemy import exc
|
# from sqlalchemy import exc
|
||||||
|
|
||||||
|
|
||||||
# Add the current book id to kobo_synced_books table for current user, if entry is already present,
|
# Add the current book id to kobo_synced_books table for current user, if entry is already present,
|
||||||
|
@ -30,8 +30,9 @@ from flask_dance.consumer import oauth_authorized, oauth_error
|
|||||||
from flask_dance.contrib.github import make_github_blueprint, github
|
from flask_dance.contrib.github import make_github_blueprint, github
|
||||||
from flask_dance.contrib.google import make_google_blueprint, google
|
from flask_dance.contrib.google import make_google_blueprint, google
|
||||||
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
|
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
|
||||||
from flask_login import login_user, current_user, login_required
|
from .cw_login import login_user, current_user
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
from . import constants, logger, config, app, ub
|
from . import constants, logger, config, app, ub
|
||||||
|
|
||||||
@ -340,7 +341,7 @@ def github_login():
|
|||||||
|
|
||||||
|
|
||||||
@oauth.route('/unlink/github', methods=["GET"])
|
@oauth.route('/unlink/github', methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def github_login_unlink():
|
def github_login_unlink():
|
||||||
return unlink_oauth(oauthblueprints[0]['id'])
|
return unlink_oauth(oauthblueprints[0]['id'])
|
||||||
|
|
||||||
@ -364,6 +365,6 @@ def google_login():
|
|||||||
|
|
||||||
|
|
||||||
@oauth.route('/unlink/google', methods=["GET"])
|
@oauth.route('/unlink/google', methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def google_login_unlink():
|
def google_login_unlink():
|
||||||
return unlink_oauth(oauthblueprints[1]['id'])
|
return unlink_oauth(oauthblueprints[1]['id'])
|
||||||
|
53
cps/opds.py
53
cps/opds.py
@ -25,14 +25,15 @@ import json
|
|||||||
from urllib.parse import unquote_plus
|
from urllib.parse import unquote_plus
|
||||||
|
|
||||||
from flask import Blueprint, request, render_template, make_response, abort, Response, g
|
from flask import Blueprint, request, render_template, make_response, abort, Response, g
|
||||||
from flask_login import current_user
|
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
|
||||||
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
|
|
||||||
from . import logger, config, db, calibre_db, ub, isoLanguages, constants
|
from . import logger, config, db, calibre_db, ub, isoLanguages, constants
|
||||||
from .usermanagement import requires_basic_auth_if_no_ano
|
from .usermanagement import requires_basic_auth_if_no_ano, auth
|
||||||
from .helper import get_download_link, get_book_cover
|
from .helper import get_download_link, get_book_cover
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .web import render_read_books
|
from .web import render_read_books
|
||||||
@ -94,7 +95,7 @@ def feed_letter_books(book_id):
|
|||||||
@opds.route("/opds/new")
|
@opds.route("/opds/new")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_new():
|
def feed_new():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_RECENT):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_RECENT):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
|
||||||
@ -106,7 +107,7 @@ def feed_new():
|
|||||||
@opds.route("/opds/discover")
|
@opds.route("/opds/discover")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_discover():
|
def feed_discover():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_RANDOM):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_RANDOM):
|
||||||
abort(404)
|
abort(404)
|
||||||
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
||||||
entries = query.filter(calibre_db.common_filters()).order_by(func.random()).limit(config.config_books_per_page)
|
entries = query.filter(calibre_db.common_filters()).order_by(func.random()).limit(config.config_books_per_page)
|
||||||
@ -117,7 +118,7 @@ def feed_discover():
|
|||||||
@opds.route("/opds/rated")
|
@opds.route("/opds/rated")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_best_rated():
|
def feed_best_rated():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_BEST_RATED):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_BEST_RATED):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
|
||||||
@ -130,7 +131,7 @@ def feed_best_rated():
|
|||||||
@opds.route("/opds/hot")
|
@opds.route("/opds/hot")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_hot():
|
def feed_hot():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_HOT):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_HOT):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)).order_by(
|
all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)).order_by(
|
||||||
@ -154,7 +155,7 @@ def feed_hot():
|
|||||||
@opds.route("/opds/author")
|
@opds.route("/opds/author")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_authorindex():
|
def feed_authorindex():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_AUTHOR):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_AUTHOR):
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_element_index(db.Authors.sort, db.books_authors_link, 'opds.feed_letter_author')
|
return render_element_index(db.Authors.sort, db.books_authors_link, 'opds.feed_letter_author')
|
||||||
|
|
||||||
@ -162,7 +163,7 @@ def feed_authorindex():
|
|||||||
@opds.route("/opds/author/letter/<book_id>")
|
@opds.route("/opds/author/letter/<book_id>")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_letter_author(book_id):
|
def feed_letter_author(book_id):
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_AUTHOR):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_AUTHOR):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id)
|
letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id)
|
||||||
@ -185,7 +186,7 @@ def feed_author(book_id):
|
|||||||
@opds.route("/opds/publisher")
|
@opds.route("/opds/publisher")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_publisherindex():
|
def feed_publisherindex():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_PUBLISHER):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_PUBLISHER):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries = calibre_db.session.query(db.Publishers)\
|
entries = calibre_db.session.query(db.Publishers)\
|
||||||
@ -208,7 +209,7 @@ def feed_publisher(book_id):
|
|||||||
@opds.route("/opds/category")
|
@opds.route("/opds/category")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_categoryindex():
|
def feed_categoryindex():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_CATEGORY):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_CATEGORY):
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_element_index(db.Tags.name, db.books_tags_link, 'opds.feed_letter_category')
|
return render_element_index(db.Tags.name, db.books_tags_link, 'opds.feed_letter_category')
|
||||||
|
|
||||||
@ -216,7 +217,7 @@ def feed_categoryindex():
|
|||||||
@opds.route("/opds/category/letter/<book_id>")
|
@opds.route("/opds/category/letter/<book_id>")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_letter_category(book_id):
|
def feed_letter_category(book_id):
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_CATEGORY):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_CATEGORY):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id)
|
letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id)
|
||||||
@ -241,7 +242,7 @@ def feed_category(book_id):
|
|||||||
@opds.route("/opds/series")
|
@opds.route("/opds/series")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_seriesindex():
|
def feed_seriesindex():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_SERIES):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_SERIES):
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_element_index(db.Series.sort, db.books_series_link, 'opds.feed_letter_series')
|
return render_element_index(db.Series.sort, db.books_series_link, 'opds.feed_letter_series')
|
||||||
|
|
||||||
@ -249,7 +250,7 @@ def feed_seriesindex():
|
|||||||
@opds.route("/opds/series/letter/<book_id>")
|
@opds.route("/opds/series/letter/<book_id>")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_letter_series(book_id):
|
def feed_letter_series(book_id):
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_SERIES):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_SERIES):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id)
|
letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id)
|
||||||
@ -280,7 +281,7 @@ def feed_series(book_id):
|
|||||||
@opds.route("/opds/ratings")
|
@opds.route("/opds/ratings")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_ratingindex():
|
def feed_ratingindex():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_RATING):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_RATING):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
|
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
|
||||||
@ -308,7 +309,7 @@ def feed_ratings(book_id):
|
|||||||
@opds.route("/opds/formats")
|
@opds.route("/opds/formats")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_formatindex():
|
def feed_formatindex():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_FORMAT):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_FORMAT):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
entries = calibre_db.session.query(db.Data).join(db.Books)\
|
entries = calibre_db.session.query(db.Data).join(db.Books)\
|
||||||
@ -339,14 +340,14 @@ def feed_format(book_id):
|
|||||||
@opds.route("/opds/language/")
|
@opds.route("/opds/language/")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_languagesindex():
|
def feed_languagesindex():
|
||||||
if not current_user.check_visibility(constants.SIDEBAR_LANGUAGE):
|
if not auth.current_user().check_visibility(constants.SIDEBAR_LANGUAGE):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
if current_user.filter_language() == "all":
|
if auth.current_user().filter_language() == "all":
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
else:
|
else:
|
||||||
languages = calibre_db.session.query(db.Languages).filter(
|
languages = calibre_db.session.query(db.Languages).filter(
|
||||||
db.Languages.lang_code == current_user.filter_language()).all()
|
db.Languages.lang_code == auth.current_user().filter_language()).all()
|
||||||
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
|
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(languages))
|
len(languages))
|
||||||
@ -368,11 +369,11 @@ def feed_languages(book_id):
|
|||||||
@opds.route("/opds/shelfindex")
|
@opds.route("/opds/shelfindex")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_shelfindex():
|
def feed_shelfindex():
|
||||||
if not (current_user.is_authenticated or g.allow_anonymous):
|
if not (auth.current_user().is_authenticated or g.allow_anonymous):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
shelf = ub.session.query(ub.Shelf).filter(
|
shelf = 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()
|
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == auth.current_user().id)).order_by(ub.Shelf.name).all()
|
||||||
number = len(shelf)
|
number = len(shelf)
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
number)
|
number)
|
||||||
@ -382,14 +383,14 @@ def feed_shelfindex():
|
|||||||
@opds.route("/opds/shelf/<int:book_id>")
|
@opds.route("/opds/shelf/<int:book_id>")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_shelf(book_id):
|
def feed_shelf(book_id):
|
||||||
if not (current_user.is_authenticated or g.allow_anonymous):
|
if not (auth.current_user().is_authenticated or g.allow_anonymous):
|
||||||
abort(404)
|
abort(404)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
if current_user.is_anonymous:
|
if auth.current_user().is_anonymous:
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1,
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1,
|
||||||
ub.Shelf.id == book_id).first()
|
ub.Shelf.id == book_id).first()
|
||||||
else:
|
else:
|
||||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(auth.current_user().id),
|
||||||
ub.Shelf.id == book_id),
|
ub.Shelf.id == book_id),
|
||||||
and_(ub.Shelf.is_public == 1,
|
and_(ub.Shelf.is_public == 1,
|
||||||
ub.Shelf.id == book_id))).first()
|
ub.Shelf.id == book_id))).first()
|
||||||
@ -422,7 +423,7 @@ def feed_shelf(book_id):
|
|||||||
@opds.route("/opds/download/<book_id>/<book_format>/")
|
@opds.route("/opds/download/<book_id>/<book_format>/")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def opds_download_link(book_id, book_format):
|
def opds_download_link(book_id, book_format):
|
||||||
if not current_user.role_download():
|
if not auth.current_user().role_download():
|
||||||
return abort(403)
|
return abort(403)
|
||||||
if "Kobo" in request.headers.get('User-Agent'):
|
if "Kobo" in request.headers.get('User-Agent'):
|
||||||
client = "kobo"
|
client = "kobo"
|
||||||
@ -468,7 +469,7 @@ def feed_get_cover(book_id):
|
|||||||
@opds.route("/opds/readbooks")
|
@opds.route("/opds/readbooks")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_read_books():
|
def feed_read_books():
|
||||||
if not (current_user.check_visibility(constants.SIDEBAR_READ_AND_UNREAD) and not current_user.is_anonymous):
|
if not (auth.current_user().check_visibility(constants.SIDEBAR_READ_AND_UNREAD) and not auth.current_user().is_anonymous):
|
||||||
return abort(403)
|
return abort(403)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
|
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
|
||||||
@ -478,7 +479,7 @@ def feed_read_books():
|
|||||||
@opds.route("/opds/unreadbooks")
|
@opds.route("/opds/unreadbooks")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_unread_books():
|
def feed_unread_books():
|
||||||
if not (current_user.check_visibility(constants.SIDEBAR_READ_AND_UNREAD) and not current_user.is_anonymous):
|
if not (auth.current_user().check_visibility(constants.SIDEBAR_READ_AND_UNREAD) and not auth.current_user().is_anonymous):
|
||||||
return abort(403)
|
return abort(403)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
|
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
|
||||||
|
@ -25,12 +25,13 @@ from datetime import datetime
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import Blueprint, request, make_response, abort, url_for, flash, redirect
|
from flask import Blueprint, request, make_response, abort, url_for, flash, redirect
|
||||||
from flask_login import login_required, current_user, login_user
|
from .cw_login import login_user, current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from sqlalchemy.sql.expression import true
|
from sqlalchemy.sql.expression import true
|
||||||
|
|
||||||
from . import config, logger, ub
|
from . import config, logger, ub
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
|
|
||||||
remotelogin = Blueprint('remotelogin', __name__)
|
remotelogin = Blueprint('remotelogin', __name__)
|
||||||
@ -65,7 +66,7 @@ def remote_login():
|
|||||||
|
|
||||||
@remotelogin.route('/verify/<token>')
|
@remotelogin.route('/verify/<token>')
|
||||||
@remote_login_required
|
@remote_login_required
|
||||||
@login_required
|
@user_login_required
|
||||||
def verify_token(token):
|
def verify_token(token):
|
||||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
|
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
|
||||||
|
|
||||||
|
@ -19,14 +19,13 @@
|
|||||||
from flask import render_template, g, abort, request
|
from flask import render_template, g, abort, request
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from sqlalchemy.sql.expression import or_
|
from sqlalchemy.sql.expression import or_
|
||||||
|
|
||||||
from . import config, constants, logger, ub
|
from . import config, constants, logger, ub
|
||||||
from .ub import User
|
from .ub import User
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
def get_sidebar_config(kwargs=None):
|
def get_sidebar_config(kwargs=None):
|
||||||
|
@ -21,7 +21,7 @@ import datetime
|
|||||||
from . import config, constants
|
from . import config, constants
|
||||||
from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
|
from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
|
||||||
from .tasks.database import TaskReconnectDatabase
|
from .tasks.database import TaskReconnectDatabase
|
||||||
from .tasks.tempFolder import TaskDeleteTempFolder
|
from .tasks.clean import TaskClean
|
||||||
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
|
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks.metadata_backup import TaskBackupMetadata
|
from .tasks.metadata_backup import TaskBackupMetadata
|
||||||
@ -33,7 +33,7 @@ def get_scheduled_tasks(reconnect=True):
|
|||||||
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
||||||
|
|
||||||
# Delete temp folder
|
# Delete temp folder
|
||||||
tasks.append([lambda: TaskDeleteTempFolder(), 'delete temp', True])
|
tasks.append([lambda: TaskClean(), 'delete temp', True])
|
||||||
|
|
||||||
# Generate metadata.opf file for each changed book
|
# Generate metadata.opf file for each changed book
|
||||||
if config.schedule_metadata_backup:
|
if config.schedule_metadata_backup:
|
||||||
@ -94,7 +94,7 @@ def register_startup_tasks():
|
|||||||
if constants.APP_MODE in ['development', 'test'] and not should_task_be_running(start, duration):
|
if constants.APP_MODE in ['development', 'test'] and not should_task_be_running(start, duration):
|
||||||
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
|
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
|
||||||
else:
|
else:
|
||||||
scheduler.schedule_tasks_immediately(tasks=[[lambda: TaskDeleteTempFolder(), 'delete temp', True]])
|
scheduler.schedule_tasks_immediately(tasks=[[lambda: TaskClean(), 'delete temp', True]])
|
||||||
|
|
||||||
|
|
||||||
def should_task_be_running(start, duration):
|
def should_task_be_running(start, duration):
|
||||||
|
@ -19,7 +19,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from flask import Blueprint, request, redirect, url_for, flash
|
from flask import Blueprint, request, redirect, url_for, flash
|
||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from flask_babel import format_date
|
from flask_babel import format_date
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from sqlalchemy.sql.expression import func, not_, and_, or_, text, true
|
from sqlalchemy.sql.expression import func, not_, and_, or_, text, true
|
||||||
@ -30,6 +30,7 @@ from .usermanagement import login_required_if_no_ano
|
|||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
|
|
||||||
|
|
||||||
search = Blueprint('search', __name__)
|
search = Blueprint('search', __name__)
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
@ -24,14 +24,14 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from flask import Blueprint, Response, request, url_for
|
from flask import Blueprint, Response, request, url_for
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from flask_login import login_required
|
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
from cps.services.Metadata import Metadata
|
from cps.services.Metadata import Metadata
|
||||||
from . import constants, logger, ub, web_server
|
from . import constants, logger, ub, web_server
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
# current_milli_time = lambda: int(round(time() * 1000))
|
# current_milli_time = lambda: int(round(time() * 1000))
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ cl = list_classes(new_list)
|
|||||||
|
|
||||||
|
|
||||||
@meta.route("/metadata/provider")
|
@meta.route("/metadata/provider")
|
||||||
@login_required
|
@user_login_required
|
||||||
def metadata_provider():
|
def metadata_provider():
|
||||||
active = current_user.view_settings.get("metadata", {})
|
active = current_user.view_settings.get("metadata", {})
|
||||||
provider = list()
|
provider = list()
|
||||||
@ -95,7 +95,7 @@ def metadata_provider():
|
|||||||
|
|
||||||
@meta.route("/metadata/provider", methods=["POST"])
|
@meta.route("/metadata/provider", methods=["POST"])
|
||||||
@meta.route("/metadata/provider/<prov_name>", methods=["POST"])
|
@meta.route("/metadata/provider/<prov_name>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def metadata_change_active_provider(prov_name):
|
def metadata_change_active_provider(prov_name):
|
||||||
new_state = request.get_json()
|
new_state = request.get_json()
|
||||||
active = current_user.view_settings.get("metadata", {})
|
active = current_user.view_settings.get("metadata", {})
|
||||||
@ -122,7 +122,7 @@ def metadata_change_active_provider(prov_name):
|
|||||||
|
|
||||||
|
|
||||||
@meta.route("/metadata/search", methods=["POST"])
|
@meta.route("/metadata/search", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def metadata_search():
|
def metadata_search():
|
||||||
query = request.form.to_dict().get("query")
|
query = request.form.to_dict().get("query")
|
||||||
data = list()
|
data = list()
|
||||||
|
18
cps/shelf.py
18
cps/shelf.py
@ -25,13 +25,13 @@ from datetime import datetime
|
|||||||
|
|
||||||
from flask import Blueprint, flash, redirect, request, url_for, abort
|
from flask import Blueprint, flash, redirect, request, url_for, abort
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from .cw_login import current_user
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
from sqlalchemy.sql.expression import func, true
|
from sqlalchemy.sql.expression import func, true
|
||||||
|
|
||||||
from . import calibre_db, config, db, logger, ub
|
from . import calibre_db, config, db, logger, ub
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .usermanagement import login_required_if_no_ano
|
from .usermanagement import login_required_if_no_ano, user_login_required
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ shelf = Blueprint('shelf', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def add_to_shelf(shelf_id, book_id):
|
def add_to_shelf(shelf_id, book_id):
|
||||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
@ -103,7 +103,7 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
|
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def search_to_shelf(shelf_id):
|
def search_to_shelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf is None:
|
if shelf is None:
|
||||||
@ -155,7 +155,7 @@ def search_to_shelf(shelf_id):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def remove_from_shelf(shelf_id, book_id):
|
def remove_from_shelf(shelf_id, book_id):
|
||||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
@ -212,14 +212,14 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/create", methods=["GET", "POST"])
|
@shelf.route("/shelf/create", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def create_shelf():
|
def create_shelf():
|
||||||
shelf = ub.Shelf()
|
shelf = ub.Shelf()
|
||||||
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
|
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def edit_shelf(shelf_id):
|
def edit_shelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if not check_shelf_edit_permissions(shelf):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
@ -229,7 +229,7 @@ def edit_shelf(shelf_id):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def delete_shelf(shelf_id):
|
def delete_shelf(shelf_id):
|
||||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
try:
|
try:
|
||||||
@ -259,7 +259,7 @@ def show_shelf(shelf_id, sort_param, page):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def order_shelf(shelf_id):
|
def order_shelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf and check_shelf_view_permissions(shelf):
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
|
15
cps/static/css/libs/viewer.css
vendored
15
cps/static/css/libs/viewer.css
vendored
@ -1894,6 +1894,8 @@
|
|||||||
width:100%;
|
width:100%;
|
||||||
height:100%;
|
height:100%;
|
||||||
margin:0;
|
margin:0;
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers{
|
.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers{
|
||||||
@ -2646,6 +2648,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdfViewer.copyAll{
|
||||||
|
cursor:wait;
|
||||||
|
}
|
||||||
|
|
||||||
.pdfViewer .canvasWrapper{
|
.pdfViewer .canvasWrapper{
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
width:100%;
|
width:100%;
|
||||||
@ -3010,6 +3016,15 @@ body{
|
|||||||
scrollbar-color:var(--scrollbar-color) var(--scrollbar-bg-color);
|
scrollbar-color:var(--scrollbar-color) var(--scrollbar-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.wait::before{
|
||||||
|
content:"";
|
||||||
|
position:fixed;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
z-index:100000;
|
||||||
|
cursor:wait;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden,
|
.hidden,
|
||||||
[hidden]{
|
[hidden]{
|
||||||
display:none !important;
|
display:none !important;
|
||||||
|
573
cps/static/js/libs/pdf.mjs
vendored
573
cps/static/js/libs/pdf.mjs
vendored
File diff suppressed because it is too large
Load Diff
566
cps/static/js/libs/pdf.worker.mjs
vendored
566
cps/static/js/libs/pdf.worker.mjs
vendored
File diff suppressed because one or more lines are too long
266
cps/static/js/libs/viewer.mjs
vendored
266
cps/static/js/libs/viewer.mjs
vendored
@ -142,7 +142,7 @@ function scrollIntoView(element, spot, scrollMatches = false) {
|
|||||||
}
|
}
|
||||||
parent.scrollTop = offsetY;
|
parent.scrollTop = offsetY;
|
||||||
}
|
}
|
||||||
function watchScroll(viewAreaElement, callback) {
|
function watchScroll(viewAreaElement, callback, abortSignal = undefined) {
|
||||||
const debounceScroll = function (evt) {
|
const debounceScroll = function (evt) {
|
||||||
if (rAF) {
|
if (rAF) {
|
||||||
return;
|
return;
|
||||||
@ -172,7 +172,13 @@ function watchScroll(viewAreaElement, callback) {
|
|||||||
_eventHandler: debounceScroll
|
_eventHandler: debounceScroll
|
||||||
};
|
};
|
||||||
let rAF = null;
|
let rAF = null;
|
||||||
viewAreaElement.addEventListener("scroll", debounceScroll, true);
|
viewAreaElement.addEventListener("scroll", debounceScroll, {
|
||||||
|
useCapture: true,
|
||||||
|
signal: abortSignal
|
||||||
|
});
|
||||||
|
abortSignal?.addEventListener("abort", () => window.cancelAnimationFrame(rAF), {
|
||||||
|
once: true
|
||||||
|
});
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
function parseQueryString(query) {
|
function parseQueryString(query) {
|
||||||
@ -250,9 +256,8 @@ function approximateFraction(x) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
function roundToDivide(x, div) {
|
function floorToDivide(x, div) {
|
||||||
const r = x % div;
|
return x - x % div;
|
||||||
return r === 0 ? x : Math.round(x - r + div);
|
|
||||||
}
|
}
|
||||||
function getPageSizeInches({
|
function getPageSizeInches({
|
||||||
view,
|
view,
|
||||||
@ -738,6 +743,10 @@ const defaultOptions = {
|
|||||||
value: "",
|
value: "",
|
||||||
kind: OptionKind.API
|
kind: OptionKind.API
|
||||||
},
|
},
|
||||||
|
enableHWA: {
|
||||||
|
value: true,
|
||||||
|
kind: OptionKind.API + OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
|
},
|
||||||
enableXfa: {
|
enableXfa: {
|
||||||
value: true,
|
value: true,
|
||||||
kind: OptionKind.API + OptionKind.PREFERENCE
|
kind: OptionKind.API + OptionKind.PREFERENCE
|
||||||
@ -1357,9 +1366,18 @@ class EventBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AutomationEventBus extends EventBus {
|
class FirefoxEventBus extends EventBus {
|
||||||
|
#externalServices;
|
||||||
|
#globalEventNames;
|
||||||
|
#isInAutomation;
|
||||||
|
constructor(globalEventNames, externalServices, isInAutomation) {
|
||||||
|
super();
|
||||||
|
this.#globalEventNames = globalEventNames;
|
||||||
|
this.#externalServices = externalServices;
|
||||||
|
this.#isInAutomation = isInAutomation;
|
||||||
|
}
|
||||||
dispatch(eventName, data) {
|
dispatch(eventName, data) {
|
||||||
throw new Error("Not implemented: AutomationEventBus.dispatch");
|
throw new Error("Not implemented: FirefoxEventBus.dispatch");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1384,6 +1402,10 @@ class BaseExternalServices {
|
|||||||
throw new Error("Not implemented: updateEditorStates");
|
throw new Error("Not implemented: updateEditorStates");
|
||||||
}
|
}
|
||||||
async getNimbusExperimentData() {}
|
async getNimbusExperimentData() {}
|
||||||
|
async getGlobalEventNames() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
dispatchGlobalEvent(_event) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./web/preferences.js
|
;// CONCATENATED MODULE: ./web/preferences.js
|
||||||
@ -1430,6 +1452,7 @@ class BasePreferences {
|
|||||||
disableFontFace: false,
|
disableFontFace: false,
|
||||||
disableRange: false,
|
disableRange: false,
|
||||||
disableStream: false,
|
disableStream: false,
|
||||||
|
enableHWA: true,
|
||||||
enableXfa: true,
|
enableXfa: true,
|
||||||
viewerCssTheme: 0
|
viewerCssTheme: 0
|
||||||
});
|
});
|
||||||
@ -2743,6 +2766,9 @@ class DOMLocalization extends Localization {
|
|||||||
this.pauseObserving();
|
this.pauseObserving();
|
||||||
if (this.roots.size === 0) {
|
if (this.roots.size === 0) {
|
||||||
this.mutationObserver = null;
|
this.mutationObserver = null;
|
||||||
|
if (this.windowElement && this.pendingrAF) {
|
||||||
|
this.windowElement.cancelAnimationFrame(this.pendingrAF);
|
||||||
|
}
|
||||||
this.windowElement = null;
|
this.windowElement = null;
|
||||||
this.pendingrAF = null;
|
this.pendingrAF = null;
|
||||||
this.pendingElements.clear();
|
this.pendingElements.clear();
|
||||||
@ -2843,6 +2869,7 @@ class DOMLocalization extends Localization {
|
|||||||
;// CONCATENATED MODULE: ./web/l10n.js
|
;// CONCATENATED MODULE: ./web/l10n.js
|
||||||
class L10n {
|
class L10n {
|
||||||
#dir;
|
#dir;
|
||||||
|
#elements = new Set();
|
||||||
#lang;
|
#lang;
|
||||||
#l10n;
|
#l10n;
|
||||||
constructor({
|
constructor({
|
||||||
@ -2877,11 +2904,19 @@ class L10n {
|
|||||||
return messages?.[0].value || fallback;
|
return messages?.[0].value || fallback;
|
||||||
}
|
}
|
||||||
async translate(element) {
|
async translate(element) {
|
||||||
|
this.#elements.add(element);
|
||||||
try {
|
try {
|
||||||
this.#l10n.connectRoot(element);
|
this.#l10n.connectRoot(element);
|
||||||
await this.#l10n.translateRoots();
|
await this.#l10n.translateRoots();
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
async destroy() {
|
||||||
|
for (const element of this.#elements) {
|
||||||
|
this.#l10n.disconnectRoot(element);
|
||||||
|
}
|
||||||
|
this.#elements.clear();
|
||||||
|
this.#l10n.pauseObserving();
|
||||||
|
}
|
||||||
pause() {
|
pause() {
|
||||||
this.#l10n.pauseObserving();
|
this.#l10n.pauseObserving();
|
||||||
}
|
}
|
||||||
@ -2954,8 +2989,7 @@ class genericl10n_GenericL10n extends L10n {
|
|||||||
const bundle = await this.#createBundle(lang, baseURL, paths);
|
const bundle = await this.#createBundle(lang, baseURL, paths);
|
||||||
if (bundle) {
|
if (bundle) {
|
||||||
yield bundle;
|
yield bundle;
|
||||||
}
|
} else if (lang === "en-us") {
|
||||||
if (lang === "en-us") {
|
|
||||||
yield this.#createBundleFallback(lang);
|
yield this.#createBundleFallback(lang);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3625,13 +3659,6 @@ function download(blobUrl, filename) {
|
|||||||
}
|
}
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
#openBlobUrls = new WeakMap();
|
#openBlobUrls = new WeakMap();
|
||||||
downloadUrl(url, filename, _options) {
|
|
||||||
if (!createValidAbsoluteUrl(url, "http://example.com")) {
|
|
||||||
console.error(`downloadUrl - not a valid URL: ${url}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
download(url + "#pdfjs.action=download", filename);
|
|
||||||
}
|
|
||||||
downloadData(data, filename, contentType) {
|
downloadData(data, filename, contentType) {
|
||||||
const blobUrl = URL.createObjectURL(new Blob([data], {
|
const blobUrl = URL.createObjectURL(new Blob([data], {
|
||||||
type: contentType
|
type: contentType
|
||||||
@ -3666,8 +3693,19 @@ class DownloadManager {
|
|||||||
this.downloadData(data, filename, contentType);
|
this.downloadData(data, filename, contentType);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
download(blob, url, filename, _options) {
|
download(data, url, filename, _options) {
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
let blobUrl;
|
||||||
|
if (data) {
|
||||||
|
blobUrl = URL.createObjectURL(new Blob([data], {
|
||||||
|
type: "application/pdf"
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
if (!createValidAbsoluteUrl(url, "http://example.com")) {
|
||||||
|
console.error(`download - not a valid URL: ${url}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
blobUrl = url + "#pdfjs.action=download";
|
||||||
|
}
|
||||||
download(blobUrl, filename);
|
download(blobUrl, filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5180,6 +5218,7 @@ class PDFFindController {
|
|||||||
source: this,
|
source: this,
|
||||||
state,
|
state,
|
||||||
previous,
|
previous,
|
||||||
|
entireWord: this.#state?.entireWord ?? null,
|
||||||
matchesCount: this.#requestMatchesCount(),
|
matchesCount: this.#requestMatchesCount(),
|
||||||
rawQuery: this.#state?.query ?? null
|
rawQuery: this.#state?.query ?? null
|
||||||
});
|
});
|
||||||
@ -7711,7 +7750,8 @@ class PDFThumbnailView {
|
|||||||
optionalContentConfigPromise,
|
optionalContentConfigPromise,
|
||||||
linkService,
|
linkService,
|
||||||
renderingQueue,
|
renderingQueue,
|
||||||
pageColors
|
pageColors,
|
||||||
|
enableHWA
|
||||||
}) {
|
}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.renderingId = "thumbnail" + id;
|
this.renderingId = "thumbnail" + id;
|
||||||
@ -7722,6 +7762,7 @@ class PDFThumbnailView {
|
|||||||
this.pdfPageRotate = defaultViewport.rotation;
|
this.pdfPageRotate = defaultViewport.rotation;
|
||||||
this._optionalContentConfigPromise = optionalContentConfigPromise || null;
|
this._optionalContentConfigPromise = optionalContentConfigPromise || null;
|
||||||
this.pageColors = pageColors || null;
|
this.pageColors = pageColors || null;
|
||||||
|
this.enableHWA = enableHWA || false;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.linkService = linkService;
|
this.linkService = linkService;
|
||||||
this.renderingQueue = renderingQueue;
|
this.renderingQueue = renderingQueue;
|
||||||
@ -7805,10 +7846,11 @@ class PDFThumbnailView {
|
|||||||
}
|
}
|
||||||
this.resume = null;
|
this.resume = null;
|
||||||
}
|
}
|
||||||
#getPageDrawContext(upscaleFactor = 1) {
|
#getPageDrawContext(upscaleFactor = 1, enableHWA = this.enableHWA) {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d", {
|
const ctx = canvas.getContext("2d", {
|
||||||
alpha: false
|
alpha: false,
|
||||||
|
willReadFrequently: !enableHWA
|
||||||
});
|
});
|
||||||
const outputScale = new OutputScale();
|
const outputScale = new OutputScale();
|
||||||
canvas.width = upscaleFactor * this.canvasWidth * outputScale.sx | 0;
|
canvas.width = upscaleFactor * this.canvasWidth * outputScale.sx | 0;
|
||||||
@ -7927,7 +7969,7 @@ class PDFThumbnailView {
|
|||||||
const {
|
const {
|
||||||
ctx,
|
ctx,
|
||||||
canvas
|
canvas
|
||||||
} = this.#getPageDrawContext();
|
} = this.#getPageDrawContext(1, true);
|
||||||
if (img.width <= 2 * canvas.width) {
|
if (img.width <= 2 * canvas.width) {
|
||||||
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
|
||||||
return canvas;
|
return canvas;
|
||||||
@ -7974,14 +8016,17 @@ class PDFThumbnailViewer {
|
|||||||
eventBus,
|
eventBus,
|
||||||
linkService,
|
linkService,
|
||||||
renderingQueue,
|
renderingQueue,
|
||||||
pageColors
|
pageColors,
|
||||||
|
abortSignal,
|
||||||
|
enableHWA
|
||||||
}) {
|
}) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.linkService = linkService;
|
this.linkService = linkService;
|
||||||
this.renderingQueue = renderingQueue;
|
this.renderingQueue = renderingQueue;
|
||||||
this.pageColors = pageColors || null;
|
this.pageColors = pageColors || null;
|
||||||
this.scroll = watchScroll(this.container, this.#scrollUpdated.bind(this));
|
this.enableHWA = enableHWA || false;
|
||||||
|
this.scroll = watchScroll(this.container, this.#scrollUpdated.bind(this), abortSignal);
|
||||||
this.#resetView();
|
this.#resetView();
|
||||||
}
|
}
|
||||||
#scrollUpdated() {
|
#scrollUpdated() {
|
||||||
@ -8102,7 +8147,8 @@ class PDFThumbnailViewer {
|
|||||||
optionalContentConfigPromise,
|
optionalContentConfigPromise,
|
||||||
linkService: this.linkService,
|
linkService: this.linkService,
|
||||||
renderingQueue: this.renderingQueue,
|
renderingQueue: this.renderingQueue,
|
||||||
pageColors: this.pageColors
|
pageColors: this.pageColors,
|
||||||
|
enableHWA: this.enableHWA
|
||||||
});
|
});
|
||||||
this._thumbnails.push(thumbnail);
|
this._thumbnails.push(thumbnail);
|
||||||
}
|
}
|
||||||
@ -8932,13 +8978,6 @@ class TextLayerBuilder {
|
|||||||
this.div.tabIndex = 0;
|
this.div.tabIndex = 0;
|
||||||
this.div.className = "textLayer";
|
this.div.className = "textLayer";
|
||||||
}
|
}
|
||||||
#finishRendering() {
|
|
||||||
this.#renderingDone = true;
|
|
||||||
const endOfContent = document.createElement("div");
|
|
||||||
endOfContent.className = "endOfContent";
|
|
||||||
this.div.append(endOfContent);
|
|
||||||
this.#bindMouse(endOfContent);
|
|
||||||
}
|
|
||||||
async render(viewport, textContentParams = null) {
|
async render(viewport, textContentParams = null) {
|
||||||
if (this.#renderingDone && this.#textLayer) {
|
if (this.#renderingDone && this.#textLayer) {
|
||||||
this.#textLayer.update({
|
this.#textLayer.update({
|
||||||
@ -8964,7 +9003,11 @@ class TextLayerBuilder {
|
|||||||
this.highlighter?.setTextMapping(textDivs, textContentItemsStr);
|
this.highlighter?.setTextMapping(textDivs, textContentItemsStr);
|
||||||
this.accessibilityManager?.setTextMapping(textDivs);
|
this.accessibilityManager?.setTextMapping(textDivs);
|
||||||
await this.#textLayer.render();
|
await this.#textLayer.render();
|
||||||
this.#finishRendering();
|
this.#renderingDone = true;
|
||||||
|
const endOfContent = document.createElement("div");
|
||||||
|
endOfContent.className = "endOfContent";
|
||||||
|
this.div.append(endOfContent);
|
||||||
|
this.#bindMouse(endOfContent);
|
||||||
this.#onAppend?.(this.div);
|
this.#onAppend?.(this.div);
|
||||||
this.highlighter?.enable();
|
this.highlighter?.enable();
|
||||||
this.accessibilityManager?.enable();
|
this.accessibilityManager?.enable();
|
||||||
@ -9097,6 +9140,7 @@ const DEFAULT_LAYER_PROPERTIES = null;
|
|||||||
const LAYERS_ORDER = new Map([["canvasWrapper", 0], ["textLayer", 1], ["annotationLayer", 2], ["annotationEditorLayer", 3], ["xfaLayer", 3]]);
|
const LAYERS_ORDER = new Map([["canvasWrapper", 0], ["textLayer", 1], ["annotationLayer", 2], ["annotationEditorLayer", 3], ["xfaLayer", 3]]);
|
||||||
class PDFPageView {
|
class PDFPageView {
|
||||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||||
|
#enableHWA = false;
|
||||||
#hasRestrictedScaling = false;
|
#hasRestrictedScaling = false;
|
||||||
#layerProperties = null;
|
#layerProperties = null;
|
||||||
#loadingId = null;
|
#loadingId = null;
|
||||||
@ -9129,6 +9173,7 @@ class PDFPageView {
|
|||||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||||
this.maxCanvasPixels = options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
|
this.maxCanvasPixels = options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
|
||||||
this.pageColors = options.pageColors || null;
|
this.pageColors = options.pageColors || null;
|
||||||
|
this.#enableHWA = options.enableHWA || false;
|
||||||
this.eventBus = options.eventBus;
|
this.eventBus = options.eventBus;
|
||||||
this.renderingQueue = options.renderingQueue;
|
this.renderingQueue = options.renderingQueue;
|
||||||
this.l10n = options.l10n;
|
this.l10n = options.l10n;
|
||||||
@ -9739,7 +9784,8 @@ class PDFPageView {
|
|||||||
canvasWrapper.append(canvas);
|
canvasWrapper.append(canvas);
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
const ctx = canvas.getContext("2d", {
|
const ctx = canvas.getContext("2d", {
|
||||||
alpha: false
|
alpha: false,
|
||||||
|
willReadFrequently: !this.#enableHWA
|
||||||
});
|
});
|
||||||
const outputScale = this.outputScale = new OutputScale();
|
const outputScale = this.outputScale = new OutputScale();
|
||||||
if (this.maxCanvasPixels === 0) {
|
if (this.maxCanvasPixels === 0) {
|
||||||
@ -9760,13 +9806,13 @@ class PDFPageView {
|
|||||||
}
|
}
|
||||||
const sfx = approximateFraction(outputScale.sx);
|
const sfx = approximateFraction(outputScale.sx);
|
||||||
const sfy = approximateFraction(outputScale.sy);
|
const sfy = approximateFraction(outputScale.sy);
|
||||||
canvas.width = roundToDivide(width * outputScale.sx, sfx[0]);
|
canvas.width = floorToDivide(width * outputScale.sx, sfx[0]);
|
||||||
canvas.height = roundToDivide(height * outputScale.sy, sfy[0]);
|
canvas.height = floorToDivide(height * outputScale.sy, sfy[0]);
|
||||||
const {
|
const {
|
||||||
style
|
style
|
||||||
} = canvas;
|
} = canvas;
|
||||||
style.width = roundToDivide(width, sfx[1]) + "px";
|
style.width = floorToDivide(width, sfx[1]) + "px";
|
||||||
style.height = roundToDivide(height, sfy[1]) + "px";
|
style.height = floorToDivide(height, sfy[1]) + "px";
|
||||||
this.#viewportMap.set(canvas, viewport);
|
this.#viewportMap.set(canvas, viewport);
|
||||||
const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null;
|
const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null;
|
||||||
const renderContext = {
|
const renderContext = {
|
||||||
@ -9870,8 +9916,8 @@ class PDFPageView {
|
|||||||
|
|
||||||
const DEFAULT_CACHE_SIZE = 10;
|
const DEFAULT_CACHE_SIZE = 10;
|
||||||
const PagesCountLimit = {
|
const PagesCountLimit = {
|
||||||
FORCE_SCROLL_MODE_PAGE: 15000,
|
FORCE_SCROLL_MODE_PAGE: 10000,
|
||||||
FORCE_LAZY_PAGE_INIT: 7500,
|
FORCE_LAZY_PAGE_INIT: 5000,
|
||||||
PAUSE_EAGER_PAGE_INIT: 250
|
PAUSE_EAGER_PAGE_INIT: 250
|
||||||
};
|
};
|
||||||
function isValidAnnotationEditorMode(mode) {
|
function isValidAnnotationEditorMode(mode) {
|
||||||
@ -9933,6 +9979,7 @@ class PDFViewer {
|
|||||||
#annotationEditorUIManager = null;
|
#annotationEditorUIManager = null;
|
||||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||||
#containerTopLeft = null;
|
#containerTopLeft = null;
|
||||||
|
#enableHWA = false;
|
||||||
#enableHighlightFloatingButton = false;
|
#enableHighlightFloatingButton = false;
|
||||||
#enablePermissions = false;
|
#enablePermissions = false;
|
||||||
#eventAbortController = null;
|
#eventAbortController = null;
|
||||||
@ -9946,7 +9993,7 @@ class PDFViewer {
|
|||||||
#scaleTimeoutId = null;
|
#scaleTimeoutId = null;
|
||||||
#textLayerMode = TextLayerMode.ENABLE;
|
#textLayerMode = TextLayerMode.ENABLE;
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
const viewerVersion = "4.3.136";
|
const viewerVersion = "4.4.168";
|
||||||
if (version !== viewerVersion) {
|
if (version !== viewerVersion) {
|
||||||
throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`);
|
throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`);
|
||||||
}
|
}
|
||||||
@ -9982,6 +10029,7 @@ class PDFViewer {
|
|||||||
this.#enablePermissions = options.enablePermissions || false;
|
this.#enablePermissions = options.enablePermissions || false;
|
||||||
this.pageColors = options.pageColors || null;
|
this.pageColors = options.pageColors || null;
|
||||||
this.#mlManager = options.mlManager || null;
|
this.#mlManager = options.mlManager || null;
|
||||||
|
this.#enableHWA = options.enableHWA || false;
|
||||||
this.defaultRenderingQueue = !options.renderingQueue;
|
this.defaultRenderingQueue = !options.renderingQueue;
|
||||||
if (this.defaultRenderingQueue) {
|
if (this.defaultRenderingQueue) {
|
||||||
this.renderingQueue = new PDFRenderingQueue();
|
this.renderingQueue = new PDFRenderingQueue();
|
||||||
@ -9989,7 +10037,16 @@ class PDFViewer {
|
|||||||
} else {
|
} else {
|
||||||
this.renderingQueue = options.renderingQueue;
|
this.renderingQueue = options.renderingQueue;
|
||||||
}
|
}
|
||||||
this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this));
|
const {
|
||||||
|
abortSignal
|
||||||
|
} = options;
|
||||||
|
abortSignal?.addEventListener("abort", () => {
|
||||||
|
this.#resizeObserver.disconnect();
|
||||||
|
this.#resizeObserver = null;
|
||||||
|
}, {
|
||||||
|
once: true
|
||||||
|
});
|
||||||
|
this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this), abortSignal);
|
||||||
this.presentationModeState = PresentationModeState.UNKNOWN;
|
this.presentationModeState = PresentationModeState.UNKNOWN;
|
||||||
this._resetView();
|
this._resetView();
|
||||||
if (this.removePageBorders) {
|
if (this.removePageBorders) {
|
||||||
@ -10254,10 +10311,14 @@ class PDFViewer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#getAllTextInProgress = true;
|
this.#getAllTextInProgress = true;
|
||||||
const savedCursor = this.container.style.cursor;
|
const {
|
||||||
this.container.style.cursor = "wait";
|
classList
|
||||||
const interruptCopy = ev => this.#interruptCopyCondition = ev.key === "Escape";
|
} = this.viewer;
|
||||||
window.addEventListener("keydown", interruptCopy);
|
classList.add("copyAll");
|
||||||
|
const ac = new AbortController();
|
||||||
|
window.addEventListener("keydown", ev => this.#interruptCopyCondition = ev.key === "Escape", {
|
||||||
|
signal: ac.signal
|
||||||
|
});
|
||||||
this.getAllText().then(async text => {
|
this.getAllText().then(async text => {
|
||||||
if (text !== null) {
|
if (text !== null) {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
@ -10267,8 +10328,8 @@ class PDFViewer {
|
|||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.#getAllTextInProgress = false;
|
this.#getAllTextInProgress = false;
|
||||||
this.#interruptCopyCondition = false;
|
this.#interruptCopyCondition = false;
|
||||||
window.removeEventListener("keydown", interruptCopy);
|
ac.abort();
|
||||||
this.container.style.cursor = savedCursor;
|
classList.remove("copyAll");
|
||||||
});
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -10401,7 +10462,8 @@ class PDFViewer {
|
|||||||
maxCanvasPixels: this.maxCanvasPixels,
|
maxCanvasPixels: this.maxCanvasPixels,
|
||||||
pageColors,
|
pageColors,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
layerProperties: this._layerProperties
|
layerProperties: this._layerProperties,
|
||||||
|
enableHWA: this.#enableHWA
|
||||||
});
|
});
|
||||||
this._pages.push(pageView);
|
this._pages.push(pageView);
|
||||||
}
|
}
|
||||||
@ -12059,9 +12121,11 @@ const PDFViewerApplication = {
|
|||||||
isViewerEmbedded: window.parent !== window,
|
isViewerEmbedded: window.parent !== window,
|
||||||
url: "",
|
url: "",
|
||||||
baseUrl: "",
|
baseUrl: "",
|
||||||
|
_allowedGlobalEventsPromise: null,
|
||||||
_downloadUrl: "",
|
_downloadUrl: "",
|
||||||
_eventBusAbortController: null,
|
_eventBusAbortController: null,
|
||||||
_windowAbortController: null,
|
_windowAbortController: null,
|
||||||
|
_globalAbortController: new AbortController(),
|
||||||
documentInfo: null,
|
documentInfo: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
_contentDispositionFilename: null,
|
_contentDispositionFilename: null,
|
||||||
@ -12202,7 +12266,8 @@ const PDFViewerApplication = {
|
|||||||
externalServices,
|
externalServices,
|
||||||
l10n
|
l10n
|
||||||
} = this;
|
} = this;
|
||||||
const eventBus = AppOptions.get("isInAutomation") ? new AutomationEventBus() : new EventBus();
|
let eventBus;
|
||||||
|
eventBus = new EventBus();
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.overlayManager = new OverlayManager();
|
this.overlayManager = new OverlayManager();
|
||||||
const pdfRenderingQueue = new PDFRenderingQueue();
|
const pdfRenderingQueue = new PDFRenderingQueue();
|
||||||
@ -12236,6 +12301,7 @@ const PDFViewerApplication = {
|
|||||||
foreground: AppOptions.get("pageColorsForeground")
|
foreground: AppOptions.get("pageColorsForeground")
|
||||||
} : null;
|
} : null;
|
||||||
const altTextManager = appConfig.altTextDialog ? new AltTextManager(appConfig.altTextDialog, container, this.overlayManager, eventBus) : null;
|
const altTextManager = appConfig.altTextDialog ? new AltTextManager(appConfig.altTextDialog, container, this.overlayManager, eventBus) : null;
|
||||||
|
const enableHWA = AppOptions.get("enableHWA");
|
||||||
const pdfViewer = new PDFViewer({
|
const pdfViewer = new PDFViewer({
|
||||||
container,
|
container,
|
||||||
viewer,
|
viewer,
|
||||||
@ -12257,7 +12323,9 @@ const PDFViewerApplication = {
|
|||||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||||
enablePermissions: AppOptions.get("enablePermissions"),
|
enablePermissions: AppOptions.get("enablePermissions"),
|
||||||
pageColors,
|
pageColors,
|
||||||
mlManager: this.mlManager
|
mlManager: this.mlManager,
|
||||||
|
abortSignal: this._globalAbortController.signal,
|
||||||
|
enableHWA
|
||||||
});
|
});
|
||||||
this.pdfViewer = pdfViewer;
|
this.pdfViewer = pdfViewer;
|
||||||
pdfRenderingQueue.setViewer(pdfViewer);
|
pdfRenderingQueue.setViewer(pdfViewer);
|
||||||
@ -12269,7 +12337,9 @@ const PDFViewerApplication = {
|
|||||||
eventBus,
|
eventBus,
|
||||||
renderingQueue: pdfRenderingQueue,
|
renderingQueue: pdfRenderingQueue,
|
||||||
linkService: pdfLinkService,
|
linkService: pdfLinkService,
|
||||||
pageColors
|
pageColors,
|
||||||
|
abortSignal: this._globalAbortController.signal,
|
||||||
|
enableHWA
|
||||||
});
|
});
|
||||||
pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
|
pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
|
||||||
}
|
}
|
||||||
@ -12378,7 +12448,7 @@ const PDFViewerApplication = {
|
|||||||
const params = parseQueryString(queryString);
|
const params = parseQueryString(queryString);
|
||||||
file = params.get("file") ?? AppOptions.get("defaultUrl");
|
file = params.get("file") ?? AppOptions.get("defaultUrl");
|
||||||
validateFileURL(file);
|
validateFileURL(file);
|
||||||
/* const fileInput = this._openFileInput = document.createElement("input");
|
/*const fileInput = this._openFileInput = document.createElement("input");
|
||||||
fileInput.id = "fileInput";
|
fileInput.id = "fileInput";
|
||||||
fileInput.hidden = true;
|
fileInput.hidden = true;
|
||||||
fileInput.type = "file";
|
fileInput.type = "file";
|
||||||
@ -12395,24 +12465,28 @@ const PDFViewerApplication = {
|
|||||||
source: this,
|
source: this,
|
||||||
fileInput: evt.target
|
fileInput: evt.target
|
||||||
});
|
});
|
||||||
});*/
|
|
||||||
appConfig.mainContainer.addEventListener("dragover", function (evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
evt.dataTransfer.dropEffect = evt.dataTransfer.effectAllowed === "copy" ? "copy" : "move";
|
|
||||||
});
|
});
|
||||||
appConfig.mainContainer.addEventListener("drop", function (evt) {
|
appConfig.mainContainer.addEventListener("dragover", function (evt) {
|
||||||
|
for (const item of evt.dataTransfer.items) {
|
||||||
|
if (item.type === "application/pdf") {
|
||||||
|
evt.dataTransfer.dropEffect = evt.dataTransfer.effectAllowed === "copy" ? "copy" : "move";
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const {
|
evt.stopPropagation();
|
||||||
files
|
|
||||||
} = evt.dataTransfer;
|
|
||||||
if (!files || files.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/*eventBus.dispatch("fileinputchange", {
|
}
|
||||||
|
});
|
||||||
|
appConfig.mainContainer.addEventListener("drop", function (evt) {
|
||||||
|
if (evt.dataTransfer.files?.[0].type !== "application/pdf") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
eventBus.dispatch("fileinputchange", {
|
||||||
source: this,
|
source: this,
|
||||||
fileInput: evt.dataTransfer
|
fileInput: evt.dataTransfer
|
||||||
});*/
|
|
||||||
});
|
});
|
||||||
|
});*/
|
||||||
if (!AppOptions.get("supportsDocumentFonts")) {
|
if (!AppOptions.get("supportsDocumentFonts")) {
|
||||||
AppOptions.set("disableFontFace", true);
|
AppOptions.set("disableFontFace", true);
|
||||||
this.l10n.get("pdfjs-web-fonts-disabled").then(msg => {
|
this.l10n.get("pdfjs-web-fonts-disabled").then(msg => {
|
||||||
@ -12647,25 +12721,14 @@ const PDFViewerApplication = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_ensureDownloadComplete() {
|
|
||||||
if (this.pdfDocument && this.downloadComplete) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error("PDF document not downloaded.");
|
|
||||||
},
|
|
||||||
async download(options = {}) {
|
async download(options = {}) {
|
||||||
const url = this._downloadUrl,
|
let data;
|
||||||
filename = this._docFilename;
|
|
||||||
try {
|
try {
|
||||||
this._ensureDownloadComplete();
|
if (this.downloadComplete) {
|
||||||
const data = await this.pdfDocument.getData();
|
data = await this.pdfDocument.getData();
|
||||||
const blob = new Blob([data], {
|
|
||||||
type: "application/pdf"
|
|
||||||
});
|
|
||||||
await this.downloadManager.download(blob, url, filename, options);
|
|
||||||
} catch {
|
|
||||||
await this.downloadManager.downloadUrl(url, filename, options);
|
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
|
this.downloadManager.download(data, this._downloadUrl, this._docFilename, options);
|
||||||
},
|
},
|
||||||
async save(options = {}) {
|
async save(options = {}) {
|
||||||
if (this._saveInProgress) {
|
if (this._saveInProgress) {
|
||||||
@ -12673,15 +12736,9 @@ const PDFViewerApplication = {
|
|||||||
}
|
}
|
||||||
this._saveInProgress = true;
|
this._saveInProgress = true;
|
||||||
await this.pdfScriptingManager.dispatchWillSave();
|
await this.pdfScriptingManager.dispatchWillSave();
|
||||||
const url = this._downloadUrl,
|
|
||||||
filename = this._docFilename;
|
|
||||||
try {
|
try {
|
||||||
this._ensureDownloadComplete();
|
|
||||||
const data = await this.pdfDocument.saveDocument();
|
const data = await this.pdfDocument.saveDocument();
|
||||||
const blob = new Blob([data], {
|
this.downloadManager.download(data, this._downloadUrl, this._docFilename, options);
|
||||||
type: "application/pdf"
|
|
||||||
});
|
|
||||||
await this.downloadManager.download(blob, url, filename, options);
|
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
console.error(`Error when saving the document: ${reason.message}`);
|
console.error(`Error when saving the document: ${reason.message}`);
|
||||||
await this.download(options);
|
await this.download(options);
|
||||||
@ -12699,12 +12756,13 @@ const PDFViewerApplication = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
downloadOrSave(options = {}) {
|
async downloadOrSave(options = {}) {
|
||||||
if (this.pdfDocument?.annotationStorage.size > 0) {
|
const {
|
||||||
this.save(options);
|
classList
|
||||||
} else {
|
} = this.appConfig.appContainer;
|
||||||
this.download(options);
|
classList.add("wait");
|
||||||
}
|
await (this.pdfDocument?.annotationStorage.size > 0 ? this.save(options) : this.download(options));
|
||||||
|
classList.remove("wait");
|
||||||
},
|
},
|
||||||
async _documentError(key, moreInfo = null) {
|
async _documentError(key, moreInfo = null) {
|
||||||
this._unblockDocumentLoadEvent();
|
this._unblockDocumentLoadEvent();
|
||||||
@ -13471,6 +13529,14 @@ const PDFViewerApplication = {
|
|||||||
this._windowAbortController?.abort();
|
this._windowAbortController?.abort();
|
||||||
this._windowAbortController = null;
|
this._windowAbortController = null;
|
||||||
},
|
},
|
||||||
|
async testingClose() {
|
||||||
|
this.unbindEvents();
|
||||||
|
this.unbindWindowEvents();
|
||||||
|
this._globalAbortController?.abort();
|
||||||
|
this._globalAbortController = null;
|
||||||
|
this.findBar?.close();
|
||||||
|
await Promise.all([this.l10n?.destroy(), this.close()]);
|
||||||
|
},
|
||||||
_accumulateTicks(ticks, prop) {
|
_accumulateTicks(ticks, prop) {
|
||||||
if (this[prop] > 0 && ticks < 0 || this[prop] < 0 && ticks > 0) {
|
if (this[prop] > 0 && ticks < 0 || this[prop] < 0 && ticks > 0) {
|
||||||
this[prop] = 0;
|
this[prop] = 0;
|
||||||
@ -13664,7 +13730,7 @@ function webViewerHashchange(evt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
var webViewerFileInputChange = function (evt) {
|
/*var webViewerFileInputChange = function (evt) {
|
||||||
if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
|
if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -13674,7 +13740,7 @@ function webViewerHashchange(evt) {
|
|||||||
originalUrl: file.name
|
originalUrl: file.name
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/*var webViewerOpenFile = function (evt) {
|
var webViewerOpenFile = function (evt) {
|
||||||
PDFViewerApplication._openFileInput?.click();
|
PDFViewerApplication._openFileInput?.click();
|
||||||
};*/
|
};*/
|
||||||
}
|
}
|
||||||
@ -13768,6 +13834,7 @@ function webViewerUpdateFindMatchesCount({
|
|||||||
function webViewerUpdateFindControlState({
|
function webViewerUpdateFindControlState({
|
||||||
state,
|
state,
|
||||||
previous,
|
previous,
|
||||||
|
entireWord,
|
||||||
matchesCount,
|
matchesCount,
|
||||||
rawQuery
|
rawQuery
|
||||||
}) {
|
}) {
|
||||||
@ -13775,6 +13842,7 @@ function webViewerUpdateFindControlState({
|
|||||||
PDFViewerApplication.externalServices.updateFindControlState({
|
PDFViewerApplication.externalServices.updateFindControlState({
|
||||||
result: state,
|
result: state,
|
||||||
findPrevious: previous,
|
findPrevious: previous,
|
||||||
|
entireWord,
|
||||||
matchesCount,
|
matchesCount,
|
||||||
rawQuery
|
rawQuery
|
||||||
});
|
});
|
||||||
@ -14066,14 +14134,14 @@ function webViewerKeyDown(evt) {
|
|||||||
});
|
});
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
/*case 79:
|
case 79:
|
||||||
{
|
{
|
||||||
eventBus.dispatch("openfile", {
|
eventBus.dispatch("openfile", {
|
||||||
source: window
|
source: window
|
||||||
});
|
});
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;*/
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cmd === 3 || cmd === 10) {
|
if (cmd === 3 || cmd === 10) {
|
||||||
@ -14273,8 +14341,8 @@ function webViewerReportTelemetry({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const pdfjsVersion = "4.3.136";
|
const pdfjsVersion = "4.4.168";
|
||||||
const pdfjsBuild = "0cec64437";
|
const pdfjsBuild = "19fbc8998";
|
||||||
const AppConstants = {
|
const AppConstants = {
|
||||||
LinkTarget: LinkTarget,
|
LinkTarget: LinkTarget,
|
||||||
RenderingStates: RenderingStates,
|
RenderingStates: RenderingStates,
|
||||||
|
@ -49,12 +49,6 @@ pdfjs-download-button =
|
|||||||
# Length of the translation matters since we are in a mobile context, with limited screen estate.
|
# Length of the translation matters since we are in a mobile context, with limited screen estate.
|
||||||
pdfjs-download-button-label = Pellgargañ
|
pdfjs-download-button-label = Pellgargañ
|
||||||
pdfjs-bookmark-button-label = Pajenn a-vremañ
|
pdfjs-bookmark-button-label = Pajenn a-vremañ
|
||||||
# Used in Firefox for Android.
|
|
||||||
pdfjs-open-in-app-button =
|
|
||||||
.title = Digeriñ en arload
|
|
||||||
# Used in Firefox for Android.
|
|
||||||
# Length of the translation matters since we are in a mobile context, with limited screen estate.
|
|
||||||
pdfjs-open-in-app-button-label = Digeriñ en arload
|
|
||||||
|
|
||||||
## Secondary toolbar and context menu
|
## Secondary toolbar and context menu
|
||||||
|
|
||||||
@ -214,6 +208,7 @@ pdfjs-find-next-button =
|
|||||||
pdfjs-find-next-button-label = War-lerc'h
|
pdfjs-find-next-button-label = War-lerc'h
|
||||||
pdfjs-find-highlight-checkbox = Usskediñ pep tra
|
pdfjs-find-highlight-checkbox = Usskediñ pep tra
|
||||||
pdfjs-find-match-case-checkbox-label = Teurel evezh ouzh ar pennlizherennoù
|
pdfjs-find-match-case-checkbox-label = Teurel evezh ouzh ar pennlizherennoù
|
||||||
|
pdfjs-find-match-diacritics-checkbox-label = Doujañ d’an tiredoù
|
||||||
pdfjs-find-entire-word-checkbox-label = Gerioù a-bezh
|
pdfjs-find-entire-word-checkbox-label = Gerioù a-bezh
|
||||||
pdfjs-find-reached-top = Tizhet eo bet derou ar bajenn, kenderc'hel diouzh an diaz
|
pdfjs-find-reached-top = Tizhet eo bet derou ar bajenn, kenderc'hel diouzh an diaz
|
||||||
pdfjs-find-reached-bottom = Tizhet eo bet dibenn ar bajenn, kenderc'hel diouzh ar c'hrec'h
|
pdfjs-find-reached-bottom = Tizhet eo bet dibenn ar bajenn, kenderc'hel diouzh ar c'hrec'h
|
||||||
@ -311,3 +306,7 @@ pdfjs-editor-alt-text-save-button = Enrollañ
|
|||||||
|
|
||||||
## Color picker
|
## Color picker
|
||||||
|
|
||||||
|
|
||||||
|
## Show all highlights
|
||||||
|
## This is a toggle button to show/hide all the highlights.
|
||||||
|
|
||||||
|
@ -51,12 +51,6 @@ pdfjs-download-button-label = Sækja
|
|||||||
pdfjs-bookmark-button =
|
pdfjs-bookmark-button =
|
||||||
.title = Núverandi síða (Skoða vefslóð frá núverandi síðu)
|
.title = Núverandi síða (Skoða vefslóð frá núverandi síðu)
|
||||||
pdfjs-bookmark-button-label = Núverandi síða
|
pdfjs-bookmark-button-label = Núverandi síða
|
||||||
# Used in Firefox for Android.
|
|
||||||
pdfjs-open-in-app-button =
|
|
||||||
.title = Opna í smáforriti
|
|
||||||
# Used in Firefox for Android.
|
|
||||||
# Length of the translation matters since we are in a mobile context, with limited screen estate.
|
|
||||||
pdfjs-open-in-app-button-label = Opna í smáforriti
|
|
||||||
|
|
||||||
## Secondary toolbar and context menu
|
## Secondary toolbar and context menu
|
||||||
|
|
||||||
@ -284,7 +278,7 @@ pdfjs-text-annotation-type =
|
|||||||
|
|
||||||
## Password
|
## Password
|
||||||
|
|
||||||
pdfjs-password-label = Sláðu inn lykilorð til að opna þessa PDF skrá.
|
pdfjs-password-label = Settu inn lykilorð til að opna þessa PDF-skrá.
|
||||||
pdfjs-password-invalid = Ógilt lykilorð. Reyndu aftur.
|
pdfjs-password-invalid = Ógilt lykilorð. Reyndu aftur.
|
||||||
pdfjs-password-ok-button = Í lagi
|
pdfjs-password-ok-button = Í lagi
|
||||||
pdfjs-password-cancel-button = Hætta við
|
pdfjs-password-cancel-button = Hætta við
|
||||||
@ -304,8 +298,6 @@ pdfjs-editor-stamp-button-label = Bæta við eða breyta myndum
|
|||||||
pdfjs-editor-highlight-button =
|
pdfjs-editor-highlight-button =
|
||||||
.title = Áherslulita
|
.title = Áherslulita
|
||||||
pdfjs-editor-highlight-button-label = Áherslulita
|
pdfjs-editor-highlight-button-label = Áherslulita
|
||||||
pdfjs-highlight-floating-button =
|
|
||||||
.title = Áherslulita
|
|
||||||
pdfjs-highlight-floating-button1 =
|
pdfjs-highlight-floating-button1 =
|
||||||
.title = Áherslulita
|
.title = Áherslulita
|
||||||
.aria-label = Áherslulita
|
.aria-label = Áherslulita
|
||||||
|
@ -279,7 +279,7 @@ pdfjs-text-annotation-type =
|
|||||||
## Password
|
## Password
|
||||||
|
|
||||||
pdfjs-password-label = この PDF ファイルを開くためのパスワードを入力してください。
|
pdfjs-password-label = この PDF ファイルを開くためのパスワードを入力してください。
|
||||||
pdfjs-password-invalid = 無効なパスワードです。もう一度やり直してください。
|
pdfjs-password-invalid = パスワードが正しくありません。もう一度試してください。
|
||||||
pdfjs-password-ok-button = OK
|
pdfjs-password-ok-button = OK
|
||||||
pdfjs-password-cancel-button = キャンセル
|
pdfjs-password-cancel-button = キャンセル
|
||||||
pdfjs-web-fonts-disabled = ウェブフォントが無効になっています: 埋め込まれた PDF のフォントを使用できません。
|
pdfjs-web-fonts-disabled = ウェブフォントが無効になっています: 埋め込まれた PDF のフォントを使用できません。
|
||||||
@ -298,8 +298,6 @@ pdfjs-editor-stamp-button-label = 画像を追加または編集
|
|||||||
pdfjs-editor-highlight-button =
|
pdfjs-editor-highlight-button =
|
||||||
.title = 強調します
|
.title = 強調します
|
||||||
pdfjs-editor-highlight-button-label = 強調
|
pdfjs-editor-highlight-button-label = 強調
|
||||||
pdfjs-highlight-floating-button =
|
|
||||||
.title = 強調
|
|
||||||
pdfjs-highlight-floating-button1 =
|
pdfjs-highlight-floating-button1 =
|
||||||
.title = 強調
|
.title = 強調
|
||||||
.aria-label = 強調します
|
.aria-label = 強調します
|
||||||
|
@ -51,12 +51,6 @@ pdfjs-download-button-label = Sader
|
|||||||
pdfjs-bookmark-button =
|
pdfjs-bookmark-button =
|
||||||
.title = Asebter amiran (Sken-d tansa URL seg usebter amiran)
|
.title = Asebter amiran (Sken-d tansa URL seg usebter amiran)
|
||||||
pdfjs-bookmark-button-label = Asebter amiran
|
pdfjs-bookmark-button-label = Asebter amiran
|
||||||
# Used in Firefox for Android.
|
|
||||||
pdfjs-open-in-app-button =
|
|
||||||
.title = Ldi deg usnas
|
|
||||||
# Used in Firefox for Android.
|
|
||||||
# Length of the translation matters since we are in a mobile context, with limited screen estate.
|
|
||||||
pdfjs-open-in-app-button-label = Ldi deg usnas
|
|
||||||
|
|
||||||
## Secondary toolbar and context menu
|
## Secondary toolbar and context menu
|
||||||
|
|
||||||
@ -301,8 +295,27 @@ pdfjs-editor-ink-button-label = Suneɣ
|
|||||||
pdfjs-editor-stamp-button =
|
pdfjs-editor-stamp-button =
|
||||||
.title = Rnu neɣ ẓreg tugniwin
|
.title = Rnu neɣ ẓreg tugniwin
|
||||||
pdfjs-editor-stamp-button-label = Rnu neɣ ẓreg tugniwin
|
pdfjs-editor-stamp-button-label = Rnu neɣ ẓreg tugniwin
|
||||||
pdfjs-editor-remove-button =
|
pdfjs-editor-highlight-button =
|
||||||
.title = Kkes
|
.title = Derrer
|
||||||
|
pdfjs-editor-highlight-button-label = Derrer
|
||||||
|
pdfjs-highlight-floating-button1 =
|
||||||
|
.title = Derrer
|
||||||
|
.aria-label = Derrer
|
||||||
|
pdfjs-highlight-floating-button-label = Derrer
|
||||||
|
|
||||||
|
## Remove button for the various kind of editor.
|
||||||
|
|
||||||
|
pdfjs-editor-remove-ink-button =
|
||||||
|
.title = Kkes asuneɣ
|
||||||
|
pdfjs-editor-remove-freetext-button =
|
||||||
|
.title = Kkes aḍris
|
||||||
|
pdfjs-editor-remove-stamp-button =
|
||||||
|
.title = Kkes tugna
|
||||||
|
pdfjs-editor-remove-highlight-button =
|
||||||
|
.title = Kkes aderrer
|
||||||
|
|
||||||
|
##
|
||||||
|
|
||||||
# Editor Parameters
|
# Editor Parameters
|
||||||
pdfjs-editor-free-text-color-input = Initen
|
pdfjs-editor-free-text-color-input = Initen
|
||||||
pdfjs-editor-free-text-size-input = Teɣzi
|
pdfjs-editor-free-text-size-input = Teɣzi
|
||||||
@ -312,6 +325,8 @@ pdfjs-editor-ink-opacity-input = Tebrek
|
|||||||
pdfjs-editor-stamp-add-image-button =
|
pdfjs-editor-stamp-add-image-button =
|
||||||
.title = Rnu tawlaft
|
.title = Rnu tawlaft
|
||||||
pdfjs-editor-stamp-add-image-button-label = Rnu tawlaft
|
pdfjs-editor-stamp-add-image-button-label = Rnu tawlaft
|
||||||
|
# This refers to the thickness of the line used for free highlighting (not bound to text)
|
||||||
|
pdfjs-editor-free-highlight-thickness-input = Tuzert
|
||||||
pdfjs-free-text =
|
pdfjs-free-text =
|
||||||
.aria-label = Amaẓrag n uḍris
|
.aria-label = Amaẓrag n uḍris
|
||||||
pdfjs-free-text-default-content = Bdu tira...
|
pdfjs-free-text-default-content = Bdu tira...
|
||||||
@ -335,3 +350,37 @@ pdfjs-editor-alt-text-decorative-tooltip = Yettwacreḍ d adlag
|
|||||||
## Editor resizers
|
## Editor resizers
|
||||||
## This is used in an aria label to help to understand the role of the resizer.
|
## This is used in an aria label to help to understand the role of the resizer.
|
||||||
|
|
||||||
|
pdfjs-editor-resizer-label-top-left = Tiɣmert n ufella n zelmeḍ — semsawi teɣzi
|
||||||
|
pdfjs-editor-resizer-label-top-middle = Talemmat n ufella — semsawi teɣzi
|
||||||
|
pdfjs-editor-resizer-label-top-right = Tiɣmert n ufella n yeffus — semsawi teɣzi
|
||||||
|
pdfjs-editor-resizer-label-middle-right = Talemmast tayeffust — semsawi teɣzi
|
||||||
|
pdfjs-editor-resizer-label-bottom-right = Tiɣmert n wadda n yeffus — semsawi teɣzi
|
||||||
|
pdfjs-editor-resizer-label-bottom-middle = Talemmat n wadda — semsawi teɣzi
|
||||||
|
pdfjs-editor-resizer-label-bottom-left = Tiɣmert n wadda n zelmeḍ — semsawi teɣzi
|
||||||
|
pdfjs-editor-resizer-label-middle-left = Talemmast tazelmdaḍt — semsawi teɣzi
|
||||||
|
|
||||||
|
## Color picker
|
||||||
|
|
||||||
|
# This means "Color used to highlight text"
|
||||||
|
pdfjs-editor-highlight-colorpicker-label = Ini n uderrer
|
||||||
|
pdfjs-editor-colorpicker-button =
|
||||||
|
.title = Senfel ini
|
||||||
|
pdfjs-editor-colorpicker-dropdown =
|
||||||
|
.aria-label = Afran n yiniten
|
||||||
|
pdfjs-editor-colorpicker-yellow =
|
||||||
|
.title = Awraɣ
|
||||||
|
pdfjs-editor-colorpicker-green =
|
||||||
|
.title = Azegzaw
|
||||||
|
pdfjs-editor-colorpicker-blue =
|
||||||
|
.title = Amidadi
|
||||||
|
pdfjs-editor-colorpicker-pink =
|
||||||
|
.title = Axuxi
|
||||||
|
pdfjs-editor-colorpicker-red =
|
||||||
|
.title = Azggaɣ
|
||||||
|
|
||||||
|
## Show all highlights
|
||||||
|
## This is a toggle button to show/hide all the highlights.
|
||||||
|
|
||||||
|
pdfjs-editor-highlight-show-all-button-label = Sken akk
|
||||||
|
pdfjs-editor-highlight-show-all-button =
|
||||||
|
.title = Sken akk
|
||||||
|
@ -51,12 +51,6 @@ pdfjs-download-button-label = Last ned
|
|||||||
pdfjs-bookmark-button =
|
pdfjs-bookmark-button =
|
||||||
.title = Gjeldande side (sjå URL frå gjeldande side)
|
.title = Gjeldande side (sjå URL frå gjeldande side)
|
||||||
pdfjs-bookmark-button-label = Gjeldande side
|
pdfjs-bookmark-button-label = Gjeldande side
|
||||||
# Used in Firefox for Android.
|
|
||||||
pdfjs-open-in-app-button =
|
|
||||||
.title = Opne i app
|
|
||||||
# Used in Firefox for Android.
|
|
||||||
# Length of the translation matters since we are in a mobile context, with limited screen estate.
|
|
||||||
pdfjs-open-in-app-button-label = Opne i app
|
|
||||||
|
|
||||||
## Secondary toolbar and context menu
|
## Secondary toolbar and context menu
|
||||||
|
|
||||||
@ -301,9 +295,24 @@ pdfjs-editor-ink-button-label = Teikne
|
|||||||
pdfjs-editor-stamp-button =
|
pdfjs-editor-stamp-button =
|
||||||
.title = Legg til eller rediger bilde
|
.title = Legg til eller rediger bilde
|
||||||
pdfjs-editor-stamp-button-label = Legg til eller rediger bilde
|
pdfjs-editor-stamp-button-label = Legg til eller rediger bilde
|
||||||
|
pdfjs-editor-highlight-button =
|
||||||
|
.title = Markere
|
||||||
|
pdfjs-editor-highlight-button-label = Markere
|
||||||
|
pdfjs-highlight-floating-button1 =
|
||||||
|
.title = Markere
|
||||||
|
.aria-label = Markere
|
||||||
|
pdfjs-highlight-floating-button-label = Markere
|
||||||
|
|
||||||
## Remove button for the various kind of editor.
|
## Remove button for the various kind of editor.
|
||||||
|
|
||||||
|
pdfjs-editor-remove-ink-button =
|
||||||
|
.title = Fjern teikninga
|
||||||
|
pdfjs-editor-remove-freetext-button =
|
||||||
|
.title = Fjern tekst
|
||||||
|
pdfjs-editor-remove-stamp-button =
|
||||||
|
.title = Fjern bildet
|
||||||
|
pdfjs-editor-remove-highlight-button =
|
||||||
|
.title = Fjern utheving
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
||||||
@ -316,6 +325,10 @@ pdfjs-editor-ink-opacity-input = Ugjennomskinleg
|
|||||||
pdfjs-editor-stamp-add-image-button =
|
pdfjs-editor-stamp-add-image-button =
|
||||||
.title = Legg til bilde
|
.title = Legg til bilde
|
||||||
pdfjs-editor-stamp-add-image-button-label = Legg til bilde
|
pdfjs-editor-stamp-add-image-button-label = Legg til bilde
|
||||||
|
# This refers to the thickness of the line used for free highlighting (not bound to text)
|
||||||
|
pdfjs-editor-free-highlight-thickness-input = Tjukkleik
|
||||||
|
pdfjs-editor-free-highlight-thickness-title =
|
||||||
|
.title = Endre tjukn når du markerer andre element enn tekst
|
||||||
pdfjs-free-text =
|
pdfjs-free-text =
|
||||||
.aria-label = Tekstredigering
|
.aria-label = Tekstredigering
|
||||||
pdfjs-free-text-default-content = Byrje å skrive…
|
pdfjs-free-text-default-content = Byrje å skrive…
|
||||||
@ -345,9 +358,23 @@ pdfjs-editor-alt-text-textarea =
|
|||||||
## Editor resizers
|
## Editor resizers
|
||||||
## This is used in an aria label to help to understand the role of the resizer.
|
## This is used in an aria label to help to understand the role of the resizer.
|
||||||
|
|
||||||
|
pdfjs-editor-resizer-label-top-left = Øvste venstre hjørne – endre størrelse
|
||||||
|
pdfjs-editor-resizer-label-top-middle = Øvst i midten — endre størrelse
|
||||||
|
pdfjs-editor-resizer-label-top-right = Øvste høgre hjørne – endre størrelse
|
||||||
|
pdfjs-editor-resizer-label-middle-right = Midt til høgre – endre størrelse
|
||||||
|
pdfjs-editor-resizer-label-bottom-right = Nedste høgre hjørne – endre størrelse
|
||||||
|
pdfjs-editor-resizer-label-bottom-middle = Nedst i midten — endre størrelse
|
||||||
|
pdfjs-editor-resizer-label-bottom-left = Nedste venstre hjørne – endre størrelse
|
||||||
|
pdfjs-editor-resizer-label-middle-left = Midt til venstre — endre størrelse
|
||||||
|
|
||||||
## Color picker
|
## Color picker
|
||||||
|
|
||||||
|
# This means "Color used to highlight text"
|
||||||
|
pdfjs-editor-highlight-colorpicker-label = Uthevingsfarge
|
||||||
|
pdfjs-editor-colorpicker-button =
|
||||||
|
.title = Endre farge
|
||||||
|
pdfjs-editor-colorpicker-dropdown =
|
||||||
|
.aria-label = Fargeval
|
||||||
pdfjs-editor-colorpicker-yellow =
|
pdfjs-editor-colorpicker-yellow =
|
||||||
.title = Gul
|
.title = Gul
|
||||||
pdfjs-editor-colorpicker-green =
|
pdfjs-editor-colorpicker-green =
|
||||||
@ -358,3 +385,10 @@ pdfjs-editor-colorpicker-pink =
|
|||||||
.title = Rosa
|
.title = Rosa
|
||||||
pdfjs-editor-colorpicker-red =
|
pdfjs-editor-colorpicker-red =
|
||||||
.title = Raud
|
.title = Raud
|
||||||
|
|
||||||
|
## Show all highlights
|
||||||
|
## This is a toggle button to show/hide all the highlights.
|
||||||
|
|
||||||
|
pdfjs-editor-highlight-show-all-button-label = Vis alle
|
||||||
|
pdfjs-editor-highlight-show-all-button =
|
||||||
|
.title = Vis alle
|
||||||
|
@ -302,6 +302,10 @@ pdfjs-editor-stamp-button-label = Dodajanje ali urejanje slik
|
|||||||
pdfjs-editor-highlight-button =
|
pdfjs-editor-highlight-button =
|
||||||
.title = Označevalnik
|
.title = Označevalnik
|
||||||
pdfjs-editor-highlight-button-label = Označevalnik
|
pdfjs-editor-highlight-button-label = Označevalnik
|
||||||
|
pdfjs-highlight-floating-button1 =
|
||||||
|
.title = Označi
|
||||||
|
.aria-label = Označi
|
||||||
|
pdfjs-highlight-floating-button-label = Označi
|
||||||
|
|
||||||
## Remove button for the various kind of editor.
|
## Remove button for the various kind of editor.
|
||||||
|
|
||||||
|
@ -16,31 +16,48 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from urllib.request import urlopen
|
import datetime
|
||||||
|
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
|
from sqlalchemy.sql.expression import or_
|
||||||
|
|
||||||
from cps import logger, file_helper
|
from cps import logger, file_helper, ub
|
||||||
from cps.services.worker import CalibreTask
|
from cps.services.worker import CalibreTask
|
||||||
|
|
||||||
|
|
||||||
class TaskDeleteTempFolder(CalibreTask):
|
class TaskClean(CalibreTask):
|
||||||
def __init__(self, task_message=N_('Delete temp folder contents')):
|
def __init__(self, task_message=N_('Delete temp folder contents')):
|
||||||
super(TaskDeleteTempFolder, self).__init__(task_message)
|
super(TaskClean, self).__init__(task_message)
|
||||||
self.log = logger.create()
|
self.log = logger.create()
|
||||||
|
self.app_db_session = ub.get_new_session_instance()
|
||||||
|
|
||||||
def run(self, worker_thread):
|
def run(self, worker_thread):
|
||||||
|
# delete temp folder
|
||||||
try:
|
try:
|
||||||
file_helper.del_temp_dir()
|
file_helper.del_temp_dir()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
except (PermissionError, OSError) as e:
|
except (PermissionError, OSError) as e:
|
||||||
self.log.error("Error deleting temp folder: {}".format(e))
|
self.log.error("Error deleting temp folder: {}".format(e))
|
||||||
|
# delete expired session keys
|
||||||
|
self.log.debug("Deleted expired session_keys" )
|
||||||
|
expiry = int(datetime.datetime.now().timestamp())
|
||||||
|
try:
|
||||||
|
self.app_db_session.query(ub.User_Sessions).filter(or_(ub.User_Sessions.expiry < expiry,
|
||||||
|
ub.User_Sessions.expiry == None)).delete()
|
||||||
|
self.app_db_session.commit()
|
||||||
|
except Exception as ex:
|
||||||
|
self.log.debug('Error deleting expired session keys: ' + str(ex))
|
||||||
|
self._handleError('Error deleting expired session keys: ' + str(ex))
|
||||||
|
self.app_db_session.rollback()
|
||||||
|
return
|
||||||
|
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
|
self.app_db_session.remove()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return "Delete Temp Folder"
|
return "Clean up"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cancellable(self):
|
def is_cancellable(self):
|
@ -17,7 +17,7 @@
|
|||||||
from markupsafe import escape
|
from markupsafe import escape
|
||||||
|
|
||||||
from flask import Blueprint, jsonify
|
from flask import Blueprint, jsonify
|
||||||
from flask_login import login_required, current_user
|
from .cw_login import current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import format_datetime
|
from flask_babel import format_datetime
|
||||||
from babel.units import format_unit
|
from babel.units import format_unit
|
||||||
@ -26,6 +26,7 @@ from . import logger
|
|||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, STAT_ENDED, \
|
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, STAT_ENDED, \
|
||||||
STAT_CANCELLED
|
STAT_CANCELLED
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
tasks = Blueprint('tasks', __name__)
|
tasks = Blueprint('tasks', __name__)
|
||||||
|
|
||||||
@ -33,14 +34,14 @@ log = logger.create()
|
|||||||
|
|
||||||
|
|
||||||
@tasks.route("/ajax/emailstat")
|
@tasks.route("/ajax/emailstat")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_email_status_json():
|
def get_email_status_json():
|
||||||
tasks = WorkerThread.get_instance().tasks
|
tasks = WorkerThread.get_instance().tasks
|
||||||
return jsonify(render_task_status(tasks))
|
return jsonify(render_task_status(tasks))
|
||||||
|
|
||||||
|
|
||||||
@tasks.route("/tasks")
|
@tasks.route("/tasks")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_tasks_status():
|
def get_tasks_status():
|
||||||
# if current user admin, show all email, otherwise only own emails
|
# if current user admin, show all email, otherwise only own emails
|
||||||
return render_title_template('tasks.html', title=_("Tasks"), page="tasks")
|
return render_title_template('tasks.html', title=_("Tasks"), page="tasks")
|
||||||
|
242
cps/ub.py
242
cps/ub.py
@ -26,8 +26,8 @@ import uuid
|
|||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
|
|
||||||
from flask_login import AnonymousUserMixin, current_user
|
from .cw_login import AnonymousUserMixin, current_user
|
||||||
from flask_login import user_logged_in
|
from .cw_login import user_logged_in
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
||||||
@ -71,17 +71,19 @@ def signal_store_user_session(object, user):
|
|||||||
|
|
||||||
|
|
||||||
def store_user_session():
|
def store_user_session():
|
||||||
if flask_session.get('user_id', ""):
|
_user = flask_session.get('_user_id', "")
|
||||||
flask_session['_user_id'] = flask_session.get('user_id', "")
|
_id = flask_session.get('_id', "")
|
||||||
|
_random = flask_session.get('_random', "")
|
||||||
if flask_session.get('_user_id', ""):
|
if flask_session.get('_user_id', ""):
|
||||||
try:
|
try:
|
||||||
if not check_user_session(flask_session.get('_user_id', ""), flask_session.get('_id', "")):
|
if not check_user_session(_user, _id, _random):
|
||||||
user_session = User_Sessions(flask_session.get('_user_id', ""), flask_session.get('_id', ""))
|
expiry = int((datetime.datetime.now() + datetime.timedelta(days=31)).timestamp())
|
||||||
|
user_session = User_Sessions(_user, _id, _random, expiry)
|
||||||
session.add(user_session)
|
session.add(user_session)
|
||||||
session.commit()
|
session.commit()
|
||||||
log.debug("Login and store session : " + flask_session.get('_id', ""))
|
log.debug("Login and store session : " + _id)
|
||||||
else:
|
else:
|
||||||
log.debug("Found stored session: " + flask_session.get('_id', ""))
|
log.debug("Found stored session: " + _id)
|
||||||
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@ -100,13 +102,23 @@ def delete_user_session(user_id, session_key):
|
|||||||
log.exception(ex)
|
log.exception(ex)
|
||||||
|
|
||||||
|
|
||||||
def check_user_session(user_id, session_key):
|
def check_user_session(user_id, session_key, random):
|
||||||
try:
|
try:
|
||||||
return bool(session.query(User_Sessions).filter(User_Sessions.user_id==user_id,
|
found = session.query(User_Sessions).filter(User_Sessions.user_id==user_id,
|
||||||
User_Sessions.session_key==session_key).one_or_none())
|
User_Sessions.session_key==session_key,
|
||||||
|
User_Sessions.random == random,
|
||||||
|
).one_or_none()
|
||||||
|
if found is not None:
|
||||||
|
new_expiry = int((datetime.datetime.now() + datetime.timedelta(days=31)).timestamp())
|
||||||
|
if new_expiry - found.expiry > 86400:
|
||||||
|
found.expiry = new_expiry
|
||||||
|
session.merge(found)
|
||||||
|
session.commit()
|
||||||
|
return bool(found)
|
||||||
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
user_logged_in.connect(signal_store_user_session)
|
user_logged_in.connect(signal_store_user_session)
|
||||||
@ -335,11 +347,16 @@ class User_Sessions(Base):
|
|||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
user_id = Column(Integer, ForeignKey('user.id'))
|
user_id = Column(Integer, ForeignKey('user.id'))
|
||||||
session_key = Column(String, default="")
|
session_key = Column(String, default="")
|
||||||
|
random = Column(String, default="")
|
||||||
|
expiry = Column(Integer)
|
||||||
|
|
||||||
def __init__(self, user_id, session_key):
|
|
||||||
|
def __init__(self, user_id, session_key, random, expiry):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.session_key = session_key
|
self.session_key = session_key
|
||||||
|
self.random = random
|
||||||
|
self.expiry = expiry
|
||||||
|
|
||||||
|
|
||||||
# Baseclass representing Shelfs in calibre-web in app.db
|
# Baseclass representing Shelfs in calibre-web in app.db
|
||||||
@ -552,39 +569,14 @@ class Thumbnail(Base):
|
|||||||
|
|
||||||
# Add missing tables during migration of database
|
# Add missing tables during migration of database
|
||||||
def add_missing_tables(engine, _session):
|
def add_missing_tables(engine, _session):
|
||||||
if not engine.dialect.has_table(engine.connect(), "book_read_link"):
|
|
||||||
ReadBook.__table__.create(bind=engine)
|
|
||||||
if not engine.dialect.has_table(engine.connect(), "bookmark"):
|
|
||||||
Bookmark.__table__.create(bind=engine)
|
|
||||||
if not engine.dialect.has_table(engine.connect(), "kobo_reading_state"):
|
|
||||||
KoboReadingState.__table__.create(bind=engine)
|
|
||||||
if not engine.dialect.has_table(engine.connect(), "kobo_bookmark"):
|
|
||||||
KoboBookmark.__table__.create(bind=engine)
|
|
||||||
if not engine.dialect.has_table(engine.connect(), "kobo_statistics"):
|
|
||||||
KoboStatistics.__table__.create(bind=engine)
|
|
||||||
if not engine.dialect.has_table(engine.connect(), "archived_book"):
|
if not engine.dialect.has_table(engine.connect(), "archived_book"):
|
||||||
ArchivedBook.__table__.create(bind=engine)
|
ArchivedBook.__table__.create(bind=engine)
|
||||||
if not engine.dialect.has_table(engine.connect(), "thumbnail"):
|
if not engine.dialect.has_table(engine.connect(), "thumbnail"):
|
||||||
Thumbnail.__table__.create(bind=engine)
|
Thumbnail.__table__.create(bind=engine)
|
||||||
if not engine.dialect.has_table(engine.connect(), "registration"):
|
|
||||||
Registration.__table__.create(bind=engine)
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
|
||||||
trans.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# migrate all settings missing in registration table
|
# migrate all settings missing in registration table
|
||||||
def migrate_registration_table(engine, _session):
|
def migrate_registration_table(engine, _session):
|
||||||
try:
|
|
||||||
_session.query(exists().where(Registration.allow)).scalar()
|
|
||||||
_session.commit()
|
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE registration ADD column 'allow' INTEGER"))
|
|
||||||
conn.execute(text("update registration set 'allow' = 1"))
|
|
||||||
trans.commit()
|
|
||||||
try:
|
try:
|
||||||
# Handle table exists, but no content
|
# Handle table exists, but no content
|
||||||
cnt = _session.query(Registration).count()
|
cnt = _session.query(Registration).count()
|
||||||
@ -598,190 +590,38 @@ def migrate_registration_table(engine, _session):
|
|||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
# Remove login capability of user Guest
|
def migrate_user_session_table(engine, _session):
|
||||||
def migrate_guest_password(engine):
|
|
||||||
try:
|
try:
|
||||||
with engine.connect() as conn:
|
_session.query(exists().where(User_Sessions.random)).scalar()
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("UPDATE user SET password='' where name = 'Guest' and password !=''"))
|
|
||||||
trans.commit()
|
|
||||||
except exc.OperationalError:
|
|
||||||
print('Settings database is not writeable. Exiting...')
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_shelfs(engine, _session):
|
|
||||||
try:
|
|
||||||
_session.query(exists().where(Shelf.uuid)).scalar()
|
|
||||||
except exc.OperationalError:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'uuid' STRING"))
|
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'created' DATETIME"))
|
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'last_modified' DATETIME"))
|
|
||||||
conn.execute(text("ALTER TABLE book_shelf_link ADD column 'date_added' DATETIME"))
|
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false"))
|
|
||||||
trans.commit()
|
|
||||||
for shelf in _session.query(Shelf).all():
|
|
||||||
shelf.uuid = str(uuid.uuid4())
|
|
||||||
shelf.created = datetime.datetime.now()
|
|
||||||
shelf.last_modified = datetime.datetime.now()
|
|
||||||
for book_shelf in _session.query(BookShelf).all():
|
|
||||||
book_shelf.date_added = datetime.datetime.now()
|
|
||||||
_session.commit()
|
|
||||||
|
|
||||||
try:
|
|
||||||
_session.query(exists().where(Shelf.kobo_sync)).scalar()
|
|
||||||
except exc.OperationalError:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false"))
|
|
||||||
trans.commit()
|
|
||||||
try:
|
|
||||||
_session.query(exists().where(BookShelf.order)).scalar()
|
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1"))
|
|
||||||
trans.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_readBook(engine, _session):
|
|
||||||
try:
|
|
||||||
_session.query(exists().where(ReadBook.read_status)).scalar()
|
|
||||||
except exc.OperationalError:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0"))
|
|
||||||
conn.execute(text("UPDATE book_read_link SET 'read_status' = 1 WHERE is_read"))
|
|
||||||
conn.execute(text("ALTER TABLE book_read_link ADD column 'last_modified' DATETIME"))
|
|
||||||
conn.execute(text("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME"))
|
|
||||||
conn.execute(text("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0"))
|
|
||||||
trans.commit()
|
|
||||||
test = _session.query(ReadBook).filter(ReadBook.last_modified == None).all()
|
|
||||||
for book in test:
|
|
||||||
book.last_modified = datetime.datetime.utcnow()
|
|
||||||
_session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_remoteAuthToken(engine, _session):
|
|
||||||
try:
|
|
||||||
_session.query(exists().where(RemoteAuthToken.token_type)).scalar()
|
|
||||||
_session.commit()
|
_session.commit()
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
trans = conn.begin()
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0"))
|
conn.execute(text("ALTER TABLE user_session ADD column 'random' String"))
|
||||||
conn.execute(text("update remote_auth_token set 'token_type' = 0"))
|
conn.execute(text("ALTER TABLE user_session ADD column 'expiry' Integer"))
|
||||||
trans.commit()
|
trans.commit()
|
||||||
|
|
||||||
|
|
||||||
# Migrate database to current version, has to be updated after every database change. Currently migration from
|
# Migrate database to current version, has to be updated after every database change. Currently migration from
|
||||||
# everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding
|
# maybe 4/5 versions back to current should work.
|
||||||
# rows with SQL commands
|
# Migration is done by checking if relevant columns are existing, and then adding rows with SQL commands
|
||||||
def migrate_Database(_session):
|
def migrate_Database(_session):
|
||||||
engine = _session.bind
|
engine = _session.bind
|
||||||
add_missing_tables(engine, _session)
|
add_missing_tables(engine, _session)
|
||||||
migrate_registration_table(engine, _session)
|
migrate_registration_table(engine, _session)
|
||||||
migrate_readBook(engine, _session)
|
migrate_user_session_table(engine, _session)
|
||||||
migrate_remoteAuthToken(engine, _session)
|
|
||||||
migrate_shelfs(engine, _session)
|
|
||||||
try:
|
|
||||||
create = False
|
|
||||||
_session.query(exists().where(User.sidebar_view)).scalar()
|
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1"))
|
|
||||||
trans.commit()
|
|
||||||
create = True
|
|
||||||
try:
|
|
||||||
if create:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("SELECT language_books FROM user"))
|
|
||||||
trans.commit()
|
|
||||||
except exc.OperationalError:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("UPDATE user SET 'sidebar_view' = (random_books* :side_random + language_books * :side_lang "
|
|
||||||
"+ series_books * :side_series + category_books * :side_category + hot_books * "
|
|
||||||
":side_hot + :side_autor + :detail_random)"),
|
|
||||||
{'side_random': constants.SIDEBAR_RANDOM, 'side_lang': constants.SIDEBAR_LANGUAGE,
|
|
||||||
'side_series': constants.SIDEBAR_SERIES, 'side_category': constants.SIDEBAR_CATEGORY,
|
|
||||||
'side_hot': constants.SIDEBAR_HOT, 'side_autor': constants.SIDEBAR_AUTHOR,
|
|
||||||
'detail_random': constants.DETAIL_RANDOM})
|
|
||||||
trans.commit()
|
|
||||||
try:
|
|
||||||
_session.query(exists().where(User.denied_tags)).scalar()
|
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE user ADD column `denied_tags` String DEFAULT ''"))
|
|
||||||
conn.execute(text("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''"))
|
|
||||||
conn.execute(text("ALTER TABLE user ADD column `denied_column_value` String DEFAULT ''"))
|
|
||||||
conn.execute(text("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''"))
|
|
||||||
trans.commit()
|
|
||||||
try:
|
|
||||||
_session.query(exists().where(User.view_settings)).scalar()
|
|
||||||
except exc.OperationalError:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'"))
|
|
||||||
trans.commit()
|
|
||||||
try:
|
|
||||||
_session.query(exists().where(User.kobo_only_shelves_sync)).scalar()
|
|
||||||
except exc.OperationalError:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("ALTER TABLE user ADD column `kobo_only_shelves_sync` SMALLINT DEFAULT 0"))
|
|
||||||
trans.commit()
|
|
||||||
try:
|
|
||||||
# check if name is in User table instead of nickname
|
|
||||||
_session.query(exists().where(User.name)).scalar()
|
|
||||||
except exc.OperationalError:
|
|
||||||
# Create new table user_id and copy contents of table user into it
|
|
||||||
with engine.connect() as conn:
|
|
||||||
trans = conn.begin()
|
|
||||||
conn.execute(text("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
|
|
||||||
"name VARCHAR(64),"
|
|
||||||
"email VARCHAR(120),"
|
|
||||||
"role SMALLINT,"
|
|
||||||
"password VARCHAR,"
|
|
||||||
"kindle_mail VARCHAR(120),"
|
|
||||||
"locale VARCHAR(2),"
|
|
||||||
"sidebar_view INTEGER,"
|
|
||||||
"default_language VARCHAR(3),"
|
|
||||||
"denied_tags VARCHAR,"
|
|
||||||
"allowed_tags VARCHAR,"
|
|
||||||
"denied_column_value VARCHAR,"
|
|
||||||
"allowed_column_value VARCHAR,"
|
|
||||||
"view_settings JSON,"
|
|
||||||
"kobo_only_shelves_sync SMALLINT,"
|
|
||||||
"UNIQUE (name),"
|
|
||||||
"UNIQUE (email))"))
|
|
||||||
conn.execute(text("INSERT INTO user_id(id, name, email, role, password, kindle_mail,locale,"
|
|
||||||
"sidebar_view, default_language, denied_tags, allowed_tags, denied_column_value, "
|
|
||||||
"allowed_column_value, view_settings, kobo_only_shelves_sync)"
|
|
||||||
"SELECT id, nickname, email, role, password, kindle_mail, locale,"
|
|
||||||
"sidebar_view, default_language, denied_tags, allowed_tags, denied_column_value, "
|
|
||||||
"allowed_column_value, view_settings, kobo_only_shelves_sync FROM user"))
|
|
||||||
# delete old user table and rename new user_id table to user:
|
|
||||||
conn.execute(text("DROP TABLE user"))
|
|
||||||
conn.execute(text("ALTER TABLE user_id RENAME TO user"))
|
|
||||||
trans.commit()
|
|
||||||
if _session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
|
|
||||||
is None:
|
|
||||||
create_anonymous_user(_session)
|
|
||||||
|
|
||||||
migrate_guest_password(engine)
|
|
||||||
|
|
||||||
|
|
||||||
def clean_database(_session):
|
def clean_database(_session):
|
||||||
# Remove expired remote login tokens
|
# Remove expired remote login tokens
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
try:
|
||||||
_session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\
|
_session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\
|
||||||
filter(RemoteAuthToken.token_type != 1).delete()
|
filter(RemoteAuthToken.token_type != 1).delete()
|
||||||
_session.commit()
|
_session.commit()
|
||||||
|
except exc.OperationalError: # Database is not writeable
|
||||||
|
print('Settings database is not writeable. Exiting...')
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
# Save downloaded books per user in calibre-web's own database
|
# Save downloaded books per user in calibre-web's own database
|
||||||
|
@ -19,93 +19,123 @@
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func
|
||||||
from werkzeug.security import check_password_hash
|
from .cw_login import login_required
|
||||||
from flask_login import login_required, login_user
|
|
||||||
from flask import request, Response
|
from flask import request, g
|
||||||
|
from flask_httpauth import HTTPBasicAuth
|
||||||
|
from werkzeug.datastructures import Authorization
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
|
from . import lm, ub, config, logger, limiter, constants, services
|
||||||
|
|
||||||
from . import lm, ub, config, constants, services, logger, limiter
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
auth = HTTPBasicAuth()
|
||||||
|
|
||||||
|
|
||||||
|
@auth.verify_password
|
||||||
|
def verify_password(username, password):
|
||||||
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
|
||||||
|
if user:
|
||||||
|
if user.name.lower() == "guest":
|
||||||
|
if config.config_anonbrowse == 1:
|
||||||
|
return user
|
||||||
|
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
||||||
|
login_result, error = services.ldap.bind_user(user.name, password)
|
||||||
|
if login_result:
|
||||||
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
|
return user
|
||||||
|
if error is not None:
|
||||||
|
log.error(error)
|
||||||
|
else:
|
||||||
|
limiter.check()
|
||||||
|
if check_password_hash(str(user.password), password):
|
||||||
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
|
return user
|
||||||
|
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||||
|
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def requires_basic_auth_if_no_ano(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
authorisation = auth.get_auth()
|
||||||
|
status = None
|
||||||
|
user = None
|
||||||
|
if config.config_allow_reverse_proxy_header_login and not authorisation:
|
||||||
|
user = load_user_from_reverse_proxy_header(request)
|
||||||
|
if config.config_anonbrowse == 1 and not authorisation:
|
||||||
|
authorisation = Authorization(
|
||||||
|
b"Basic", {'username': "Guest", 'password': ""})
|
||||||
|
if not user:
|
||||||
|
user = auth.authenticate(authorisation, "")
|
||||||
|
if user in (False, None):
|
||||||
|
status = 401
|
||||||
|
if status:
|
||||||
|
try:
|
||||||
|
return auth.auth_error_callback(status)
|
||||||
|
except TypeError:
|
||||||
|
return auth.auth_error_callback()
|
||||||
|
g.flask_httpauth_user = user if user is not True \
|
||||||
|
else auth.username if auth else None
|
||||||
|
return auth.ensure_sync(f)(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def login_required_if_no_ano(func):
|
def login_required_if_no_ano(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args, **kwargs):
|
||||||
|
if config.config_allow_reverse_proxy_header_login:
|
||||||
|
user = load_user_from_reverse_proxy_header(request)
|
||||||
|
if user:
|
||||||
|
g.flask_httpauth_user = user
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
g.flask_httpauth_user = None
|
||||||
if config.config_anonbrowse == 1:
|
if config.config_anonbrowse == 1:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return login_required(func)(*args, **kwargs)
|
return login_required(func)(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_view
|
return decorated_view
|
||||||
|
|
||||||
def requires_basic_auth_if_no_ano(f):
|
|
||||||
@wraps(f)
|
def user_login_required(func):
|
||||||
def decorated(*args, **kwargs):
|
@wraps(func)
|
||||||
auth = request.authorization
|
def decorated_view(*args, **kwargs):
|
||||||
if not auth or auth.type != 'basic':
|
if config.config_allow_reverse_proxy_header_login:
|
||||||
if config.config_anonbrowse != 1:
|
|
||||||
user = load_user_from_reverse_proxy_header(request)
|
user = load_user_from_reverse_proxy_header(request)
|
||||||
if user:
|
if user:
|
||||||
return f(*args, **kwargs)
|
g.flask_httpauth_user = user
|
||||||
return _authenticate()
|
return func(*args, **kwargs)
|
||||||
else:
|
g.flask_httpauth_user = None
|
||||||
return f(*args, **kwargs)
|
return login_required(func)(*args, **kwargs)
|
||||||
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
|
||||||
login_result, error = services.ldap.bind_user(auth.username, auth.password)
|
return decorated_view
|
||||||
if login_result:
|
|
||||||
user = _fetch_user_by_name(auth.username)
|
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
|
||||||
login_user(user)
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
elif login_result is not None:
|
|
||||||
log.error(error)
|
|
||||||
return _authenticate()
|
|
||||||
user = _load_user_from_auth_header(auth.username, auth.password)
|
|
||||||
if not user:
|
|
||||||
return _authenticate()
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
|
|
||||||
def _load_user_from_auth_header(username, password):
|
|
||||||
limiter.check()
|
|
||||||
user = _fetch_user_by_name(username)
|
|
||||||
if bool(user and check_password_hash(str(user.password), password)) and user.name != "Guest":
|
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
|
||||||
login_user(user)
|
|
||||||
return user
|
|
||||||
else:
|
|
||||||
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
|
||||||
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _authenticate():
|
|
||||||
return Response(
|
|
||||||
'Could not verify your access level for that URL.\n'
|
|
||||||
'You have to login with proper credentials', 401,
|
|
||||||
{'WWW-Authenticate': 'Basic realm="Login Required"'})
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_user_by_name(username):
|
|
||||||
return ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
|
|
||||||
|
|
||||||
|
|
||||||
@lm.user_loader
|
|
||||||
def load_user(user_id):
|
|
||||||
user = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
@lm.request_loader
|
|
||||||
def load_user_from_reverse_proxy_header(req):
|
def load_user_from_reverse_proxy_header(req):
|
||||||
if config.config_allow_reverse_proxy_header_login:
|
|
||||||
rp_header_name = config.config_reverse_proxy_login_header_name
|
rp_header_name = config.config_reverse_proxy_login_header_name
|
||||||
if rp_header_name:
|
if rp_header_name:
|
||||||
rp_header_username = req.headers.get(rp_header_name)
|
rp_header_username = req.headers.get(rp_header_name)
|
||||||
if rp_header_username:
|
if rp_header_username:
|
||||||
user = _fetch_user_by_name(rp_header_username)
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first()
|
||||||
if user:
|
if user:
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
login_user(user)
|
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@lm.user_loader
|
||||||
|
def load_user(user_id, random, session_key):
|
||||||
|
user = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
|
if session_key:
|
||||||
|
entry = ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.random == random,
|
||||||
|
ub.User_Sessions.session_key == session_key).first()
|
||||||
|
if not entry or entry.user_id != user.id:
|
||||||
|
return None
|
||||||
|
elif random:
|
||||||
|
entry = ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.random == random).first()
|
||||||
|
if not entry or entry.user_id != user.id:
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
24
cps/web.py
24
cps/web.py
@ -29,7 +29,7 @@ from flask import request, redirect, send_from_directory, make_response, flash,
|
|||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import login_user, logout_user, login_required, current_user
|
from .cw_login import login_user, logout_user, current_user
|
||||||
from flask_limiter import RateLimitExceeded
|
from flask_limiter import RateLimitExceeded
|
||||||
from flask_limiter.util import get_remote_address
|
from flask_limiter.util import get_remote_address
|
||||||
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
||||||
@ -59,6 +59,7 @@ from .kobo_sync_status import change_archived_books
|
|||||||
from . import limiter
|
from . import limiter
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks_status import render_task_status
|
from .tasks_status import render_task_status
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
|
|
||||||
feature_support = {
|
feature_support = {
|
||||||
@ -143,14 +144,14 @@ def viewer_required(f):
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/emailstat")
|
@web.route("/ajax/emailstat")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_email_status_json():
|
def get_email_status_json():
|
||||||
tasks = WorkerThread.get_instance().tasks
|
tasks = WorkerThread.get_instance().tasks
|
||||||
return jsonify(render_task_status(tasks))
|
return jsonify(render_task_status(tasks))
|
||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/bookmark/<int:book_id>/<book_format>", methods=['POST'])
|
@web.route("/ajax/bookmark/<int:book_id>/<book_format>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def set_bookmark(book_id, book_format):
|
def set_bookmark(book_id, book_format):
|
||||||
bookmark_key = request.form["bookmark"]
|
bookmark_key = request.form["bookmark"]
|
||||||
ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id),
|
ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id),
|
||||||
@ -170,7 +171,7 @@ def set_bookmark(book_id, book_format):
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/toggleread/<int:book_id>", methods=['POST'])
|
@web.route("/ajax/toggleread/<int:book_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def toggle_read(book_id):
|
def toggle_read(book_id):
|
||||||
message = edit_book_read_status(book_id)
|
message = edit_book_read_status(book_id)
|
||||||
if message:
|
if message:
|
||||||
@ -180,7 +181,7 @@ def toggle_read(book_id):
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
|
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def toggle_archived(book_id):
|
def toggle_archived(book_id):
|
||||||
is_archived = change_archived_books(book_id, message="Book {} archive bit toggled".format(book_id))
|
is_archived = change_archived_books(book_id, message="Book {} archive bit toggled".format(book_id))
|
||||||
if is_archived:
|
if is_archived:
|
||||||
@ -204,7 +205,7 @@ def update_view():
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
@web.route("/ajax/getcomic/<int:book_id>/<book_format>/<int:page>")
|
@web.route("/ajax/getcomic/<int:book_id>/<book_format>/<int:page>")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_comic_book(book_id, book_format, page):
|
def get_comic_book(book_id, book_format, page):
|
||||||
book = calibre_db.get_book(book_id)
|
book = calibre_db.get_book(book_id)
|
||||||
if not book:
|
if not book:
|
||||||
@ -816,7 +817,7 @@ def books_list(data, sort_param, book_id, page):
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/table")
|
@web.route("/table")
|
||||||
@login_required
|
@user_login_required
|
||||||
def books_table():
|
def books_table():
|
||||||
visibility = current_user.view_settings.get('table', {})
|
visibility = current_user.view_settings.get('table', {})
|
||||||
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
@ -825,7 +826,7 @@ def books_table():
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/listbooks")
|
@web.route("/ajax/listbooks")
|
||||||
@login_required
|
@user_login_required
|
||||||
def list_books():
|
def list_books():
|
||||||
off = int(request.args.get("offset") or 0)
|
off = int(request.args.get("offset") or 0)
|
||||||
limit = int(request.args.get("limit") or config.config_books_per_page)
|
limit = int(request.args.get("limit") or config.config_books_per_page)
|
||||||
@ -906,7 +907,7 @@ def list_books():
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/table_settings", methods=['POST'])
|
@web.route("/ajax/table_settings", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def update_table_settings():
|
def update_table_settings():
|
||||||
current_user.view_settings['table'] = json.loads(request.data)
|
current_user.view_settings['table'] = json.loads(request.data)
|
||||||
try:
|
try:
|
||||||
@ -1339,7 +1340,6 @@ def register():
|
|||||||
|
|
||||||
def handle_login_user(user, remember, message, category):
|
def handle_login_user(user, remember, message, category):
|
||||||
login_user(user, remember=remember)
|
login_user(user, remember=remember)
|
||||||
ub.store_user_session()
|
|
||||||
flash(message, category=category)
|
flash(message, category=category)
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
|
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
|
||||||
@ -1443,7 +1443,7 @@ def login_post():
|
|||||||
|
|
||||||
|
|
||||||
@web.route('/logout')
|
@web.route('/logout')
|
||||||
@login_required
|
@user_login_required
|
||||||
def logout():
|
def logout():
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
ub.delete_user_session(current_user.id, flask_session.get('_id', ""))
|
ub.delete_user_session(current_user.id, flask_session.get('_id', ""))
|
||||||
@ -1528,7 +1528,7 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/me", methods=["GET", "POST"])
|
@web.route("/me", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def profile():
|
def profile():
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
translations = get_available_locale()
|
translations = get_available_locale()
|
||||||
|
@ -2,7 +2,7 @@ Werkzeug<3.0.0
|
|||||||
APScheduler>=3.6.3,<3.11.0
|
APScheduler>=3.6.3,<3.11.0
|
||||||
Babel>=1.3,<3.0
|
Babel>=1.3,<3.0
|
||||||
Flask-Babel>=0.11.1,<4.1.0
|
Flask-Babel>=0.11.1,<4.1.0
|
||||||
Flask-Login>=0.3.2,<0.6.4
|
# Flask-Login>=0.3.2,<0.6.4
|
||||||
Flask-Principal>=0.3.2,<0.5.1
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
Flask>=1.0.2,<3.1.0
|
Flask>=1.0.2,<3.1.0
|
||||||
iso-639>=0.4.5,<0.5.0
|
iso-639>=0.4.5,<0.5.0
|
||||||
@ -21,3 +21,4 @@ Flask-Limiter>=2.3.0,<3.6.0
|
|||||||
regex>=2022.3.2,<2024.6.25
|
regex>=2022.3.2,<2024.6.25
|
||||||
bleach>=6.0.0,<6.2.0
|
bleach>=6.0.0,<6.2.0
|
||||||
python-magic>=0.4.27,<0.5.0
|
python-magic>=0.4.27,<0.5.0
|
||||||
|
flask-httpAuth>=4.4.0
|
||||||
|
Loading…
Reference in New Issue
Block a user