Fix for flask limiter 4.0

Fix reseting flask_limiter keys
This commit is contained in:
Ozzie Isaacs
2026-02-17 18:30:47 +01:00
parent cabcace3f0
commit e3bf369ad6
7 changed files with 128 additions and 183 deletions

View File

@@ -19,38 +19,30 @@
import base64 import base64
from datetime import datetime, timezone from datetime import datetime, timezone
from functools import wraps
import os import os
import uuid import uuid
import zipfile import zipfile
from time import gmtime, strftime from time import gmtime, strftime
import json import json
from urllib.parse import unquote from urllib.parse import unquote
import requests
from flask import ( from flask import Blueprint, request, make_response, jsonify, current_app, url_for, redirect, abort, g
Blueprint,
request,
make_response,
jsonify,
current_app,
url_for,
redirect,
abort
)
from .cw_login import current_user
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.sql.expression import and_, or_ from sqlalchemy.sql.expression import and_, or_
from sqlalchemy.exc import StatementError from sqlalchemy.exc import StatementError
import requests from flask_limiter.util import get_remote_address
from .cw_login import current_user, login_user
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
from . import isoLanguages
from .epub import get_epub_layout from .epub import get_epub_layout
from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE, BASE_DIR from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE, BASE_DIR
from .helper import get_download_link from .helper import get_download_link
from .services import SyncToken as SyncToken from .services import SyncToken as SyncToken
from .web import download_required from .web import download_required
from .kobo_auth import requires_kobo_auth, get_auth_token from . import isoLanguages, limiter
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]} KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
KOBO_STOREAPI_URL = "https://storeapi.kobo.com" KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
@@ -65,6 +57,33 @@ kobo_auth.register_url_value_preprocessor(kobo)
log = logger.create() log = logger.create()
def get_auth_token():
if "auth_token" in g:
return g.get("auth_token")
else:
return None
def requires_kobo_auth(f):
@wraps(f)
def inner(*args, **kwargs):
auth_token = get_auth_token()
if auth_token is not None:
user = (
ub.session.query(ub.User)
.join(ub.RemoteAuthToken)
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
.first()
)
if user is not None:
login_user(user)
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
return f(*args, **kwargs)
log.debug("Received Kobo request without a recognizable auth token.")
return abort(401)
return inner
def get_store_url_for_current_request(): def get_store_url_for_current_request():
# Programmatically modify the current url to point to the official Kobo store # Programmatically modify the current url to point to the official Kobo store
__, __, request_path_with_auth_token = request.full_path.rpartition("/kobo/") __, __, request_path_with_auth_token = request.full_path.rpartition("/kobo/")
@@ -140,7 +159,7 @@ def convert_to_kobo_timestamp_string(timestamp):
@kobo.route("/v1/library/sync") @kobo.route("/v1/library/sync")
@requires_kobo_auth @requires_kobo_auth
# @download_required # @limiter.limit("3/minute", key_func=get_remote_address)
def HandleSyncRequest(): def HandleSyncRequest():
if not current_user.role_download(): if not current_user.role_download():
log.info("Users need download permissions for syncing library to Kobo reader") log.info("Users need download permissions for syncing library to Kobo reader")
@@ -336,6 +355,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
@kobo.route("/v1/library/<book_uuid>/metadata") @kobo.route("/v1/library/<book_uuid>/metadata")
@requires_kobo_auth @requires_kobo_auth
@download_required @download_required
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleMetadataRequest(book_uuid): def HandleMetadataRequest(book_uuid):
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:
log.debug('Kobo: Received unproxied request, changed request port to external server port') log.debug('Kobo: Received unproxied request, changed request port to external server port')
@@ -368,7 +388,7 @@ def get_download_url_for_book(book_id, book_format):
) )
return url_for( return url_for(
"kobo.download_book", "kobo.download_book",
auth_token=kobo_auth.get_auth_token(), auth_token=get_auth_token(),
book_id=book_id, book_id=book_id,
book_format=book_format.lower(), book_format=book_format.lower(),
_external=True, _external=True,
@@ -503,9 +523,11 @@ def get_metadata(book):
return metadata return metadata
@csrf.exempt @csrf.exempt
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"]) @kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
# Creates a Shelf with the given items, and returns the shelf's uuid. # Creates a Shelf with the given items, and returns the shelf's uuid.
def HandleTagCreate(): def HandleTagCreate():
# catch delete requests, otherwise they are handled in the book delete handler # catch delete requests, otherwise they are handled in the book delete handler
@@ -541,6 +563,7 @@ def HandleTagCreate():
@csrf.exempt @csrf.exempt
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"]) @kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleTagUpdate(tag_id): def HandleTagUpdate(tag_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id, shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id,
ub.Shelf.user_id == current_user.id).one_or_none() ub.Shelf.user_id == current_user.id).one_or_none()
@@ -595,6 +618,7 @@ def add_items_to_shelf(items, shelf):
@csrf.exempt @csrf.exempt
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"]) @kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleTagAddItem(tag_id): def HandleTagAddItem(tag_id):
items = None items = None
try: try:
@@ -625,6 +649,7 @@ def HandleTagAddItem(tag_id):
@csrf.exempt @csrf.exempt
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"]) @kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleTagRemoveItem(tag_id): def HandleTagRemoveItem(tag_id):
items = None items = None
try: try:
@@ -758,6 +783,7 @@ def create_kobo_tag(shelf):
@csrf.exempt @csrf.exempt
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"]) @kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleStateRequest(book_uuid): def HandleStateRequest(book_uuid):
book = calibre_db.get_book_by_uuid(book_uuid) book = calibre_db.get_book_by_uuid(book_uuid)
if not book or not book.data: if not book or not book.data:
@@ -907,6 +933,7 @@ def get_current_bookmark_response(current_bookmark):
@kobo.route("/<book_uuid>/<width>/<height>/<isGreyscale>/image.jpg", defaults={'Quality': ""}) @kobo.route("/<book_uuid>/<width>/<height>/<isGreyscale>/image.jpg", defaults={'Quality': ""})
@kobo.route("/<book_uuid>/<width>/<height>/<Quality>/<isGreyscale>/image.jpg") @kobo.route("/<book_uuid>/<width>/<height>/<Quality>/<isGreyscale>/image.jpg")
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale): def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
try: try:
if int(height) > 1000: if int(height) > 1000:
@@ -943,6 +970,7 @@ def TopLevelEndpoint():
@csrf.exempt @csrf.exempt
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"]) @kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleBookDeletionRequest(book_uuid): def HandleBookDeletionRequest(book_uuid):
log.info("Kobo book delete request received for book %s" % book_uuid) log.info("Kobo book delete request received for book %s" % book_uuid)
book = calibre_db.get_book_by_uuid(book_uuid) book = calibre_db.get_book_by_uuid(book_uuid)
@@ -962,6 +990,7 @@ def HandleBookDeletionRequest(book_uuid):
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"]) @kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"])
@kobo.route("/v1/library/<dummy>/preview", methods=["POST"]) @kobo.route("/v1/library/<dummy>/preview", methods=["POST"])
def HandleUnimplementedRequest(dummy=None): def HandleUnimplementedRequest(dummy=None):
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)", log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)",
request.base_url) request.base_url)
return redirect_or_proxy_request() return redirect_or_proxy_request()
@@ -976,6 +1005,9 @@ def HandleUnimplementedRequest(dummy=None):
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"]) @kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
@kobo.route("/v1/assets", methods=["GET"]) @kobo.route("/v1/assets", methods=["GET"])
def HandleUserRequest(dummy=None): def HandleUserRequest(dummy=None):
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
log.error("Key: {}".format(limiter.current_limit.key))
log.error("Remaining: {}".format(limiter.current_limit.remaining))
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url) log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
return redirect_or_proxy_request() return redirect_or_proxy_request()
@@ -983,6 +1015,7 @@ def HandleUserRequest(dummy=None):
@csrf.exempt @csrf.exempt
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"]) @kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
def handle_benefits(): def handle_benefits():
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
if config.config_kobo_proxy: if config.config_kobo_proxy:
return redirect_or_proxy_request() return redirect_or_proxy_request()
else: else:
@@ -992,6 +1025,7 @@ def handle_benefits():
@csrf.exempt @csrf.exempt
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"]) @kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
def handle_getests(): def handle_getests():
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
if config.config_kobo_proxy: if config.config_kobo_proxy:
return redirect_or_proxy_request() return redirect_or_proxy_request()
else: else:
@@ -1018,6 +1052,7 @@ def handle_getests():
@kobo.route("/v1/affiliate", methods=["GET", "POST"]) @kobo.route("/v1/affiliate", methods=["GET", "POST"])
@kobo.route("/v1/deals", methods=["GET", "POST"]) @kobo.route("/v1/deals", methods=["GET", "POST"])
def HandleProductsRequest(dummy=None): def HandleProductsRequest(dummy=None):
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)", log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)",
request.base_url) request.base_url)
return redirect_or_proxy_request() return redirect_or_proxy_request()
@@ -1046,7 +1081,10 @@ def make_calibre_web_auth_response():
@kobo.route("/v1/auth/refresh", methods=["POST"]) @kobo.route("/v1/auth/refresh", methods=["POST"])
@kobo.route("/v1/auth/device", methods=["POST"]) @kobo.route("/v1/auth/device", methods=["POST"])
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleAuthRequest(): def HandleAuthRequest():
log.error(limiter.current_limit)
log.error(limiter.current_limit)
log.debug('Kobo Auth request') log.debug('Kobo Auth request')
if config.config_kobo_proxy: if config.config_kobo_proxy:
try: try:
@@ -1058,6 +1096,7 @@ def HandleAuthRequest():
@kobo.route("/v1/initialization") @kobo.route("/v1/initialization")
@requires_kobo_auth @requires_kobo_auth
# @limiter.limit("3/minute", key_func=get_remote_address)
def HandleInitRequest(): def HandleInitRequest():
log.info('Init') log.info('Init')
@@ -1088,7 +1127,7 @@ def HandleInitRequest():
kobo_resources["image_host"] = calibre_web_url kobo_resources["image_host"] = calibre_web_url
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url + kobo_resources["image_url_quality_template"] = unquote(calibre_web_url +
url_for("kobo.HandleCoverImageRequest", url_for("kobo.HandleCoverImageRequest",
auth_token=kobo_auth.get_auth_token(), auth_token=get_auth_token(),
book_uuid="{ImageId}", book_uuid="{ImageId}",
width="{width}", width="{width}",
height="{height}", height="{height}",
@@ -1096,7 +1135,7 @@ def HandleInitRequest():
isGreyscale='isGreyscale')) isGreyscale='isGreyscale'))
kobo_resources["image_url_template"] = unquote(calibre_web_url + kobo_resources["image_url_template"] = unquote(calibre_web_url +
url_for("kobo.HandleCoverImageRequest", url_for("kobo.HandleCoverImageRequest",
auth_token=kobo_auth.get_auth_token(), auth_token=get_auth_token(),
book_uuid="{ImageId}", book_uuid="{ImageId}",
width="{width}", width="{width}",
height="{height}", height="{height}",
@@ -1104,7 +1143,7 @@ def HandleInitRequest():
else: else:
kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/") kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/")
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest", kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
auth_token=kobo_auth.get_auth_token(), auth_token=get_auth_token(),
book_uuid="{ImageId}", book_uuid="{ImageId}",
width="{width}", width="{width}",
height="{height}", height="{height}",
@@ -1112,7 +1151,7 @@ def HandleInitRequest():
isGreyscale='isGreyscale', isGreyscale='isGreyscale',
_external=True)) _external=True))
kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest", kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
auth_token=kobo_auth.get_auth_token(), auth_token=get_auth_token(),
book_uuid="{ImageId}", book_uuid="{ImageId}",
width="{width}", width="{width}",
height="{height}", height="{height}",
@@ -1128,6 +1167,7 @@ def HandleInitRequest():
@kobo.route("/download/<book_id>/<book_format>") @kobo.route("/download/<book_id>/<book_format>")
@requires_kobo_auth @requires_kobo_auth
@download_required @download_required
# @limiter.limit("3/minute", key_func=get_remote_address)
def download_book(book_id, book_format): def download_book(book_id, book_format):
return get_download_link(book_id, book_format, "kobo") return get_download_link(book_id, book_format, "kobo")
@@ -1299,3 +1339,5 @@ def NATIVE_KOBO_RESOURCES():
"userguide_host": "https://ereaderfiles.kobo.com", "userguide_host": "https://ereaderfiles.kobo.com",
"wishlist_page": "https://www.kobo.com/{region}/{language}/account/wishlist" "wishlist_page": "https://www.kobo.com/{region}/{language}/account/wishlist"
} }
limiter.limit("3/minute", key_func=get_remote_address)(kobo)

