mirror of
https://github.com/janeczku/calibre-web
synced 2024-12-27 02:20:31 +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
cps
MyLoginManager.py__init__.pyabout.pyadmin.pybabel.py
requirements.txtcw_login
db.pyeditbooks.pygdrive.pyhelper.pyjinjia.pykobo.pykobo_auth.pykobo_sync_status.pyoauth_bb.pyopds.pyremotelogin.pyrender_template.pyschedule.pysearch.pysearch_metadata.pyshelf.pystatic
css/libs
js/libs
locale
tasks
tasks_status.pyub.pyusermanagement.pyweb.py@ -19,12 +19,12 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import 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):
|
||||
@ -36,18 +36,5 @@ class MyLoginManager(LoginManager):
|
||||
return super(). _session_protection_failed()
|
||||
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.config.update(
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
SESSION_COOKIE_SAMESITE='Lax',
|
||||
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
||||
SESSION_COOKIE_SAMESITE='Strict',
|
||||
REMEMBER_COOKIE_SAMESITE='Strict', # will be available in flask-login 0.5.1 earliest
|
||||
WTF_CSRF_SSL_STRICT=False
|
||||
)
|
||||
|
||||
|
@ -26,12 +26,12 @@ import sqlite3
|
||||
from collections import OrderedDict
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
import jinja2
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from . import db, calibre_db, converter, uploader, constants, dep_check
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import user_login_required
|
||||
|
||||
|
||||
about = flask.Blueprint('about', __name__)
|
||||
@ -74,7 +74,7 @@ def collect_stats():
|
||||
|
||||
|
||||
@about.route("/stats")
|
||||
@flask_login.login_required
|
||||
@user_login_required
|
||||
def stats():
|
||||
counter = calibre_db.session.query(db.Books).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 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 get_locale, format_time, format_datetime, format_timedelta
|
||||
from flask import session as flask_session
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
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 .render_template import render_title_template, get_sidebar_config
|
||||
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 . import debug_info
|
||||
|
||||
@ -103,13 +103,13 @@ def admin_required(f):
|
||||
|
||||
@admi.before_app_request
|
||||
def before_request():
|
||||
try:
|
||||
if not ub.check_user_session(current_user.id,
|
||||
flask_session.get('_id')) and 'opds' not in request.path \
|
||||
and config.config_session == 1:
|
||||
logout_user()
|
||||
except AttributeError:
|
||||
pass # ? fails on requesting /ajax/emailstat during restart ?
|
||||
#try:
|
||||
#if not ub.check_user_session(current_user.id,
|
||||
# flask_session.get('_id')) and 'opds' not in request.path \
|
||||
# and config.config_session == 1:
|
||||
# logout_user()
|
||||
#except AttributeError:
|
||||
# pass # ? fails on requesting /ajax/emailstat during restart ?
|
||||
g.constants = constants
|
||||
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION', '')
|
||||
g.allow_registration = config.config_public_reg
|
||||
@ -129,14 +129,14 @@ def before_request():
|
||||
return redirect(url_for('admin.db_configuration'))
|
||||
|
||||
|
||||
@admi.route("/admin")
|
||||
@login_required
|
||||
def admin_forbidden():
|
||||
abort(403)
|
||||
#@admi.route("/admin")
|
||||
#@user_login_required
|
||||
#def admin_forbidden():
|
||||
# abort(403)
|
||||
|
||||
|
||||
@admi.route("/shutdown", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def shutdown():
|
||||
task = request.get_json().get('parameter', -1)
|
||||
@ -165,7 +165,7 @@ def shutdown():
|
||||
|
||||
|
||||
@admi.route("/metadata_backup", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def queue_metadata_backup():
|
||||
show_text = {}
|
||||
@ -189,7 +189,7 @@ def reconnect():
|
||||
|
||||
@admi.route("/ajax/updateThumbnails", methods=['POST'])
|
||||
@admin_required
|
||||
@login_required
|
||||
@user_login_required
|
||||
def update_thumbnails():
|
||||
content = config.get_scheduled_task_settings()
|
||||
if content['schedule_generate_book_covers']:
|
||||
@ -199,7 +199,7 @@ def update_thumbnails():
|
||||
|
||||
|
||||
@admi.route("/admin/view")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def admin():
|
||||
version = updater_thread.get_current_version_info()
|
||||
@ -233,7 +233,7 @@ def admin():
|
||||
|
||||
|
||||
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def db_configuration():
|
||||
if request.method == "POST":
|
||||
@ -242,7 +242,7 @@ def db_configuration():
|
||||
|
||||
|
||||
@admi.route("/admin/config", methods=["GET"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def configuration():
|
||||
return render_title_template("config_edit.html",
|
||||
@ -253,28 +253,28 @@ def configuration():
|
||||
|
||||
|
||||
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def ajax_config():
|
||||
return _configuration_update_helper()
|
||||
|
||||
|
||||
@admi.route("/admin/ajaxdbconfig", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def ajax_db_config():
|
||||
return _db_configuration_update_helper()
|
||||
|
||||
|
||||
@admi.route("/admin/alive", methods=["GET"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def calibreweb_alive():
|
||||
return "", 200
|
||||
|
||||
|
||||
@admi.route("/admin/viewconfig")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def view_configuration():
|
||||
read_column = calibre_db.session.query(db.CustomColumns) \
|
||||
@ -291,7 +291,7 @@ def view_configuration():
|
||||
|
||||
|
||||
@admi.route("/admin/usertable")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def edit_user_table():
|
||||
visibility = current_user.view_settings.get('useredit', {})
|
||||
@ -326,7 +326,7 @@ def edit_user_table():
|
||||
|
||||
|
||||
@admi.route("/ajax/listusers")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def list_users():
|
||||
off = int(request.args.get("offset") or 0)
|
||||
@ -377,7 +377,7 @@ def list_users():
|
||||
|
||||
|
||||
@admi.route("/ajax/deleteuser", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def delete_user():
|
||||
user_ids = request.form.to_dict(flat=False)
|
||||
@ -412,7 +412,7 @@ def delete_user():
|
||||
|
||||
|
||||
@admi.route("/ajax/getlocale")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def table_get_locale():
|
||||
locale = get_available_locale()
|
||||
@ -424,7 +424,7 @@ def table_get_locale():
|
||||
|
||||
|
||||
@admi.route("/ajax/getdefaultlanguage")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def table_get_default_lang():
|
||||
languages = calibre_db.speaking_language()
|
||||
@ -436,7 +436,7 @@ def table_get_default_lang():
|
||||
|
||||
|
||||
@admi.route("/ajax/editlistusers/<param>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def edit_list_user(param):
|
||||
vals = request.form.to_dict(flat=False)
|
||||
@ -541,7 +541,7 @@ def edit_list_user(param):
|
||||
|
||||
|
||||
@admi.route("/ajax/user_table_settings", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def update_table_settings():
|
||||
current_user.view_settings['useredit'] = json.loads(request.data)
|
||||
@ -558,7 +558,7 @@ def update_table_settings():
|
||||
|
||||
|
||||
@admi.route("/admin/viewconfig", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def update_view_configuration():
|
||||
to_save = request.form.to_dict()
|
||||
@ -603,7 +603,7 @@ def update_view_configuration():
|
||||
|
||||
|
||||
@admi.route("/ajax/loaddialogtexts/<element_id>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def load_dialogtexts(element_id):
|
||||
texts = {"header": "", "main": "", "valid": 1}
|
||||
if element_id == "config_delete_kobo_token":
|
||||
@ -639,7 +639,7 @@ def load_dialogtexts(element_id):
|
||||
|
||||
|
||||
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def edit_domain(allow):
|
||||
# POST /post
|
||||
@ -653,7 +653,7 @@ def edit_domain(allow):
|
||||
|
||||
|
||||
@admi.route("/ajax/adddomain/<int:allow>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def add_domain(allow):
|
||||
domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower()
|
||||
@ -667,7 +667,7 @@ def add_domain(allow):
|
||||
|
||||
|
||||
@admi.route("/ajax/deletedomain", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def delete_domain():
|
||||
try:
|
||||
@ -685,7 +685,7 @@ def delete_domain():
|
||||
|
||||
|
||||
@admi.route("/ajax/domainlist/<int:allow>")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def list_domain(allow):
|
||||
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>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def edit_restriction(res_type, user_id):
|
||||
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'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def add_user_0_restriction(res_type):
|
||||
return add_restriction(res_type, 0)
|
||||
|
||||
|
||||
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def add_restriction(res_type, user_id):
|
||||
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'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def delete_user_0_restriction(res_type):
|
||||
return delete_restriction(res_type, 0)
|
||||
|
||||
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def delete_restriction(res_type, user_id):
|
||||
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>/<int:user_id>")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def list_restriction(res_type, user_id):
|
||||
if res_type == 0: # Tags as template
|
||||
@ -916,20 +916,20 @@ def list_restriction(res_type, user_id):
|
||||
|
||||
|
||||
@admi.route("/ajax/fullsync", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def ajax_self_fullsync():
|
||||
return do_full_kobo_sync(current_user.id)
|
||||
|
||||
|
||||
@admi.route("/ajax/fullsync/<int:userid>", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def ajax_fullsync(userid):
|
||||
return do_full_kobo_sync(userid)
|
||||
|
||||
|
||||
@admi.route("/ajax/pathchooser/")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def ajax_pathchooser():
|
||||
return pathchooser()
|
||||
@ -1246,7 +1246,7 @@ def _configuration_ldap_helper(to_save):
|
||||
|
||||
|
||||
@admi.route("/ajax/simulatedbchange", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def simulatedbchange():
|
||||
db_change, db_valid = _db_simulate_change()
|
||||
@ -1254,7 +1254,7 @@ def simulatedbchange():
|
||||
|
||||
|
||||
@admi.route("/admin/user/new", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def new_user():
|
||||
content = ub.User()
|
||||
@ -1276,7 +1276,7 @@ def new_user():
|
||||
|
||||
|
||||
@admi.route("/admin/mailsettings", methods=["GET"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def edit_mailsettings():
|
||||
content = config.get_mail_settings()
|
||||
@ -1285,7 +1285,7 @@ def edit_mailsettings():
|
||||
|
||||
|
||||
@admi.route("/admin/mailsettings", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def update_mailsettings():
|
||||
to_save = request.form.to_dict()
|
||||
@ -1342,7 +1342,7 @@ def update_mailsettings():
|
||||
|
||||
|
||||
@admi.route("/admin/scheduledtasks")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def edit_scheduledtasks():
|
||||
content = config.get_scheduled_task_settings()
|
||||
@ -1363,7 +1363,7 @@ def edit_scheduledtasks():
|
||||
|
||||
|
||||
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def update_scheduledtasks():
|
||||
error = False
|
||||
@ -1406,7 +1406,7 @@ def update_scheduledtasks():
|
||||
|
||||
|
||||
@admi.route("/admin/user/<int:user_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def edit_user(user_id):
|
||||
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"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def reset_user_password(user_id):
|
||||
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")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def view_logfile():
|
||||
logfiles = {0: logger.get_logfile(config.config_logfile),
|
||||
@ -1467,7 +1467,7 @@ def view_logfile():
|
||||
|
||||
|
||||
@admi.route("/ajax/log/<int:logtype>")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def send_logfile(logtype):
|
||||
if logtype == 1:
|
||||
@ -1483,7 +1483,7 @@ def send_logfile(logtype):
|
||||
|
||||
|
||||
@admi.route("/admin/logdownload/<int:logtype>")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def download_log(logtype):
|
||||
if logtype == 0:
|
||||
@ -1498,14 +1498,14 @@ def download_log(logtype):
|
||||
|
||||
|
||||
@admi.route("/admin/debug")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def download_debug():
|
||||
return debug_info.send_debug()
|
||||
|
||||
|
||||
@admi.route("/get_update_status", methods=['GET'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def get_update_status():
|
||||
if feature_support['updater']:
|
||||
@ -1516,7 +1516,7 @@ def get_update_status():
|
||||
|
||||
|
||||
@admi.route("/get_updater_status", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def get_updater_status():
|
||||
status = {}
|
||||
@ -1611,7 +1611,7 @@ def ldap_import_create_user(user, user_data):
|
||||
|
||||
|
||||
@admi.route('/import_ldap_users', methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def import_ldap_users():
|
||||
showtext = {}
|
||||
@ -1666,7 +1666,7 @@ def import_ldap_users():
|
||||
|
||||
|
||||
@admi.route("/ajax/canceltask", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def cancel_task():
|
||||
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 babel.core import UnknownLocaleError
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
from .cw_login import current_user
|
||||
|
||||
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 urllib.parse import quote
|
||||
import unidecode
|
||||
from weakref import WeakSet
|
||||
|
||||
from sqlite3 import OperationalError as sqliteOperationalError
|
||||
from sqlalchemy import create_engine
|
||||
@ -40,7 +41,7 @@ except ImportError:
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
||||
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 get_locale
|
||||
from flask import flash
|
||||
@ -48,8 +49,6 @@ from flask import flash
|
||||
from . import logger, ub, isoLanguages
|
||||
from .pagination import Pagination
|
||||
|
||||
from weakref import WeakSet
|
||||
|
||||
|
||||
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 lazy_gettext as N_
|
||||
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.orm.exc import StaleDataError
|
||||
from sqlalchemy.sql.expression import func
|
||||
@ -43,10 +43,11 @@ from . import config, ub, db, calibre_db
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks.upload import TaskUpload
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
from .kobo_sync_status import change_archived_books
|
||||
from .redirect import get_redirect_location
|
||||
from .file_helper import validate_mime_type
|
||||
from .usermanagement import user_login_required, login_required_if_no_ano
|
||||
|
||||
|
||||
editbook = Blueprint('edit-book', __name__)
|
||||
log = logger.create()
|
||||
@ -73,14 +74,14 @@ def edit_required(f):
|
||||
|
||||
|
||||
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def delete_book_from_details(book_id):
|
||||
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>/<string:book_format>", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def delete_book_ajax(book_id, book_format):
|
||||
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>")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def table_get_custom_enum(c_id):
|
||||
ret = list()
|
||||
cc = (calibre_db.session.query(db.CustomColumns)
|
||||
@ -455,7 +456,7 @@ def edit_list_book(param):
|
||||
|
||||
|
||||
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def get_sorted_entry(field, bookid):
|
||||
if field in ['title', 'authors', 'sort', 'author_sort']:
|
||||
book = calibre_db.get_filtered_book(bookid)
|
||||
@ -472,7 +473,7 @@ def get_sorted_entry(field, bookid):
|
||||
|
||||
|
||||
@editbook.route("/ajax/simulatemerge", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@edit_required
|
||||
def simulate_merge_list_book():
|
||||
vals = request.get_json().get('Merge_books')
|
||||
@ -488,7 +489,7 @@ def simulate_merge_list_book():
|
||||
|
||||
|
||||
@editbook.route("/ajax/mergebooks", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@edit_required
|
||||
def merge_list_book():
|
||||
vals = request.get_json().get('Merge_books')
|
||||
@ -526,7 +527,7 @@ def merge_list_book():
|
||||
|
||||
|
||||
@editbook.route("/ajax/xchange", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
@edit_required
|
||||
def table_xchange_author_title():
|
||||
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_babel import gettext as _
|
||||
from flask_login import login_required
|
||||
|
||||
from . import logger, gdriveutils, config, ub, calibre_db, csrf
|
||||
from .admin import admin_required
|
||||
from .file_helper import get_temp_dir
|
||||
from .usermanagement import user_login_required
|
||||
|
||||
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||
log = logger.create()
|
||||
@ -49,7 +49,7 @@ gdrive_watch_callback_token = 'target=calibreweb-watch_files' # nosec
|
||||
|
||||
|
||||
@gdrive.route("/authenticate")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def authenticate_google_drive():
|
||||
try:
|
||||
@ -76,7 +76,7 @@ def google_drive_callback():
|
||||
|
||||
|
||||
@gdrive.route("/watch/subscribe")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def watch_gdrive():
|
||||
if not config.config_google_drive_watch_changes_response:
|
||||
@ -102,7 +102,7 @@ def watch_gdrive():
|
||||
|
||||
|
||||
@gdrive.route("/watch/revoke")
|
||||
@login_required
|
||||
@user_login_required
|
||||
@admin_required
|
||||
def revoke_watch_gdrive():
|
||||
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 lazy_gettext as N_
|
||||
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.exc import InvalidRequestError, OperationalError
|
||||
from werkzeug.datastructures import Headers
|
||||
|
@ -27,10 +27,9 @@ import datetime
|
||||
import mimetypes
|
||||
from uuid import uuid4
|
||||
|
||||
# from babel.dates import format_date
|
||||
from flask import Blueprint, request, url_for
|
||||
from flask_babel import format_date
|
||||
from flask_login import current_user
|
||||
from .cw_login import current_user
|
||||
|
||||
from . import constants, logger
|
||||
|
||||
|
@ -36,7 +36,7 @@ from flask import (
|
||||
redirect,
|
||||
abort
|
||||
)
|
||||
from flask_login import current_user
|
||||
from .cw_login import current_user
|
||||
from werkzeug.datastructures import Headers
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.sql.expression import and_, or_
|
||||
@ -44,7 +44,6 @@ from sqlalchemy.exc import StatementError
|
||||
from sqlalchemy.sql import select
|
||||
import requests
|
||||
|
||||
|
||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||
from . import isoLanguages
|
||||
from .epub import get_epub_layout
|
||||
|
@ -65,12 +65,14 @@ from os import urandom
|
||||
from functools import wraps
|
||||
|
||||
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_limiter import RateLimitExceeded
|
||||
|
||||
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import user_login_required
|
||||
|
||||
|
||||
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>")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def generate_auth_token(user_id):
|
||||
warning = False
|
||||
host_list = request.host.rsplit(':')
|
||||
@ -120,7 +122,7 @@ def generate_auth_token(user_id):
|
||||
|
||||
|
||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def delete_auth_token(user_id):
|
||||
# Invalidate any previously generated Kobo Auth token for this user
|
||||
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/>.
|
||||
|
||||
|
||||
from flask_login import current_user
|
||||
from .cw_login import current_user
|
||||
from . import ub
|
||||
import datetime
|
||||
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,
|
||||
|
@ -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.google import make_google_blueprint, google
|
||||
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 .usermanagement import user_login_required
|
||||
|
||||
from . import constants, logger, config, app, ub
|
||||
|
||||
@ -340,7 +341,7 @@ def github_login():
|
||||
|
||||
|
||||
@oauth.route('/unlink/github', methods=["GET"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def github_login_unlink():
|
||||
return unlink_oauth(oauthblueprints[0]['id'])
|
||||
|
||||
@ -364,6 +365,6 @@ def google_login():
|
||||
|
||||
|
||||
@oauth.route('/unlink/google', methods=["GET"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def google_login_unlink():
|
||||
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 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 gettext as _
|
||||
|
||||
|
||||
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||
|
||||
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 .pagination import Pagination
|
||||
from .web import render_read_books
|
||||
@ -94,7 +95,7 @@ def feed_letter_books(book_id):
|
||||
@opds.route("/opds/new")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_new():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_RECENT):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_RECENT):
|
||||
abort(404)
|
||||
off = request.args.get("offset") or 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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_discover():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_RANDOM):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_RANDOM):
|
||||
abort(404)
|
||||
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)
|
||||
@ -117,7 +118,7 @@ def feed_discover():
|
||||
@opds.route("/opds/rated")
|
||||
@requires_basic_auth_if_no_ano
|
||||
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)
|
||||
off = request.args.get("offset") or 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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_hot():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_HOT):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_HOT):
|
||||
abort(404)
|
||||
off = request.args.get("offset") or 0
|
||||
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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_authorindex():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_AUTHOR):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_AUTHOR):
|
||||
abort(404)
|
||||
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>")
|
||||
@requires_basic_auth_if_no_ano
|
||||
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)
|
||||
off = request.args.get("offset") or 0
|
||||
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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_publisherindex():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_PUBLISHER):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_PUBLISHER):
|
||||
abort(404)
|
||||
off = request.args.get("offset") or 0
|
||||
entries = calibre_db.session.query(db.Publishers)\
|
||||
@ -208,7 +209,7 @@ def feed_publisher(book_id):
|
||||
@opds.route("/opds/category")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_categoryindex():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_CATEGORY):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_CATEGORY):
|
||||
abort(404)
|
||||
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>")
|
||||
@requires_basic_auth_if_no_ano
|
||||
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)
|
||||
off = request.args.get("offset") or 0
|
||||
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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_seriesindex():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_SERIES):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_SERIES):
|
||||
abort(404)
|
||||
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>")
|
||||
@requires_basic_auth_if_no_ano
|
||||
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)
|
||||
off = request.args.get("offset") or 0
|
||||
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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_ratingindex():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_RATING):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_RATING):
|
||||
abort(404)
|
||||
off = request.args.get("offset") or 0
|
||||
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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_formatindex():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_FORMAT):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_FORMAT):
|
||||
abort(404)
|
||||
off = request.args.get("offset") or 0
|
||||
entries = calibre_db.session.query(db.Data).join(db.Books)\
|
||||
@ -339,14 +340,14 @@ def feed_format(book_id):
|
||||
@opds.route("/opds/language/")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_languagesindex():
|
||||
if not current_user.check_visibility(constants.SIDEBAR_LANGUAGE):
|
||||
if not auth.current_user().check_visibility(constants.SIDEBAR_LANGUAGE):
|
||||
abort(404)
|
||||
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()
|
||||
else:
|
||||
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)
|
||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||
len(languages))
|
||||
@ -368,11 +369,11 @@ def feed_languages(book_id):
|
||||
@opds.route("/opds/shelfindex")
|
||||
@requires_basic_auth_if_no_ano
|
||||
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)
|
||||
off = request.args.get("offset") or 0
|
||||
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)
|
||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||
number)
|
||||
@ -382,14 +383,14 @@ def feed_shelfindex():
|
||||
@opds.route("/opds/shelf/<int:book_id>")
|
||||
@requires_basic_auth_if_no_ano
|
||||
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)
|
||||
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,
|
||||
ub.Shelf.id == book_id).first()
|
||||
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),
|
||||
and_(ub.Shelf.is_public == 1,
|
||||
ub.Shelf.id == book_id))).first()
|
||||
@ -422,7 +423,7 @@ def feed_shelf(book_id):
|
||||
@opds.route("/opds/download/<book_id>/<book_format>/")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def opds_download_link(book_id, book_format):
|
||||
if not current_user.role_download():
|
||||
if not auth.current_user().role_download():
|
||||
return abort(403)
|
||||
if "Kobo" in request.headers.get('User-Agent'):
|
||||
client = "kobo"
|
||||
@ -468,7 +469,7 @@ def feed_get_cover(book_id):
|
||||
@opds.route("/opds/readbooks")
|
||||
@requires_basic_auth_if_no_ano
|
||||
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)
|
||||
off = request.args.get("offset") or 0
|
||||
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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
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)
|
||||
off = request.args.get("offset") or 0
|
||||
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 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 sqlalchemy.sql.expression import true
|
||||
|
||||
from . import config, logger, ub
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import user_login_required
|
||||
|
||||
|
||||
remotelogin = Blueprint('remotelogin', __name__)
|
||||
@ -65,7 +66,7 @@ def remote_login():
|
||||
|
||||
@remotelogin.route('/verify/<token>')
|
||||
@remote_login_required
|
||||
@login_required
|
||||
@user_login_required
|
||||
def verify_token(token):
|
||||
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_babel import gettext as _
|
||||
from werkzeug.local import LocalProxy
|
||||
from flask_login import current_user
|
||||
from .cw_login import current_user
|
||||
from sqlalchemy.sql.expression import or_
|
||||
|
||||
from . import config, constants, logger, ub
|
||||
from .ub import User
|
||||
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
def get_sidebar_config(kwargs=None):
|
||||
|
@ -21,7 +21,7 @@ import datetime
|
||||
from . import config, constants
|
||||
from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
|
||||
from .tasks.database import TaskReconnectDatabase
|
||||
from .tasks.tempFolder import TaskDeleteTempFolder
|
||||
from .tasks.clean import TaskClean
|
||||
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks.metadata_backup import TaskBackupMetadata
|
||||
@ -33,7 +33,7 @@ def get_scheduled_tasks(reconnect=True):
|
||||
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
||||
|
||||
# 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
|
||||
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):
|
||||
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
|
||||
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):
|
||||
|
@ -19,7 +19,7 @@ from datetime import datetime
|
||||
|
||||
from flask import Blueprint, request, redirect, url_for, flash
|
||||
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 gettext as _
|
||||
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 .pagination import Pagination
|
||||
|
||||
|
||||
search = Blueprint('search', __name__)
|
||||
|
||||
log = logger.create()
|
||||
|
@ -24,14 +24,14 @@ import os
|
||||
import sys
|
||||
|
||||
from flask import Blueprint, Response, request, url_for
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from .cw_login import current_user
|
||||
from flask_babel import get_locale
|
||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from cps.services.Metadata import Metadata
|
||||
from . import constants, logger, ub, web_server
|
||||
from .usermanagement import user_login_required
|
||||
|
||||
# current_milli_time = lambda: int(round(time() * 1000))
|
||||
|
||||
@ -81,7 +81,7 @@ cl = list_classes(new_list)
|
||||
|
||||
|
||||
@meta.route("/metadata/provider")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def metadata_provider():
|
||||
active = current_user.view_settings.get("metadata", {})
|
||||
provider = list()
|
||||
@ -95,7 +95,7 @@ def metadata_provider():
|
||||
|
||||
@meta.route("/metadata/provider", methods=["POST"])
|
||||
@meta.route("/metadata/provider/<prov_name>", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def metadata_change_active_provider(prov_name):
|
||||
new_state = request.get_json()
|
||||
active = current_user.view_settings.get("metadata", {})
|
||||
@ -122,7 +122,7 @@ def metadata_change_active_provider(prov_name):
|
||||
|
||||
|
||||
@meta.route("/metadata/search", methods=["POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def metadata_search():
|
||||
query = request.form.to_dict().get("query")
|
||||
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_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.sql.expression import func, true
|
||||
|
||||
from . import calibre_db, config, db, logger, ub
|
||||
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()
|
||||
|
||||
@ -39,7 +39,7 @@ shelf = Blueprint('shelf', __name__)
|
||||
|
||||
|
||||
@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):
|
||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
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"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def search_to_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
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"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def remove_from_shelf(shelf_id, book_id):
|
||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
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"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def create_shelf():
|
||||
shelf = ub.Shelf()
|
||||
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
|
||||
|
||||
|
||||
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def edit_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
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"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def delete_shelf(shelf_id):
|
||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
try:
|
||||
@ -259,7 +259,7 @@ def show_shelf(shelf_id, sort_param, page):
|
||||
|
||||
|
||||
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def order_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
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%;
|
||||
height:100%;
|
||||
margin:0;
|
||||
top:0;
|
||||
left:0;
|
||||
}
|
||||
|
||||
.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers{
|
||||
@ -2646,6 +2648,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pdfViewer.copyAll{
|
||||
cursor:wait;
|
||||
}
|
||||
|
||||
.pdfViewer .canvasWrapper{
|
||||
overflow:hidden;
|
||||
width:100%;
|
||||
@ -3010,6 +3016,15 @@ body{
|
||||
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]{
|
||||
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
264
cps/static/js/libs/viewer.mjs
vendored
264
cps/static/js/libs/viewer.mjs
vendored
@ -142,7 +142,7 @@ function scrollIntoView(element, spot, scrollMatches = false) {
|
||||
}
|
||||
parent.scrollTop = offsetY;
|
||||
}
|
||||
function watchScroll(viewAreaElement, callback) {
|
||||
function watchScroll(viewAreaElement, callback, abortSignal = undefined) {
|
||||
const debounceScroll = function (evt) {
|
||||
if (rAF) {
|
||||
return;
|
||||
@ -172,7 +172,13 @@ function watchScroll(viewAreaElement, callback) {
|
||||
_eventHandler: debounceScroll
|
||||
};
|
||||
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;
|
||||
}
|
||||
function parseQueryString(query) {
|
||||
@ -250,9 +256,8 @@ function approximateFraction(x) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function roundToDivide(x, div) {
|
||||
const r = x % div;
|
||||
return r === 0 ? x : Math.round(x - r + div);
|
||||
function floorToDivide(x, div) {
|
||||
return x - x % div;
|
||||
}
|
||||
function getPageSizeInches({
|
||||
view,
|
||||
@ -738,6 +743,10 @@ const defaultOptions = {
|
||||
value: "",
|
||||
kind: OptionKind.API
|
||||
},
|
||||
enableHWA: {
|
||||
value: true,
|
||||
kind: OptionKind.API + OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
enableXfa: {
|
||||
value: true,
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
async getNimbusExperimentData() {}
|
||||
async getGlobalEventNames() {
|
||||
return null;
|
||||
}
|
||||
dispatchGlobalEvent(_event) {}
|
||||
}
|
||||
|
||||
;// CONCATENATED MODULE: ./web/preferences.js
|
||||
@ -1430,6 +1452,7 @@ class BasePreferences {
|
||||
disableFontFace: false,
|
||||
disableRange: false,
|
||||
disableStream: false,
|
||||
enableHWA: true,
|
||||
enableXfa: true,
|
||||
viewerCssTheme: 0
|
||||
});
|
||||
@ -2743,6 +2766,9 @@ class DOMLocalization extends Localization {
|
||||
this.pauseObserving();
|
||||
if (this.roots.size === 0) {
|
||||
this.mutationObserver = null;
|
||||
if (this.windowElement && this.pendingrAF) {
|
||||
this.windowElement.cancelAnimationFrame(this.pendingrAF);
|
||||
}
|
||||
this.windowElement = null;
|
||||
this.pendingrAF = null;
|
||||
this.pendingElements.clear();
|
||||
@ -2843,6 +2869,7 @@ class DOMLocalization extends Localization {
|
||||
;// CONCATENATED MODULE: ./web/l10n.js
|
||||
class L10n {
|
||||
#dir;
|
||||
#elements = new Set();
|
||||
#lang;
|
||||
#l10n;
|
||||
constructor({
|
||||
@ -2877,11 +2904,19 @@ class L10n {
|
||||
return messages?.[0].value || fallback;
|
||||
}
|
||||
async translate(element) {
|
||||
this.#elements.add(element);
|
||||
try {
|
||||
this.#l10n.connectRoot(element);
|
||||
await this.#l10n.translateRoots();
|
||||
} catch {}
|
||||
}
|
||||
async destroy() {
|
||||
for (const element of this.#elements) {
|
||||
this.#l10n.disconnectRoot(element);
|
||||
}
|
||||
this.#elements.clear();
|
||||
this.#l10n.pauseObserving();
|
||||
}
|
||||
pause() {
|
||||
this.#l10n.pauseObserving();
|
||||
}
|
||||
@ -2954,8 +2989,7 @@ class genericl10n_GenericL10n extends L10n {
|
||||
const bundle = await this.#createBundle(lang, baseURL, paths);
|
||||
if (bundle) {
|
||||
yield bundle;
|
||||
}
|
||||
if (lang === "en-us") {
|
||||
} else if (lang === "en-us") {
|
||||
yield this.#createBundleFallback(lang);
|
||||
}
|
||||
}
|
||||
@ -3625,13 +3659,6 @@ function download(blobUrl, filename) {
|
||||
}
|
||||
class DownloadManager {
|
||||
#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) {
|
||||
const blobUrl = URL.createObjectURL(new Blob([data], {
|
||||
type: contentType
|
||||
@ -3666,8 +3693,19 @@ class DownloadManager {
|
||||
this.downloadData(data, filename, contentType);
|
||||
return false;
|
||||
}
|
||||
download(blob, url, filename, _options) {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
download(data, url, filename, _options) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -5180,6 +5218,7 @@ class PDFFindController {
|
||||
source: this,
|
||||
state,
|
||||
previous,
|
||||
entireWord: this.#state?.entireWord ?? null,
|
||||
matchesCount: this.#requestMatchesCount(),
|
||||
rawQuery: this.#state?.query ?? null
|
||||
});
|
||||
@ -7711,7 +7750,8 @@ class PDFThumbnailView {
|
||||
optionalContentConfigPromise,
|
||||
linkService,
|
||||
renderingQueue,
|
||||
pageColors
|
||||
pageColors,
|
||||
enableHWA
|
||||
}) {
|
||||
this.id = id;
|
||||
this.renderingId = "thumbnail" + id;
|
||||
@ -7722,6 +7762,7 @@ class PDFThumbnailView {
|
||||
this.pdfPageRotate = defaultViewport.rotation;
|
||||
this._optionalContentConfigPromise = optionalContentConfigPromise || null;
|
||||
this.pageColors = pageColors || null;
|
||||
this.enableHWA = enableHWA || false;
|
||||
this.eventBus = eventBus;
|
||||
this.linkService = linkService;
|
||||
this.renderingQueue = renderingQueue;
|
||||
@ -7805,10 +7846,11 @@ class PDFThumbnailView {
|
||||
}
|
||||
this.resume = null;
|
||||
}
|
||||
#getPageDrawContext(upscaleFactor = 1) {
|
||||
#getPageDrawContext(upscaleFactor = 1, enableHWA = this.enableHWA) {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d", {
|
||||
alpha: false
|
||||
alpha: false,
|
||||
willReadFrequently: !enableHWA
|
||||
});
|
||||
const outputScale = new OutputScale();
|
||||
canvas.width = upscaleFactor * this.canvasWidth * outputScale.sx | 0;
|
||||
@ -7927,7 +7969,7 @@ class PDFThumbnailView {
|
||||
const {
|
||||
ctx,
|
||||
canvas
|
||||
} = this.#getPageDrawContext();
|
||||
} = this.#getPageDrawContext(1, true);
|
||||
if (img.width <= 2 * canvas.width) {
|
||||
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
|
||||
return canvas;
|
||||
@ -7974,14 +8016,17 @@ class PDFThumbnailViewer {
|
||||
eventBus,
|
||||
linkService,
|
||||
renderingQueue,
|
||||
pageColors
|
||||
pageColors,
|
||||
abortSignal,
|
||||
enableHWA
|
||||
}) {
|
||||
this.container = container;
|
||||
this.eventBus = eventBus;
|
||||
this.linkService = linkService;
|
||||
this.renderingQueue = renderingQueue;
|
||||
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();
|
||||
}
|
||||
#scrollUpdated() {
|
||||
@ -8102,7 +8147,8 @@ class PDFThumbnailViewer {
|
||||
optionalContentConfigPromise,
|
||||
linkService: this.linkService,
|
||||
renderingQueue: this.renderingQueue,
|
||||
pageColors: this.pageColors
|
||||
pageColors: this.pageColors,
|
||||
enableHWA: this.enableHWA
|
||||
});
|
||||
this._thumbnails.push(thumbnail);
|
||||
}
|
||||
@ -8932,13 +8978,6 @@ class TextLayerBuilder {
|
||||
this.div.tabIndex = 0;
|
||||
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) {
|
||||
if (this.#renderingDone && this.#textLayer) {
|
||||
this.#textLayer.update({
|
||||
@ -8964,7 +9003,11 @@ class TextLayerBuilder {
|
||||
this.highlighter?.setTextMapping(textDivs, textContentItemsStr);
|
||||
this.accessibilityManager?.setTextMapping(textDivs);
|
||||
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.highlighter?.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]]);
|
||||
class PDFPageView {
|
||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||
#enableHWA = false;
|
||||
#hasRestrictedScaling = false;
|
||||
#layerProperties = null;
|
||||
#loadingId = null;
|
||||
@ -9129,6 +9173,7 @@ class PDFPageView {
|
||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||
this.maxCanvasPixels = options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
|
||||
this.pageColors = options.pageColors || null;
|
||||
this.#enableHWA = options.enableHWA || false;
|
||||
this.eventBus = options.eventBus;
|
||||
this.renderingQueue = options.renderingQueue;
|
||||
this.l10n = options.l10n;
|
||||
@ -9739,7 +9784,8 @@ class PDFPageView {
|
||||
canvasWrapper.append(canvas);
|
||||
this.canvas = canvas;
|
||||
const ctx = canvas.getContext("2d", {
|
||||
alpha: false
|
||||
alpha: false,
|
||||
willReadFrequently: !this.#enableHWA
|
||||
});
|
||||
const outputScale = this.outputScale = new OutputScale();
|
||||
if (this.maxCanvasPixels === 0) {
|
||||
@ -9760,13 +9806,13 @@ class PDFPageView {
|
||||
}
|
||||
const sfx = approximateFraction(outputScale.sx);
|
||||
const sfy = approximateFraction(outputScale.sy);
|
||||
canvas.width = roundToDivide(width * outputScale.sx, sfx[0]);
|
||||
canvas.height = roundToDivide(height * outputScale.sy, sfy[0]);
|
||||
canvas.width = floorToDivide(width * outputScale.sx, sfx[0]);
|
||||
canvas.height = floorToDivide(height * outputScale.sy, sfy[0]);
|
||||
const {
|
||||
style
|
||||
} = canvas;
|
||||
style.width = roundToDivide(width, sfx[1]) + "px";
|
||||
style.height = roundToDivide(height, sfy[1]) + "px";
|
||||
style.width = floorToDivide(width, sfx[1]) + "px";
|
||||
style.height = floorToDivide(height, sfy[1]) + "px";
|
||||
this.#viewportMap.set(canvas, viewport);
|
||||
const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null;
|
||||
const renderContext = {
|
||||
@ -9870,8 +9916,8 @@ class PDFPageView {
|
||||
|
||||
const DEFAULT_CACHE_SIZE = 10;
|
||||
const PagesCountLimit = {
|
||||
FORCE_SCROLL_MODE_PAGE: 15000,
|
||||
FORCE_LAZY_PAGE_INIT: 7500,
|
||||
FORCE_SCROLL_MODE_PAGE: 10000,
|
||||
FORCE_LAZY_PAGE_INIT: 5000,
|
||||
PAUSE_EAGER_PAGE_INIT: 250
|
||||
};
|
||||
function isValidAnnotationEditorMode(mode) {
|
||||
@ -9933,6 +9979,7 @@ class PDFViewer {
|
||||
#annotationEditorUIManager = null;
|
||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||
#containerTopLeft = null;
|
||||
#enableHWA = false;
|
||||
#enableHighlightFloatingButton = false;
|
||||
#enablePermissions = false;
|
||||
#eventAbortController = null;
|
||||
@ -9946,7 +9993,7 @@ class PDFViewer {
|
||||
#scaleTimeoutId = null;
|
||||
#textLayerMode = TextLayerMode.ENABLE;
|
||||
constructor(options) {
|
||||
const viewerVersion = "4.3.136";
|
||||
const viewerVersion = "4.4.168";
|
||||
if (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.pageColors = options.pageColors || null;
|
||||
this.#mlManager = options.mlManager || null;
|
||||
this.#enableHWA = options.enableHWA || false;
|
||||
this.defaultRenderingQueue = !options.renderingQueue;
|
||||
if (this.defaultRenderingQueue) {
|
||||
this.renderingQueue = new PDFRenderingQueue();
|
||||
@ -9989,7 +10037,16 @@ class PDFViewer {
|
||||
} else {
|
||||
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._resetView();
|
||||
if (this.removePageBorders) {
|
||||
@ -10254,10 +10311,14 @@ class PDFViewer {
|
||||
return;
|
||||
}
|
||||
this.#getAllTextInProgress = true;
|
||||
const savedCursor = this.container.style.cursor;
|
||||
this.container.style.cursor = "wait";
|
||||
const interruptCopy = ev => this.#interruptCopyCondition = ev.key === "Escape";
|
||||
window.addEventListener("keydown", interruptCopy);
|
||||
const {
|
||||
classList
|
||||
} = this.viewer;
|
||||
classList.add("copyAll");
|
||||
const ac = new AbortController();
|
||||
window.addEventListener("keydown", ev => this.#interruptCopyCondition = ev.key === "Escape", {
|
||||
signal: ac.signal
|
||||
});
|
||||
this.getAllText().then(async text => {
|
||||
if (text !== null) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
@ -10267,8 +10328,8 @@ class PDFViewer {
|
||||
}).finally(() => {
|
||||
this.#getAllTextInProgress = false;
|
||||
this.#interruptCopyCondition = false;
|
||||
window.removeEventListener("keydown", interruptCopy);
|
||||
this.container.style.cursor = savedCursor;
|
||||
ac.abort();
|
||||
classList.remove("copyAll");
|
||||
});
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@ -10401,7 +10462,8 @@ class PDFViewer {
|
||||
maxCanvasPixels: this.maxCanvasPixels,
|
||||
pageColors,
|
||||
l10n: this.l10n,
|
||||
layerProperties: this._layerProperties
|
||||
layerProperties: this._layerProperties,
|
||||
enableHWA: this.#enableHWA
|
||||
});
|
||||
this._pages.push(pageView);
|
||||
}
|
||||
@ -12059,9 +12121,11 @@ const PDFViewerApplication = {
|
||||
isViewerEmbedded: window.parent !== window,
|
||||
url: "",
|
||||
baseUrl: "",
|
||||
_allowedGlobalEventsPromise: null,
|
||||
_downloadUrl: "",
|
||||
_eventBusAbortController: null,
|
||||
_windowAbortController: null,
|
||||
_globalAbortController: new AbortController(),
|
||||
documentInfo: null,
|
||||
metadata: null,
|
||||
_contentDispositionFilename: null,
|
||||
@ -12202,7 +12266,8 @@ const PDFViewerApplication = {
|
||||
externalServices,
|
||||
l10n
|
||||
} = this;
|
||||
const eventBus = AppOptions.get("isInAutomation") ? new AutomationEventBus() : new EventBus();
|
||||
let eventBus;
|
||||
eventBus = new EventBus();
|
||||
this.eventBus = eventBus;
|
||||
this.overlayManager = new OverlayManager();
|
||||
const pdfRenderingQueue = new PDFRenderingQueue();
|
||||
@ -12236,6 +12301,7 @@ const PDFViewerApplication = {
|
||||
foreground: AppOptions.get("pageColorsForeground")
|
||||
} : null;
|
||||
const altTextManager = appConfig.altTextDialog ? new AltTextManager(appConfig.altTextDialog, container, this.overlayManager, eventBus) : null;
|
||||
const enableHWA = AppOptions.get("enableHWA");
|
||||
const pdfViewer = new PDFViewer({
|
||||
container,
|
||||
viewer,
|
||||
@ -12257,7 +12323,9 @@ const PDFViewerApplication = {
|
||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||
enablePermissions: AppOptions.get("enablePermissions"),
|
||||
pageColors,
|
||||
mlManager: this.mlManager
|
||||
mlManager: this.mlManager,
|
||||
abortSignal: this._globalAbortController.signal,
|
||||
enableHWA
|
||||
});
|
||||
this.pdfViewer = pdfViewer;
|
||||
pdfRenderingQueue.setViewer(pdfViewer);
|
||||
@ -12269,7 +12337,9 @@ const PDFViewerApplication = {
|
||||
eventBus,
|
||||
renderingQueue: pdfRenderingQueue,
|
||||
linkService: pdfLinkService,
|
||||
pageColors
|
||||
pageColors,
|
||||
abortSignal: this._globalAbortController.signal,
|
||||
enableHWA
|
||||
});
|
||||
pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
|
||||
}
|
||||
@ -12395,24 +12465,28 @@ const PDFViewerApplication = {
|
||||
source: this,
|
||||
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();
|
||||
const {
|
||||
files
|
||||
} = evt.dataTransfer;
|
||||
if (!files || files.length === 0) {
|
||||
evt.stopPropagation();
|
||||
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,
|
||||
fileInput: evt.dataTransfer
|
||||
});*/
|
||||
});
|
||||
});*/
|
||||
if (!AppOptions.get("supportsDocumentFonts")) {
|
||||
AppOptions.set("disableFontFace", true);
|
||||
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 = {}) {
|
||||
const url = this._downloadUrl,
|
||||
filename = this._docFilename;
|
||||
let data;
|
||||
try {
|
||||
this._ensureDownloadComplete();
|
||||
const 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);
|
||||
if (this.downloadComplete) {
|
||||
data = await this.pdfDocument.getData();
|
||||
}
|
||||
} catch {}
|
||||
this.downloadManager.download(data, this._downloadUrl, this._docFilename, options);
|
||||
},
|
||||
async save(options = {}) {
|
||||
if (this._saveInProgress) {
|
||||
@ -12673,15 +12736,9 @@ const PDFViewerApplication = {
|
||||
}
|
||||
this._saveInProgress = true;
|
||||
await this.pdfScriptingManager.dispatchWillSave();
|
||||
const url = this._downloadUrl,
|
||||
filename = this._docFilename;
|
||||
try {
|
||||
this._ensureDownloadComplete();
|
||||
const data = await this.pdfDocument.saveDocument();
|
||||
const blob = new Blob([data], {
|
||||
type: "application/pdf"
|
||||
});
|
||||
await this.downloadManager.download(blob, url, filename, options);
|
||||
this.downloadManager.download(data, this._downloadUrl, this._docFilename, options);
|
||||
} catch (reason) {
|
||||
console.error(`Error when saving the document: ${reason.message}`);
|
||||
await this.download(options);
|
||||
@ -12699,12 +12756,13 @@ const PDFViewerApplication = {
|
||||
});
|
||||
}
|
||||
},
|
||||
downloadOrSave(options = {}) {
|
||||
if (this.pdfDocument?.annotationStorage.size > 0) {
|
||||
this.save(options);
|
||||
} else {
|
||||
this.download(options);
|
||||
}
|
||||
async downloadOrSave(options = {}) {
|
||||
const {
|
||||
classList
|
||||
} = this.appConfig.appContainer;
|
||||
classList.add("wait");
|
||||
await (this.pdfDocument?.annotationStorage.size > 0 ? this.save(options) : this.download(options));
|
||||
classList.remove("wait");
|
||||
},
|
||||
async _documentError(key, moreInfo = null) {
|
||||
this._unblockDocumentLoadEvent();
|
||||
@ -13471,6 +13529,14 @@ const PDFViewerApplication = {
|
||||
this._windowAbortController?.abort();
|
||||
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) {
|
||||
if (this[prop] > 0 && ticks < 0 || this[prop] < 0 && ticks > 0) {
|
||||
this[prop] = 0;
|
||||
@ -13664,7 +13730,7 @@ function webViewerHashchange(evt) {
|
||||
}
|
||||
}
|
||||
{
|
||||
var webViewerFileInputChange = function (evt) {
|
||||
/*var webViewerFileInputChange = function (evt) {
|
||||
if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
|
||||
return;
|
||||
}
|
||||
@ -13674,7 +13740,7 @@ function webViewerHashchange(evt) {
|
||||
originalUrl: file.name
|
||||
});
|
||||
};
|
||||
/*var webViewerOpenFile = function (evt) {
|
||||
var webViewerOpenFile = function (evt) {
|
||||
PDFViewerApplication._openFileInput?.click();
|
||||
};*/
|
||||
}
|
||||
@ -13768,6 +13834,7 @@ function webViewerUpdateFindMatchesCount({
|
||||
function webViewerUpdateFindControlState({
|
||||
state,
|
||||
previous,
|
||||
entireWord,
|
||||
matchesCount,
|
||||
rawQuery
|
||||
}) {
|
||||
@ -13775,6 +13842,7 @@ function webViewerUpdateFindControlState({
|
||||
PDFViewerApplication.externalServices.updateFindControlState({
|
||||
result: state,
|
||||
findPrevious: previous,
|
||||
entireWord,
|
||||
matchesCount,
|
||||
rawQuery
|
||||
});
|
||||
@ -14066,14 +14134,14 @@ function webViewerKeyDown(evt) {
|
||||
});
|
||||
handled = true;
|
||||
break;
|
||||
/*case 79:
|
||||
case 79:
|
||||
{
|
||||
eventBus.dispatch("openfile", {
|
||||
source: window
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;*/
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cmd === 3 || cmd === 10) {
|
||||
@ -14273,8 +14341,8 @@ function webViewerReportTelemetry({
|
||||
|
||||
|
||||
|
||||
const pdfjsVersion = "4.3.136";
|
||||
const pdfjsBuild = "0cec64437";
|
||||
const pdfjsVersion = "4.4.168";
|
||||
const pdfjsBuild = "19fbc8998";
|
||||
const AppConstants = {
|
||||
LinkTarget: LinkTarget,
|
||||
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.
|
||||
pdfjs-download-button-label = Pellgargañ
|
||||
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
|
||||
|
||||
@ -214,6 +208,7 @@ pdfjs-find-next-button =
|
||||
pdfjs-find-next-button-label = War-lerc'h
|
||||
pdfjs-find-highlight-checkbox = Usskediñ pep tra
|
||||
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-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
|
||||
@ -311,3 +306,7 @@ pdfjs-editor-alt-text-save-button = Enrollañ
|
||||
|
||||
## 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 =
|
||||
.title = Núverandi síða (Skoða vefslóð frá núverandi síðu)
|
||||
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
|
||||
|
||||
@ -284,7 +278,7 @@ pdfjs-text-annotation-type =
|
||||
|
||||
## 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-ok-button = Í lagi
|
||||
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 =
|
||||
.title = Áherslulita
|
||||
pdfjs-editor-highlight-button-label = Áherslulita
|
||||
pdfjs-highlight-floating-button =
|
||||
.title = Áherslulita
|
||||
pdfjs-highlight-floating-button1 =
|
||||
.title = Áherslulita
|
||||
.aria-label = Áherslulita
|
||||
|
@ -279,7 +279,7 @@ pdfjs-text-annotation-type =
|
||||
## Password
|
||||
|
||||
pdfjs-password-label = この PDF ファイルを開くためのパスワードを入力してください。
|
||||
pdfjs-password-invalid = 無効なパスワードです。もう一度やり直してください。
|
||||
pdfjs-password-invalid = パスワードが正しくありません。もう一度試してください。
|
||||
pdfjs-password-ok-button = OK
|
||||
pdfjs-password-cancel-button = キャンセル
|
||||
pdfjs-web-fonts-disabled = ウェブフォントが無効になっています: 埋め込まれた PDF のフォントを使用できません。
|
||||
@ -298,8 +298,6 @@ pdfjs-editor-stamp-button-label = 画像を追加または編集
|
||||
pdfjs-editor-highlight-button =
|
||||
.title = 強調します
|
||||
pdfjs-editor-highlight-button-label = 強調
|
||||
pdfjs-highlight-floating-button =
|
||||
.title = 強調
|
||||
pdfjs-highlight-floating-button1 =
|
||||
.title = 強調
|
||||
.aria-label = 強調します
|
||||
|
@ -51,12 +51,6 @@ pdfjs-download-button-label = Sader
|
||||
pdfjs-bookmark-button =
|
||||
.title = Asebter amiran (Sken-d tansa URL seg usebter 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
|
||||
|
||||
@ -301,8 +295,27 @@ pdfjs-editor-ink-button-label = Suneɣ
|
||||
pdfjs-editor-stamp-button =
|
||||
.title = Rnu neɣ ẓreg tugniwin
|
||||
pdfjs-editor-stamp-button-label = Rnu neɣ ẓreg tugniwin
|
||||
pdfjs-editor-remove-button =
|
||||
.title = Kkes
|
||||
pdfjs-editor-highlight-button =
|
||||
.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
|
||||
pdfjs-editor-free-text-color-input = Initen
|
||||
pdfjs-editor-free-text-size-input = Teɣzi
|
||||
@ -312,6 +325,8 @@ pdfjs-editor-ink-opacity-input = Tebrek
|
||||
pdfjs-editor-stamp-add-image-button =
|
||||
.title = 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 =
|
||||
.aria-label = Amaẓrag n uḍris
|
||||
pdfjs-free-text-default-content = Bdu tira...
|
||||
@ -335,3 +350,37 @@ pdfjs-editor-alt-text-decorative-tooltip = Yettwacreḍ d adlag
|
||||
## Editor resizers
|
||||
## 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 =
|
||||
.title = Gjeldande side (sjå URL frå 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
|
||||
|
||||
@ -301,9 +295,24 @@ pdfjs-editor-ink-button-label = Teikne
|
||||
pdfjs-editor-stamp-button =
|
||||
.title = 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.
|
||||
|
||||
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 =
|
||||
.title = 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 =
|
||||
.aria-label = Tekstredigering
|
||||
pdfjs-free-text-default-content = Byrje å skrive…
|
||||
@ -345,9 +358,23 @@ pdfjs-editor-alt-text-textarea =
|
||||
## Editor resizers
|
||||
## 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
|
||||
|
||||
# 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 =
|
||||
.title = Gul
|
||||
pdfjs-editor-colorpicker-green =
|
||||
@ -358,3 +385,10 @@ pdfjs-editor-colorpicker-pink =
|
||||
.title = Rosa
|
||||
pdfjs-editor-colorpicker-red =
|
||||
.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 =
|
||||
.title = 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.
|
||||
|
||||
|
@ -16,31 +16,48 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from urllib.request import urlopen
|
||||
import datetime
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TaskDeleteTempFolder(CalibreTask):
|
||||
class TaskClean(CalibreTask):
|
||||
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.app_db_session = ub.get_new_session_instance()
|
||||
|
||||
def run(self, worker_thread):
|
||||
# delete temp folder
|
||||
try:
|
||||
file_helper.del_temp_dir()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except (PermissionError, OSError) as 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.app_db_session.remove()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "Delete Temp Folder"
|
||||
return "Clean up"
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
@ -17,7 +17,7 @@
|
||||
from markupsafe import escape
|
||||
|
||||
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 format_datetime
|
||||
from babel.units import format_unit
|
||||
@ -26,6 +26,7 @@ from . import logger
|
||||
from .render_template import render_title_template
|
||||
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, STAT_ENDED, \
|
||||
STAT_CANCELLED
|
||||
from .usermanagement import user_login_required
|
||||
|
||||
tasks = Blueprint('tasks', __name__)
|
||||
|
||||
@ -33,14 +34,14 @@ log = logger.create()
|
||||
|
||||
|
||||
@tasks.route("/ajax/emailstat")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def get_email_status_json():
|
||||
tasks = WorkerThread.get_instance().tasks
|
||||
return jsonify(render_task_status(tasks))
|
||||
|
||||
|
||||
@tasks.route("/tasks")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def get_tasks_status():
|
||||
# if current user admin, show all email, otherwise only own emails
|
||||
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 binascii import hexlify
|
||||
|
||||
from flask_login import AnonymousUserMixin, current_user
|
||||
from flask_login import user_logged_in
|
||||
from .cw_login import AnonymousUserMixin, current_user
|
||||
from .cw_login import user_logged_in
|
||||
|
||||
try:
|
||||
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
||||
@ -71,17 +71,19 @@ def signal_store_user_session(object, user):
|
||||
|
||||
|
||||
def store_user_session():
|
||||
if flask_session.get('user_id', ""):
|
||||
flask_session['_user_id'] = flask_session.get('user_id', "")
|
||||
_user = flask_session.get('_user_id', "")
|
||||
_id = flask_session.get('_id', "")
|
||||
_random = flask_session.get('_random', "")
|
||||
if flask_session.get('_user_id', ""):
|
||||
try:
|
||||
if not check_user_session(flask_session.get('_user_id', ""), flask_session.get('_id', "")):
|
||||
user_session = User_Sessions(flask_session.get('_user_id', ""), flask_session.get('_id', ""))
|
||||
if not check_user_session(_user, _id, _random):
|
||||
expiry = int((datetime.datetime.now() + datetime.timedelta(days=31)).timestamp())
|
||||
user_session = User_Sessions(_user, _id, _random, expiry)
|
||||
session.add(user_session)
|
||||
session.commit()
|
||||
log.debug("Login and store session : " + flask_session.get('_id', ""))
|
||||
log.debug("Login and store session : " + _id)
|
||||
else:
|
||||
log.debug("Found stored session: " + flask_session.get('_id', ""))
|
||||
log.debug("Found stored session: " + _id)
|
||||
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
||||
session.rollback()
|
||||
log.exception(e)
|
||||
@ -100,13 +102,23 @@ def delete_user_session(user_id, session_key):
|
||||
log.exception(ex)
|
||||
|
||||
|
||||
def check_user_session(user_id, session_key):
|
||||
def check_user_session(user_id, session_key, random):
|
||||
try:
|
||||
return bool(session.query(User_Sessions).filter(User_Sessions.user_id==user_id,
|
||||
User_Sessions.session_key==session_key).one_or_none())
|
||||
found = session.query(User_Sessions).filter(User_Sessions.user_id==user_id,
|
||||
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:
|
||||
session.rollback()
|
||||
log.exception(e)
|
||||
return False
|
||||
|
||||
|
||||
user_logged_in.connect(signal_store_user_session)
|
||||
@ -335,11 +347,16 @@ class User_Sessions(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
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__()
|
||||
self.user_id = user_id
|
||||
self.session_key = session_key
|
||||
self.random = random
|
||||
self.expiry = expiry
|
||||
|
||||
|
||||
# Baseclass representing Shelfs in calibre-web in app.db
|
||||
@ -552,39 +569,14 @@ class Thumbnail(Base):
|
||||
|
||||
# Add missing tables during migration of database
|
||||
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"):
|
||||
ArchivedBook.__table__.create(bind=engine)
|
||||
if not engine.dialect.has_table(engine.connect(), "thumbnail"):
|
||||
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
|
||||
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:
|
||||
# Handle table exists, but no content
|
||||
cnt = _session.query(Registration).count()
|
||||
@ -598,190 +590,38 @@ def migrate_registration_table(engine, _session):
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# Remove login capability of user Guest
|
||||
def migrate_guest_password(engine):
|
||||
def migrate_user_session_table(engine, _session):
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
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.query(exists().where(User_Sessions.random)).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 remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0"))
|
||||
conn.execute(text("update remote_auth_token set 'token_type' = 0"))
|
||||
conn.execute(text("ALTER TABLE user_session ADD column 'random' String"))
|
||||
conn.execute(text("ALTER TABLE user_session ADD column 'expiry' Integer"))
|
||||
trans.commit()
|
||||
|
||||
|
||||
# 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
|
||||
# rows with SQL commands
|
||||
# maybe 4/5 versions back to current should work.
|
||||
# Migration is done by checking if relevant columns are existing, and then adding rows with SQL commands
|
||||
def migrate_Database(_session):
|
||||
engine = _session.bind
|
||||
add_missing_tables(engine, _session)
|
||||
migrate_registration_table(engine, _session)
|
||||
migrate_readBook(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)
|
||||
migrate_user_session_table(engine, _session)
|
||||
|
||||
|
||||
def clean_database(_session):
|
||||
# Remove expired remote login tokens
|
||||
now = datetime.datetime.now()
|
||||
try:
|
||||
_session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\
|
||||
filter(RemoteAuthToken.token_type != 1).delete()
|
||||
_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
|
||||
|
@ -19,93 +19,123 @@
|
||||
from functools import wraps
|
||||
|
||||
from sqlalchemy.sql.expression import func
|
||||
from werkzeug.security import check_password_hash
|
||||
from flask_login import login_required, login_user
|
||||
from flask import request, Response
|
||||
from .cw_login import login_required
|
||||
|
||||
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()
|
||||
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):
|
||||
@wraps(func)
|
||||
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:
|
||||
return func(*args, **kwargs)
|
||||
return login_required(func)(*args, **kwargs)
|
||||
|
||||
return decorated_view
|
||||
|
||||
def requires_basic_auth_if_no_ano(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
auth = request.authorization
|
||||
if not auth or auth.type != 'basic':
|
||||
if config.config_anonbrowse != 1:
|
||||
|
||||
def user_login_required(func):
|
||||
@wraps(func)
|
||||
def decorated_view(*args, **kwargs):
|
||||
if config.config_allow_reverse_proxy_header_login:
|
||||
user = load_user_from_reverse_proxy_header(request)
|
||||
if user:
|
||||
return f(*args, **kwargs)
|
||||
return _authenticate()
|
||||
else:
|
||||
return f(*args, **kwargs)
|
||||
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
||||
login_result, error = services.ldap.bind_user(auth.username, auth.password)
|
||||
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
|
||||
g.flask_httpauth_user = user
|
||||
return func(*args, **kwargs)
|
||||
g.flask_httpauth_user = None
|
||||
return login_required(func)(*args, **kwargs)
|
||||
|
||||
return decorated_view
|
||||
|
||||
|
||||
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):
|
||||
if config.config_allow_reverse_proxy_header_login:
|
||||
rp_header_name = config.config_reverse_proxy_login_header_name
|
||||
if rp_header_name:
|
||||
rp_header_username = req.headers.get(rp_header_name)
|
||||
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:
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
login_user(user)
|
||||
return user
|
||||
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_babel import gettext as _
|
||||
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.util import get_remote_address
|
||||
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
||||
@ -59,6 +59,7 @@ from .kobo_sync_status import change_archived_books
|
||||
from . import limiter
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks_status import render_task_status
|
||||
from .usermanagement import user_login_required
|
||||
|
||||
|
||||
feature_support = {
|
||||
@ -143,14 +144,14 @@ def viewer_required(f):
|
||||
|
||||
|
||||
@web.route("/ajax/emailstat")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def get_email_status_json():
|
||||
tasks = WorkerThread.get_instance().tasks
|
||||
return jsonify(render_task_status(tasks))
|
||||
|
||||
|
||||
@web.route("/ajax/bookmark/<int:book_id>/<book_format>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def set_bookmark(book_id, book_format):
|
||||
bookmark_key = request.form["bookmark"]
|
||||
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'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def toggle_read(book_id):
|
||||
message = edit_book_read_status(book_id)
|
||||
if message:
|
||||
@ -180,7 +181,7 @@ def toggle_read(book_id):
|
||||
|
||||
|
||||
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def toggle_archived(book_id):
|
||||
is_archived = change_archived_books(book_id, message="Book {} archive bit toggled".format(book_id))
|
||||
if is_archived:
|
||||
@ -204,7 +205,7 @@ def update_view():
|
||||
|
||||
'''
|
||||
@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):
|
||||
book = calibre_db.get_book(book_id)
|
||||
if not book:
|
||||
@ -816,7 +817,7 @@ def books_list(data, sort_param, book_id, page):
|
||||
|
||||
|
||||
@web.route("/table")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def books_table():
|
||||
visibility = current_user.view_settings.get('table', {})
|
||||
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||
@ -825,7 +826,7 @@ def books_table():
|
||||
|
||||
|
||||
@web.route("/ajax/listbooks")
|
||||
@login_required
|
||||
@user_login_required
|
||||
def list_books():
|
||||
off = int(request.args.get("offset") or 0)
|
||||
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'])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def update_table_settings():
|
||||
current_user.view_settings['table'] = json.loads(request.data)
|
||||
try:
|
||||
@ -1339,7 +1340,6 @@ def register():
|
||||
|
||||
def handle_login_user(user, remember, message, category):
|
||||
login_user(user, remember=remember)
|
||||
ub.store_user_session()
|
||||
flash(message, category=category)
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
|
||||
@ -1443,7 +1443,7 @@ def login_post():
|
||||
|
||||
|
||||
@web.route('/logout')
|
||||
@login_required
|
||||
@user_login_required
|
||||
def logout():
|
||||
if current_user is not None and current_user.is_authenticated:
|
||||
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"])
|
||||
@login_required
|
||||
@user_login_required
|
||||
def profile():
|
||||
languages = calibre_db.speaking_language()
|
||||
translations = get_available_locale()
|
||||
|
@ -2,7 +2,7 @@ Werkzeug<3.0.0
|
||||
APScheduler>=3.6.3,<3.11.0
|
||||
Babel>=1.3,<3.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>=1.0.2,<3.1.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
|
||||
bleach>=6.0.0,<6.2.0
|
||||
python-magic>=0.4.27,<0.5.0
|
||||
flask-httpAuth>=4.4.0
|
||||
|
Loading…
Reference in New Issue
Block a user