View File

@@ -62,14 +62,12 @@ particular calls to non-Kobo specific endpoints such as the CalibreWeb book down
from binascii import hexlify from binascii import hexlify
from datetime import datetime from datetime import datetime
from os import urandom from os import urandom
from functools import wraps
from flask import g, Blueprint, abort, request from flask import g, Blueprint, request
from .cw_login import login_user, current_user from .cw_login import current_user
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_limiter import RateLimitExceeded
from . import logger, config, calibre_db, db, helper, ub, lm, limiter from . import logger, config, calibre_db, db, helper, ub, lm
from .render_template import render_title_template from .render_template import render_title_template
from .usermanagement import user_login_required from .usermanagement import user_login_required
@@ -135,13 +133,6 @@ def disable_failed_auth_redirect_for_blueprint(bp):
lm.blueprint_login_views[bp.name] = None lm.blueprint_login_views[bp.name] = None
def get_auth_token():
if "auth_token" in g:
return g.get("auth_token")
else:
return None
def register_url_value_preprocessor(kobo): def register_url_value_preprocessor(kobo):
@kobo.url_value_preprocessor @kobo.url_value_preprocessor
# pylint: disable=unused-variable # pylint: disable=unused-variable
@@ -149,28 +140,3 @@ def register_url_value_preprocessor(kobo):
g.auth_token = values.pop("auth_token") g.auth_token = values.pop("auth_token")
def requires_kobo_auth(f):
@wraps(f)
def inner(*args, **kwargs):
auth_token = get_auth_token()
if auth_token is not None:
#try:
# limiter.check()
#except RateLimitExceeded:
# return abort(429)
#except (ConnectionError, Exception) as e:
# log.error("Connection error to limiter backend: %s", e)
# return abort(429)
user = (
ub.session.query(ub.User)
.join(ub.RemoteAuthToken)
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
.first()
)
if user is not None:
login_user(user)
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
return f(*args, **kwargs)
log.debug("Received Kobo request without a recognizable auth token.")
return abort(401)
return inner

View File

@@ -46,11 +46,10 @@ def main():
try: try:
from .kobo import kobo, get_kobo_activated from .kobo import kobo, get_kobo_activated
from .kobo_auth import kobo_auth from .kobo_auth import kobo_auth
from flask_limiter.util import get_remote_address
kobo_available = get_kobo_activated() kobo_available = get_kobo_activated()
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator) except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
kobo_available = False kobo_available = False
kobo = kobo_auth = get_remote_address = None kobo = kobo_auth = None
try: try:
from .oauth_bb import oauth from .oauth_bb import oauth
@@ -77,7 +76,6 @@ def main():
app.register_blueprint(gdrive) app.register_blueprint(gdrive)
app.register_blueprint(editbook) app.register_blueprint(editbook)
if kobo_available: if kobo_available:
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
app.register_blueprint(kobo) app.register_blueprint(kobo)
app.register_blueprint(kobo_auth) app.register_blueprint(kobo_auth)
if oauth_available: if oauth_available:

View File

@@ -43,14 +43,14 @@ def verify_password(username, password):
if config.config_login_type == constants.LOGIN_LDAP and services.ldap: if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
login_result, error = services.ldap.bind_user(user.name, password) login_result, error = services.ldap.bind_user(user.name, password)
if login_result: if login_result:
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] [limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
return user return user
if error is not None: if error is not None:
log.error(error) log.error(error)
else: else:
# limiter.check() # limiter.check()
if check_password_hash(str(user.password), password): if check_password_hash(str(user.password), password):
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] [limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
return user return user
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr) 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) log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
@@ -120,7 +120,7 @@ def load_user_from_reverse_proxy_header(req):
if rp_header_username: if rp_header_username:
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first() user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first()
if user: if user:
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] [limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
return user return user
return None return None

View File

@@ -30,7 +30,6 @@ from flask import session as flask_session
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_babel import get_locale from flask_babel import get_locale
from .cw_login import login_user, logout_user, 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 flask_limiter.util import get_remote_address
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import text, func, false, not_, and_, or_ from sqlalchemy.sql.expression import text, func, false, not_, and_, or_
@@ -39,7 +38,7 @@ from sqlalchemy.sql.functions import coalesce
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from . import constants, logger, isoLanguages, services from . import constants, logger, isoLanguages, services, limiter
from . import db, ub, config, app from . import db, ub, config, app
from . import calibre_db, kobo_sync_status from . import calibre_db, kobo_sync_status
from .search import render_search_results, render_adv_search_results from .search import render_search_results, render_adv_search_results
@@ -55,7 +54,6 @@ from .usermanagement import login_required_if_no_ano
from .kobo_sync_status import remove_synced_book from .kobo_sync_status import remove_synced_book
from .render_template import render_title_template from .render_template import render_title_template
from .kobo_sync_status import change_archived_books from .kobo_sync_status import change_archived_books
from . import limiter
from .services.worker import WorkerThread from .services.worker import WorkerThread
from .tasks_status import render_task_status from .tasks_status import render_task_status
from .usermanagement import user_login_required from .usermanagement import user_login_required
@@ -1285,15 +1283,6 @@ def register_post():
if not config.config_public_reg: if not config.config_public_reg:
abort(404) abort(404)
to_save = request.form.to_dict() to_save = request.form.to_dict()
#try:
# limiter.check()
#except RateLimitExceeded:
# flash(_(u"Please wait one minute to register next user"), category="error")
# return render_title_template('register.html', config=config, title=_("Register"), page="register")
#except (ConnectionError, Exception) as e:
# log.error("Connection error to limiter backend: %s", e)
# flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
# return render_title_template('register.html', config=config, title=_("Register"), page="register")
if current_user is not None and current_user.is_authenticated: if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if not config.get_mail_server_configured(): if not config.get_mail_server_configured():
@@ -1358,7 +1347,7 @@ def register():
def handle_login_user(user, remember, message, category): def handle_login_user(user, remember, message, category):
login_user(user, remember=remember) login_user(user, remember=remember)
flash(message, category=category) flash(message, category=category)
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] [limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
return redirect(get_redirect_location(request.form.get('next', None), "web.index")) return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
@@ -1392,15 +1381,6 @@ def login():
def login_post(): def login_post():
form = request.form.to_dict() form = request.form.to_dict()
username = strip_whitespaces(form.get('username', "")).lower().replace("\n","").replace("\r","") username = strip_whitespaces(form.get('username', "")).lower().replace("\n","").replace("\r","")
#try:
# limiter.check()
#except RateLimitExceeded:
# flash(_("Please wait one minute before next login"), category="error")
# return render_login(username, form.get("password", ""))
#except (ConnectionError, Exception) as e:
# log.error("Connection error to limiter backend: %s", e)
# flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
# return render_login(username, form.get("password", ""))
if current_user is not None and current_user.is_authenticated: if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap: if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:

View File

@@ -33,26 +33,26 @@ dependencies = [
"Flask>=1.0.2,<3.2.0", "Flask>=1.0.2,<3.2.0",
"iso-639>=0.4.5,<0.5.0;python_version<'3.12'", "iso-639>=0.4.5,<0.5.0;python_version<'3.12'",
"pycountry>=20.0.0,<25.0.0;python_version>='3.12'", "pycountry>=20.0.0,<25.0.0;python_version>='3.12'",
"PyPDF>=6.1.3,<6.5.0", "PyPDF>=6.1.3,<6.7.0",
"pytz>=2016.10", "pytz>=2016.10",
"requests>=2.32.0,<2.33.0", "requests>=2.32.0,<2.33.0",
"SQLAlchemy>=1.3.0,<2.1.0", "SQLAlchemy>=1.3.0,<2.1.0",
"tornado>=6.4.2,<6.6", "tornado>=6.4.2,<6.6",
"Wand>=0.4.4,<0.7.0", "Wand>=0.4.4,<0.7.0",
"unidecode>=0.04.19,<1.4.0", "unidecode>=0.04.19,<1.5.0",
"lxml>=4.9.1,<5.4.0", "lxml>=4.9.1,<5.4.0",
"flask-wtf>=0.14.2,<1.3.0", "flask-wtf>=0.14.2,<1.3.0",
"chardet>=3.0.0,<5.3.0", "chardet>=3.0.0,<5.3.0",
"netifaces-plus>=0.12.0,<0.13.0", "netifaces-plus>=0.12.0,<0.13.0",
"urllib3>=1.22,<3.0", "urllib3>=1.22,<3.0",
"Flask-Limiter>=2.3.0,<3.13.0", "Flask-Limiter>=2.3.0,<4.2.0",
"regex>=2022.3.2,<2025.3.20", "regex>=2022.3.2,<2026.1.16",
"bleach>=6.0.0,<6.3.0", "bleach>=6.0.0,<6.4.0",
"python-magic>=0.4.27,<0.5.0", "python-magic>=0.4.27,<0.5.0",
"python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'", "python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'",
"flask-httpAuth>=4.4.0,<5.0.0", "flask-httpAuth>=4.4.0,<5.0.0",
"cryptography>=39.0.0,<45.0.0", "cryptography>=39.0.0,<47.0.0",
"certifi>=2024.7.4,<2025.8.24", "certifi>=2024.7.4,<2026.1.5",
] ]
dynamic = ["version"] dynamic = ["version"]
@@ -70,9 +70,9 @@ content-type = "text/markdown"
[project.optional-dependencies] [project.optional-dependencies]
gdrive = [ gdrive = [
"google-api-python-client>=2.73.00,<2.200.0", "google-api-python-client>=2.73.00,<2.200.0",
"gevent>20.6.0,<24.12.0", "gevent>20.6.0,<25.9.2",
"greenlet>=0.4.17,<3.3.0", "greenlet>=0.4.17,<3.4.0",
"httplib2>=0.9.2,<0.23.0", "httplib2>=0.9.2,<0.32.0",
"oauth2client>=4.0.0,<4.1.4", "oauth2client>=4.0.0,<4.1.4",
"uritemplate>=3.0.0,<4.3.0", "uritemplate>=3.0.0,<4.3.0",
"pyasn1-modules>=0.0.8,<0.7.0", "pyasn1-modules>=0.0.8,<0.7.0",
@@ -91,19 +91,19 @@ goodreads = [
] ]
ldap = [ ldap = [
"python-ldap>=3.0.0,<3.5.0", "python-ldap>=3.0.0,<3.5.0",
"Flask-SimpleLDAP>=1.4.0,<2.1.0", "Flask-SimpleLDAP>=1.4.0,<2.2.0",
] ]
oauth = [ oauth = [
"Flask-Dance>=2.0.0,<7.2.0", "Flask-Dance>=2.0.0,<7.2.0",
"SQLAlchemy-Utils>=0.33.5,<0.42.0", "SQLAlchemy-Utils>=0.33.5,<0.43.0",
] ]
metadata = [ metadata = [
"rarfile>=3.2,<5.0", "rarfile>=3.2,<5.0",
"scholarly>=1.2.0,<1.8", "scholarly>=1.2.0,<1.8",
"markdown2>=2.0.0,<2.6.0", "markdown2>=2.0.0,<2.6.0",
"html2text>=2020.1.16,<2025.2.26", "html2text>=2020.1.16,<2025.4.16",
"python-dateutil>=2.1,<2.10.0", "python-dateutil>=2.1,<2.10.0",
"beautifulsoup4>=4.0.1,<4.14.0", "beautifulsoup4>=4.0.1,<4.15.0",
"faust-cchardet>=2.1.18,<2.1.20", "faust-cchardet>=2.1.18,<2.1.20",
"py7zr>=0.15.0,<0.21.0", "py7zr>=0.15.0,<0.21.0",
"mutagen>=1.40.0,<1.50.0", "mutagen>=1.40.0,<1.50.0",

View File

@@ -37,20 +37,20 @@
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> <div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
<p class='text-justify attribute'><strong>Start Time: </strong>2026-01-24 20:50:12</p> <p class='text-justify attribute'><strong>Start Time: </strong>2026-02-17 19:21:22</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Stop Time: </strong>2026-01-25 04:15:44</p> <p class='text-justify attribute'><strong>Stop Time: </strong>2026-02-18 02:50:40</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Duration: </strong>6h 13 min</p> <p class='text-justify attribute'><strong>Duration: </strong>6h 14 min</p>
</div> </div>
</div> </div>
</div> </div>
@@ -2039,13 +2039,13 @@
<tr id="su" class="errorClass"> <tr id="su" class="failClass">
<td>TestLoadMetadata</td> <td>TestLoadMetadata</td>
<td class="text-center">1</td> <td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">1</td> <td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c18', 1)">Detail</a> <a onclick="showClassDetail('c18', 1)">Detail</a>
</td> </td>
@@ -2053,27 +2053,26 @@
<tr id="et18.1" class="none bg-info"> <tr id="ft18.1" class="none bg-danger">
<td> <td>
<div class='testcase'>TestLoadMetadata - test_load_metadata</div> <div class='testcase'>TestLoadMetadata - test_load_metadata</div>
</td> </td>
<td colspan='6'> <td colspan='6'>
<div class="text-center"> <div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et18.1')">ERROR</a> <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft18.1')">FAIL</a>
</div> </div>
<!--css div popup start--> <!--css div popup start-->
<div id="div_et18.1" class="popup_window test_output" style="display:block;"> <div id="div_ft18.1" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'> <div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();" <button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_et18.1').style.display='none'"><span onclick="document.getElementById('div_ft18.1').style.display='none'"><span
aria-hidden="true">&times;</span></button> aria-hidden="true">&times;</span></button>
</div> </div>
<div class="text-left pull-left"> <div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last): <pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py&#34;, line 87, in test_load_metadata File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py&#34;, line 220, in test_load_metadata
if results[cont][&#39;source&#39;] == &#39;https://comicvine.gamespot.com/&#39;: self.assertLessEqual(diff(BytesIO(cover), BytesIO(new_cover), delete_diff_file=True), 0.025)
~~~~~~~^^^^^^ AssertionError: 0.04218017335689641 not less than or equal to 0.025</pre>
IndexError: list index out of range</pre>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
@@ -2270,13 +2269,13 @@ IndexError: list index out of range</pre>
<tr id="su" class="failClass"> <tr id="su" class="passClass">
<td>TestLoadMetadataScholar</td> <td>TestLoadMetadataScholar</td>
<td class="text-center">1</td> <td class="text-center">1</td>
<td class="text-center">0</td>
<td class="text-center">1</td> <td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c20', 1)">Detail</a> <a onclick="showClassDetail('c20', 1)">Detail</a>
</td> </td>
@@ -2284,31 +2283,11 @@ IndexError: list index out of range</pre>
<tr id="ft20.1" class="none bg-danger"> <tr id='pt20.1' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestLoadMetadataScholar - test_load_metadata</div> <div class='testcase'>TestLoadMetadataScholar - test_load_metadata</div>
</td> </td>
<td colspan='6'> <td colspan='6' align='center'>PASS</td>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft20.1')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft20.1" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_ft20.1').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_metadata_scholar.py&#34;, line 74, in test_load_metadata
self.assertEqual(30, len(results))
AssertionError: 30 != 20</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
@@ -4684,11 +4663,11 @@ AssertionError: 30 != 20</pre>
<tr id="su" class="failClass"> <tr id="su" class="skipClass">
<td>TestThumbnails</td> <td>TestThumbnails</td>
<td class="text-center">8</td> <td class="text-center">8</td>
<td class="text-center">6</td> <td class="text-center">7</td>
<td class="text-center">1</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">1</td> <td class="text-center">1</td>
<td class="text-center"> <td class="text-center">
@@ -4761,31 +4740,11 @@ AssertionError: 30 != 20</pre>
<tr id="ft52.8" class="none bg-danger"> <tr id='pt52.8' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestThumbnails - test_sideloaded_book</div> <div class='testcase'>TestThumbnails - test_sideloaded_book</div>
</td> </td>
<td colspan='6'> <td colspan='6' align='center'>PASS</td>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft52.8')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft52.8" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_ft52.8').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_thumbnails.py&#34;, line 317, in test_sideloaded_book
self.assertAlmostEqual(diff(BytesIO(list_cover), BytesIO(old_list_cover), delete_diff_file=True), 0.0,
AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182775 difference)</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
@@ -5992,9 +5951,9 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr id='total_row' class="text-center bg-grey"> <tr id='total_row' class="text-center bg-grey">
<td>Total</td> <td>Total</td>
<td>538</td> <td>538</td>
<td>528</td> <td>530</td>
<td>2</td>
<td>1</td> <td>1</td>
<td>0</td>
<td>7</td> <td>7</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
@@ -6023,7 +5982,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>Platform</th> <th>Platform</th>
<td>Linux 6.8.0-90-generic #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025 x86_64 x86_64</td> <td>Linux 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64 x86_64</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@@ -6041,19 +6000,19 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>babel</th> <th>babel</th>
<td>2.17.0</td> <td>2.18.0</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
<tr> <tr>
<th>bleach</th> <th>bleach</th>
<td>6.2.0</td> <td>6.3.0</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
<tr> <tr>
<th>certifi</th> <th>certifi</th>
<td>2025.8.3</td> <td>2026.1.4</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@@ -6065,7 +6024,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>cryptography</th> <th>cryptography</th>
<td>44.0.3</td> <td>46.0.5</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@@ -6089,7 +6048,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>Flask-Limiter</th> <th>Flask-Limiter</th>
<td>3.12</td> <td>4.1.1</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@@ -6137,7 +6096,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>pypdf</th> <th>pypdf</th>
<td>6.4.2</td> <td>6.6.2</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@@ -6155,7 +6114,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>regex</th> <th>regex</th>
<td>2024.11.6</td> <td>2026.1.15</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@@ -6179,7 +6138,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>Unidecode</th> <th>Unidecode</th>
<td>1.3.8</td> <td>1.4.0</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@@ -6203,7 +6162,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.188.0</td> <td>2.190.0</td>
<td>TestBackupMetadataGdrive</td> <td>TestBackupMetadataGdrive</td>
</tr> </tr>
@@ -6233,7 +6192,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.188.0</td> <td>2.190.0</td>
<td>TestCliGdrivedb</td> <td>TestCliGdrivedb</td>
</tr> </tr>
@@ -6263,7 +6222,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.188.0</td> <td>2.190.0</td>
<td>TestEbookConvertCalibreGDrive</td> <td>TestEbookConvertCalibreGDrive</td>
</tr> </tr>
@@ -6293,7 +6252,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.188.0</td> <td>2.190.0</td>
<td>TestEbookConvertGDriveKepubify</td> <td>TestEbookConvertGDriveKepubify</td>
</tr> </tr>
@@ -6335,7 +6294,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.188.0</td> <td>2.190.0</td>
<td>TestEditAuthorsGdrive</td> <td>TestEditAuthorsGdrive</td>
</tr> </tr>
@@ -6371,7 +6330,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.188.0</td> <td>2.190.0</td>
<td>TestEditBooksOnGdrive</td> <td>TestEditBooksOnGdrive</td>
</tr> </tr>
@@ -6413,7 +6372,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.188.0</td> <td>2.190.0</td>
<td>TestEmbedMetadataGdrive</td> <td>TestEmbedMetadataGdrive</td>
</tr> </tr>
@@ -6443,7 +6402,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.188.0</td> <td>2.190.0</td>
<td>TestSetupGdrive</td> <td>TestSetupGdrive</td>
</tr> </tr>
@@ -6496,8 +6455,8 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
</tr> </tr>
<tr> <tr>
<th>Flask-SimpleLDAP</th> <th>flask-simpleldap</th>
<td>2.0.0</td> <td>2.1.0</td>
<td>TestLdapLogin</td> <td>TestLdapLogin</td>
</tr> </tr>
@@ -6545,7 +6504,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
</div> </div>
<script> <script>
drawCircle(528, 2, 1, 7); drawCircle(530, 1, 0, 7);
showCase(5); showCase(5);
</script> </script>